Allowing the object.method(args...) syntax as an alias for method(object, args ...)

Yes this is why I picked it: assurance that implementing it in the language wouldn’t cause any breaking changes.

Much too inconvenient. If I had to recite the Lord’s Prayer every time I expressed my thoughts, I’d never express my thoughts.

Better yet, we could use ↦ (\mapsto<tab>) for anonymous function definitions, to be consistent with mathematics (as everyone knows, → is used for specifying a function’s domain and codomain, e.g. 𝑓 : ℝ → ℝ⁺, while ↦ is used for function definitions, e.g. 𝑥 ↦ 𝑥² :stuck_out_tongue_winking_eye:)

I’m kidding, I’m kidding. Maybe. I appreciate the openness to changing the language. But imo, there would need to be a very compelling reason to break anonymous function syntax. I’d rather break .. than ->, but I’m growing partial to using -- for UFCS method calls since it wouldn’t need any breaking changes, is reasonably easy to type, and can easily be verbalized as “dash.”

Another reminder: UFCS is not an object-oriented concept, so let’s not confuse it with OOP. (I might have to append this reminder to every message. :laughing:)

Yeah, I get the hangup on map, reduce, filter, etc.: functions that describe sentences of the form, “Do something when applying this behavior to that collection.” Perhaps they could have as easily followed the form, “Do something when taking this collection and applying that behavior to it,” but that’s not how the language authors decided to do it. [Not true; a function call of the former form is transformed into the latter form when using the do statement! :sweat_smile:]

UFCS syntax isn’t optimal for chaining using these methods, I agree. However,

  • most functions don’t follow this form of taking a function as a first argument,
  • broadcasting covers most use cases you’d otherwise use map for, and
  • there are additional benefits to UFCS beyond chaining, such as autocomplete and the simple ability to express thoughts in noun→verb form,

so let’s not throw the babies out with the bathwater.

Further, so long as the object is captured, UFCS can still bring some convenience. Example:

arr = collect(-2:2)
# cube each element and filter by positives
let cube(x) = x^3, ispositive(x) = x > 0
    arr--(cube--map)()--(ispositive--filter)() == [1, 8] # true
end

In English it’d read like: “Take the array, apply a cube mapping, and then apply a positive filtering.” Unfortunately it gets ugly if the anonymous functions aren’t named.

Or since these are now single-argument functions, one could chain with pipes to avoid parentheses:

arr |> cube--map |> ispositive--filter

or compose a function as:

newfunc = ispositive--filter ∘ cube--map
newfunc(arr)

Admittedly it’d be nicer to say “Take the array, map onto it the cubing function, then filter it by positives” but I’m working with what I’ve got until I can find good syntax for sending the object to the last argument.

You would never ever ever want to do this, but for illustration:

mapreduce(√, +, arr) == (arr |> (+ -- (√ -- mapreduce)))

Again though, UFCS is better suited to the greater universe of functions that don’t take a function as a first argument, such as

"Hello, world!"--split(",")--replace.("o"=>"e")--join(":") == "Helle: werld!" # true
document--root()--firstelement()--setnodecontent!("Hello, world!")

There’s a big world of functions out there structured like this.

Oh! I am honored. My favorite bit is the syntax of any expression of _ becoming a single-argument anonymous function. That feels like it should be a core language feature.

The point of discussion is which features that make common human thought patterns convenient, concise, and easy to express should be in the language, regardless what specific problem is being solved with the code and what packages are loaded for it. Being careful of course to select only the thought patterns which are so common and so useful that it’d be silly not to implement them, especially as we approach a new major version.

Imagine if the “if you want convenience just make a macro and package it” attitude was applied to list comprehensions, matrices, broadcasting, or any of Julia’s best and most frequently used syntax sugar. Imagine all the syntax clashes with other packages!:woozy_face:

P.S. Taking inspiration DataPipes.jl and from Clojure’s -> threading into first argument and ->> threading into last argument, I’m contemplating --- to pass the object into the last non-keyword argument. Maybe? :thinking:

arr---map(cube)---filter(ispositive)

Then the ugly example from above could be

mapreduce(√, +, arr) == arr---mapreduce(√, +)

--- could be verbalized as “em-dash,” which appropriately sounds like “end-dash.”

Hu, now you seem to get carried away a bit … let me just comment on some of your ideas:

True, but I would say it just hides/fixes an inconsistency in languages which have functions and OOP methods. Languages using either one of them exclusively have much less need for such, e.g., Haskell does not have or need a special syntax for method or even field access – its all just functions – and Smalltalk has no special syntax for function call.
Imho Smalltalk has one of the best syntax for message-passing OOP though, e.g.,

arr at: 1 put: "Hello"; at: 2 put: "there"; yourself.

Now, you seem to overload the syntax in that it does currying, i.e., fun = cube -- map is a function, whereas arr--fun applies that function to produce a value. In Haskell that is quite convenient, but it is hard to support in languages with variadic functions as the compiler would not know if map(f, x) is already complete or you wanted y -> map(f, x, y). It’s a compromise in the end, but you probably can’t have both without something more explicit like partial above in Julia (which has a lot of variadic functions).

That is true, but language syntax requires a lot of thought about the proper abstractions/semantics behind it to be widely useful. Just consider how long the development of LINQ took, which is basically monad comprehensions, yes, the famous Monads from Haskell that is (I have ranted elsewhere why I don’t like the do-notation for monads).

Well, to my eyes it appears quite similar to read and type. Yet, if you really want to express your thoughts in a notation it’s important that it supports formal manipulations, i.e., to prove equality between seemingly different implementations or derive novel algorithmic ideas in the first place. I know just about two notations/languages which can and have been used for that purpose, namely APL and Squiggol.

I think it’s a problem to make programming languages adopt English grammar.

  1. Many major languages diverge from English grammar, and English grammar isn’t so rigid either (passive vs active voices switches the order of subject and object). I know programming languages already use English words, but I don’t think we should make things even harder for non-English speakers.
  2. Even if we want to, there is a limit to how much we can make programming languages resemble natural languages. COBOL was intended to be English-like, but it barely bore a passing resemblance to human speech, and other languages since then have quickly converged on less wordy ways to structure programs.
  3. Variables aren’t so easily classifiable as subjects vs objects vs verbs. Any type can have callable instances, so functors and nested functions can act as either noun or verb. We have lots of higher-order functions too, where the “verbs” become “nouns”.
  4. Having many syntax options to write the same program complicates the language. It may make sense for DSL macros to remove tons of boilerplate in specific libraries, but it’ll just be confusing for newcomers to learn more ways to just do function calls in the core language. You had a similar criticism about Chain.jl and Pipe.jl, and this would just add another (less flexible) syntax to that pile. Replacing piping with one of the enhanced piping libraries seems simpler and more beneficial than incorporating your additional syntax.

I mean, we could just do that in separate lines in order: peeled_orange = peel(fingernails, orange); slices = split(peeled_orange); put!(bowl, slices); for i in 1:length(bowl) eatfrom!(bowl) end. Piping just saves us from making temporary variables if we can conceptualize the program as a series of transformations on a special input, which we can’t always do e.g. binary operations.

Just one idea, really :sweat_smile:

This is a misunderstanding. arr--fun is a function, and arr--fun() applies the function to produce a value. For illustration purposes, imagine -- and --- as binary operators defined as

--(obj, meth)  = (args...; kwargs...) -> meth(obj, args...; kwargs...)
---(obj, meth) = (args...; kwargs...) -> meth(args..., obj; kwargs...)

such that arr--fun produces a function, (args...; kwargs...) -> fun(arr, args...; kwargs...).

This is a strawman argument.

I’m not proposing to create a JIT compiler for the English language! Just to support noun→verb ordering, which we already do with the |> pipe, in a way that’s more powerful (ability to pass other arguments) and more convenient (typing the |> characters is error-prone). That. Is. All.

I only offer natural language examples to draw analogies and to demonstrate how common these orderings are. If we really dive into the weeds, UFCS is exactly not supported in English sentences. Languages like English and Chinese are SVO (subject-verb-object) languages, and since the computer is implicitly the subject in all procedural commands, the syntax mymethod(my_object) is already exactly in SVO order. Languages like Latin, Hindi and Korean are SOV (subject-object-verb) languages, and the UFCS syntax my_object--mymethod() is in this order. UFCS is closer to Sanskrit than to English!

All that to say, it’s remarkable that English speakers can learn object-verb ordering and came to prefer it in many contexts despite it being less natural to us; it’s probable that there’s something special about it.

Anyway, the point is to support common thought patterns regardless of the programmer’s native tongue. And even though English is an SVO language, mentally we do employ object→verb→verb→verb chaining.

Note: despite being a SVO language, thankfully English provides a keyword explicitly for convenient chaining: the word “it,” which allows us to keep the object in memory and continue calling verbs on it without renaming it at each step.

Agree, hence why it’s prudent to ensure they’re the most useful ones. What has stood the test of time seems like a decent measure, which UFCS has done, satisfying multiple generations (boomers, gen x, millennials, and zoomers) despite all manner of other fashions rising and falling through those decades.

Is it just a persistent but ultimately doomed ugly fad, like Java? Possible. But considering that the word “it” doesn’t seem to be fading from the English language, I think not.

Finally a reasonable statement. I might possibly be persuaded, let’s see. I suspect @aplavin can provide the best arguments (or counter-arguments) here.

Yes. Or at least, subroutines of the program. Very handy.

2 Likes

That’s exactly what I mean by making programming languages adopt English grammar, maybe I should’ve been more precise. I think your proposal would be clearer and more convincing if you talked about programming/math linguistics, e.g. prefix vs postfix notation, instead of natural languages and all their needless complications. Variables, instances, and functions really just don’t fit well into nouns, subjects, objects, and verbs.

Passive voice is pretty natural “this program was written years ago.”

I think putting an input through a series of input-output systems is more of a math and engineering concept. I definitely can’t think of a language with long serial verb chains.

I think -> is widely used than .. in any OOP language. It’s important to have consistence with other languages, to attract potential users, given that this will become the mostly used syntax once we add this to the language. In addition, -> and ‘.’ are thoroughly tested by programmers and become standard for OOP language. So it’s simply impossible for .. or other unicode notations to be adopted into the language.

Another consideration here is that I believe anonymous functions will be discouraged in the future, due to the growing desire of static compilation. Therefore, library writers will prefer using functor (singleton type which subtypes Function). This will make anonymous functions less useful in Julia. Therefore, it’s reasonable to use a more complicated syntax (whatever it’s ->>, |->, \-> ) for them.

Fair.

Interesting point. I definitely don’t think in a passive voice when chaining function calls, but maybe other people do :sweat_smile:.

The example I gave of peeling the orange is one such verb chain. It uses “it” to continue “threading” (to use Clojure language) the orange through successive verb calls.

Can you elaborate on how you got to that conclusion? Anonymous functions already subtype Function:

julia> anon = x -> 2x
#3 (generic function with 1 method)

julia> typeof(anon)
var"#3#4"

julia> typeof(anon) |> supertype
Function

I also don’t see how anonymous functions themselves prevent static compilation - they are just callable structs under the hood, capturing the state required for closures.

3 Likes

I disagree. Julia is not an object-oriented language, and the behavior I’m proposing is not the same as the -> operator in C/C++, so using -> here would be a breaking change that at best adds mild visual appeal and at worst just adds confusion.

Also, -> doesn’t have that meaning in many other OOP languages anyway (in Java and Ruby it’s used for lambda functions; JavaScript has no such operator afaik; in Python it declares the function’s codomain).

Anyway, I’ve already discussed a similar topic here:

Fair enough, should have said arguments.

Indeed, I had missed that – it was late already :grinning:.
But then, your only issue is that -- and --- are invalid infix operators, right? Other than that your functions implement the desired functionality, i.e., without the need for macros or even changes in lowering etc.

Let me try to define infix operators using any of the still available symbols and see how it works out. Looks like and are not taken yet.
In a sense, would give reverse Polish notation with an explicit spacing and marker when you apply a verb, i.e.

⧺(obj, meth)  = (args...; kwargs...) -> meth(obj, args...; kwargs...)

((3 ⧺ (4 ⧺ (-)))() ⧺ (5 ⧺ (+)))()

which does not look nice currently, as the operator would need to associate to the right and bind more strongly than function application, i.e., 3 ⧺ 4 ⧺ (-)() ⧺ 5 ⧺ (+)(). Thus, even though it’s all functions the Julia parser would still need changes to support the right infix operator.
But, now I like your idea, i.e., Forth I see you! Would still be reluctant to allow user-defined infix operators a la Haskell though.

Especially on a German keyboard, typing |> is even worse. I’m not convinced though that -- and --- are less error prone, i.e., given that - is already widely used it reminds me of the equality confusion in Javascript etc.

1 Like

Why not simply make a vote? This is easier. I think at least I can do this in the Chinese community and let people choose over -> and ... We have a telegram group with thousands of people, therefore we can investigate the popularity of different syntax. So the options will be like:

  1. Break syntax of anonymous function and use -> for dot call
  2. Use .., |-> or any other symbols
  3. Do nothing and use macro or any other things

Ok, let me start:

  1. No
  2. Maybe, but get the associativity right and remember that in the latest proposal -- was no longer UFCS, but reverse partial application which is much more powerful (and useful?).
  3. Yes
1 Like

Yes! Now we’re on the same wavelength.

In addition to binding more tightly, changes might be needed to make broadcasting work properly. But yes, it almost works as-is.

My claim that -- and --- are less error-prone has to do with how easy it is to hit the right keys. I haven’t yet fully thought through the ramifications on readability, but so far I don’t dislike it. Could use some road testing perhaps.

You’re right, calling it “UFCS” was probably a mistake, considering that UFCS still fashions its syntax to mimic property getting. I never realized that what I’ve been describing could be used for Reverse Polish Notation though! :exploding_head:

3 -- 4 -- -() -- 5 -- +() == 6 # true

I’ll never get used to that. All I wanted was obj--meth1(args1...)--meth2(args2...) :laughing:

Easier to do, harder to do meaningfully. High probability of degrading the SNR of the discussion to zero.

1 Like

Was just thinking it through :grinning:
In the arithmetic example, the result might be somewhat unexpected when associating to the right:

  • The above parses as (5 + (4 - 3)), i.e., reads from the back as -- inserts as the first argument.
  • Yet, [1,2,3] -- iseven -- filter() -- sqrt -- map() would give the intended map(sqrt, filter(iseven, [1,2,3])).
2 Likes

That’s a very interesting realization. How cool! Right-associativity basically eliminates the need for ---.

Thanks for helping me think this through, it’s looking even better than I had intended. :sweat_smile: Glad to have people smarter than me looking at this!

1 Like

Eventually, you need to let people vote over these things (suppose we indeed want to move towards this direction and we do have several options to choose), because the usage of syntax sugar is to please most users. So a vote can tell us how people prefer different syntax.

Anyway, since some core team developers have already showed their objection to our proposals, whatever it’s ->, .. or --, I guess it will take a long time for this feature to land in Julia (or maybe never), unless some really influential Julian or companies in Julia community make their needs explicit to core terms. I think if you really really want to make your proposal into Julia, then you should spend some time convincing them instead of me or other people, because they are the one who have rights to make final decisions. My personal preference over syntax may never be changed.

1 Like
7 Likes

Parentheses are more straightforward when there are 3+ calls in a branching order like f( g(a, b), h(x, y, z) ), even if we wrote the callable last like ( (a, b)g, (x, y, z)h )f.

Reverse Polish notation works without any parentheses sometimes because each operation/method/function only consumes a fixed number of arguments on a stack. So if you treat + and * as binary operations, a b + c d + * is interpreted as *( +(a,b), +(c,d) ), rather than *(+( +(a,b), c, d )). Unfortunately/fortunately, multimethods or overloaded functions can have call signatures with different number of arguments, so parentheses become crucial. For example, your notation could’ve been interpreted instead as map(sqrt, filter(iseven), [1,2,3]).

In general, that is correct. The cool thing about this notation though is that () explicitly marks an application, i.e., -- simply puts its arguments onto the stack and never applies a function. Everything on the stack is then consumed by the function on top of the stack on (). Thus, [1,2,3] -- iseven -- filter () -- sqrt -- map () is unambiguously interpreted as map(sqrt, filter(iseven, [1,2,3])).
On the other hand, it means that the interpretation *( +(a , b), +(c, d)) needs additional parenthesis, i.e., a -- b -- + () (c -- d -- + ()) -- * (). Seems that every notation has its preferred uses, but the one proposed here seems quite nice for processing pipelines were a single argument is successively transformed on each (), i.e., as in the map, filter chain. Maybe not surprising as this use was the original motivation, i.e., obj -- transform_1 -- transform_2 etc.

1 Like