Love I Julia : the need for a direct object notation

Related Issues?

I’m reading a few issues and dozens of proposals that seem intimately related:

#1263  - allow overloading of a.b field access
#5571  - function chaining
#16985 - custom infix operators

I think there is a challenge at the core of these issues is particularly important. Hence, I’m curious – what is the plan for addressing these issues? What is the current thinking? Will it be done for 1.0?

Direct Object Notation

Julia’s multiple-dispatch is fantastic. In the next decade, I think object-oriented single-dispatch will be seen as quaint and Julia will pave the road to the future. However, Julia risks throwing a baby out with the bath-water: direct object notation.

English    : I love English
Javascript > I.love("Javascript")
Python     > I.love("Python")
Ruby       > I.love("Ruby")
...
Julia      > love(I, "Julia")

I think the primary reason why object oriented languages are so prolific isn’t because of the dispatch object model. It’s because the syntax of these languages follow natural rules: they distinguish and prioritize a direct object over verbs and indirect objects. This syntax differentiation makes these languages easier to read, write, and reason about.

Critically, this notation supports processing pipelines or “fluent interface” library design though method-chaining. These sorts of data processing tools (LINQ, etc.) are central to Julia’s user community and the notational expression is important for usage.

Proposal Review

There are dozens of proposals, so pardon if I’ve missed a few. However, here are my thoughts on them. Let us assume I is a computational object, and love is a function with signature {type(I), String}.

Method Notation

x.f()       => f(x)
x.f(y)      => f(x, y)
x.f(y, ...) =>  f(x, y, ...)

I.love("Julia")
This proposal would somehow permit . to be overridden so that it could do a method lookup on f, rather than finding the matching attribute named f. This has the big advantage of being compatible with OO languages, at a price of being quite magical.

Curry Operator

x |> f(y)  => f(y)(x)

I |> (x -> love(x, "Julia"))
This is the existing |> operator. It could only be used by libraries designed with currying in mind, otherwise it doesn’t solve the direct object notation problem. In particular, it can’t be used with contains(String, String).

1st Arg Pipeline

x |> f()       =>  f(x)
x |> f(y)      =>  f(x, y)
x |> f(y, ...) =>  f(x, y, ...)

I|>love("Julia")
This proposal would change the semantics of the |> to provide the 1st argument from the left-hand-side. It’s clean, but the syntax is a bit noisy. In particular, the > character is distracting which could be an issue for readability.

Placeholder Pipeline

x |> f(_, y)  =>  f(x, y)

This proposal uses _ as a place holder. While it’s a very general syntax, but even more noisy than the previous.
I|>love(_, "Julia")

Nth Arg Pipeline

x |> f(y)  =>  f(y, x)

This mechanism seems unexpected to me. Does it help with chaining? It also doesn’t work with common built-ins, like contains(String, String).
"Julia" |> love(I)

Plain Infix Notation

x f y  =>  f(x, y)

This is a infix only notation which could be chained, but, wouldn’t apply to unary or ternary functions. It’d help, but it wouldn’t address core problem.
I love "Julia"

Parenthesized Infix

(x f y)  =>  f(x, y)

This notation would admit unary and n-ary functions, however, it isn’t chainable. Being chainable is quite important to the core challenge.
(I love "Julia")

Infix Macro

x @f y  =>  @f(x,y)

This path permits infix definition of macros. While this may be better than the previous (it handles n-ary and chains) it means most libraries will be heavy on macros.
I @love "Julia"

Reclaim Bitwise Indicators?

This is the last possible time Julia might consider reclaiming bitwise indicators (especially | character) to address this problem and future language evolution. While bitwise operators are traditional, they are, for the most part, quite uncommon in modern computing – they could easily be given two-character sequence. I suggest that Julia designers may ask if it’s worth reclaiming the pipe (|) indicator, and perhaps reserve more of the single-character bitwise operators for future language evolution. In particular, the 1st Arg proposal could be dramatically improved if the pipline character (|) traditional in shell could be used:

x | f()     => f(x)
x | f(y)    => f(x, y)
x | f(...)  => f(x, ...)

I | love("Julia")

6 Likes

Please see the most recent comments on issue 1974.

3 Likes

Link for convenience: #1974.

Thank you. So as i understand, Julia 1.0 will permit overriding a getfield and setfield!.

This would let those building libraries construct “fluent interfaces” via the period operator, as well as supporting computed attributes. This is preferred over making x.f(y) mean f(x,y) at a syntax level since it permit foreign function interfaces to also enjoy this syntax. Sounds great.

So "Hello!".contains("!") will continue to not “work” unless someone money-patches with a custom override. In this case, perhaps a the binary operator syntax, ("Hello!" contains "!") might be useful regardless, so that it’s not tempting to abuse getfield with built-in types. Once an infix syntax need not chain, this syntax becomes desirable.

One other thing I noticed. It seems bitwise $ will freed up; that is wise. I suggest the core team may want to free up the other bitwise operators, pipe (“|”) and ampersand (“&”) – not just to be consistent, but, to provide syntax options for features to be added in later versions of Julia.

What a lovely language. Thanks.

Edit: Thank you Scott. I wrote this quickly and don’t know how I swapped ^ with &. Anyway, this is a side, perhaps quite weak observation.

Just to be clear, caret (“^”) is not bitwise XOR in julia, it is exponentiation.
The \veebar (LaTeX completion) character ⊻ is now used for xor in Julia starting with v0.6.

I don’t think that getfield and setfield! really address the concern. I’d like a way to invoke any function (not just callable attributes) where the 1st argument appears to the left of the function name:

"I love Julia!" contains "!"

("I love Julia!" contains "!")

"I love Julia!" ?contains("!")

"I love Julia!" |contains("!")

Ideally, such a notation would support n-ary functions, permit function chaining, be easy to type, and be lightweight on the eyes.

I think you are looking for Ruby.

5 Likes

I don’t see how you get there. Chaining isn’t something most libraries do, and there are plenty of infix operators. It’s a big leap to then say the fact you can infix macros for chaining suddenly means packages will overuse macros (something they have consciously not done)

I would hope people wouldn’t abuse some alternative syntax and would just use the standard function + dispatches. It’s easy to read and highly flexible. I don’t want OO notation, which is why I use a language which evolved past it.

@ChrisRackauckas and @ihnorton ; Thank you for reading my post. I use direct object in the English sense – “I love Julia” where “I” is the direct object, “love” is the function, and “Julia” is an indirect object. I am expressly loving Julia because multiple dispatch is a huge improvement on the state of the art. That said, just because Julia isn’t object oriented does not mean it must lack a direct object function invocation notation.

@ChrisRackauckas please pardon my sloppy phrasing – my critique of infix macros is in the context above, to those libraries or users that wish to call functions using a direct object function notation. For example, one could mint a @contains infix macro to permit a user to write "Hello!" @contains "!".

@ihnorton similarly, if one’s goal is to have a direct object function call notation, then #1974 could be abused for this purpose by adding a callable contains attribute to String, permitting one to write "Hello!".contains("!"). I don’t enjoy how object oriented languages promote what should be stand-alone functions to class methods simply because they have a more convenient syntax. But, there is something to it.

30-second experiment:

julia> macro cce(arg1, f::Symbol, arg2...)
           return :( $f($arg1, $arg2...) )
       end
@cce (macro with 1 method)

julia> love(a, b) = a * " ❤ " * b
love (generic function with 1 method)

julia> mystring = @cce "I" love "Julia!"
"I ❤ Julia!"

julia> @cce mystring contains "!"
true

julia> @cce 3 hypot 4 12 # works also with vararg functions
13.0

Do you want to preserve a parsed expression as it is? Add this method:

julia> macro cce(expr::Expr)
           return expr
       end
@cce (macro with 2 methods)

julia> @cce 1 + 2
3

Ok, this isn’t what you’re asking, but Julia lets you easily play with language syntax as you wish :wink:

3 Likes

what is the plan for addressing these issues? What is the current thinking? Will it be done for 1.0?

The answer to these questions is found in the first comment on 16985, an issue which you linked:

Just to set expectations here, I don’t think there’s going to be much in the way of “syntactic innovation” before Julia 1.0. (The only exception I can think of is the new f.(v) vectorized calling syntax.) While having some way of making arbitrary functions behave as infix operators might be nice, it’s just not a pressing issue in the language.

The vectorized calling is done now, and getfield and setfield! overloading are also intended pre-1.0, per the last comments in 1974. There are a handful of other things slated for pre-1.0 unrelated to this question. But that’s it.

3 Likes

how about reuse the do keyword?

I do love(Julia)
"I love Julia!" do contains("!")
10 do sin() do cos() do +(100) do println()
map([1,2,3]) do x
    x do (x->x^2+1)() 
      do exp()
end
3 Likes

How about this? you can make any type callable, so if you want to simulate an object like a python object you can use
the following method.
By the way OO notation is sometimes more convenient to think in, so there is no need to limit yourself.

julia> type Individual
           name::String
       end

julia> function (id::Individual)(f::Function,args...)
           f(id,args...)
       end

julia> function love(self::Individual,stuff)
           msg = "$(self.name) ❤ $(stuff)"
           print(msg)
       end
love (generic function with 1 method)

julia> I = Individual("John")
Individual("John")

julia> I(love,"julia")
John ❤ julia
julia>
3 Likes
LISP> (love I "Julia")

Not trying to be contrarian, but all of the languages you’ve mentioned pull from a rather narrow slice of the Programming Language pie. Not to say that the dotted notation isn’t useful, but there are other ways to address your concerns. In the LISP family (of which Julia counts itself a part) the usual approach is to write a Macro (e.g. the threading ->/->> macros in Clojure).

3 Likes

FYI: There’s already function composition syntax in new 0.6:

… sin ∘ cos …

Maybe someone wants to update (I’m not yet there…)?: Is there an operator for function composition in Julia? - Stack Overflow

1 Like

Thanks for the info, I didn’t know that :slight_smile:

The point was to demonstrate that sentences can be written in a “fluent” way, where do acts like . in c#. I think the syntax would be interesting and useful. However, I do not have the code ability to implement it now :frowning:

Well, then that might be good incentive to dig deep into all the interesting internals of Julia!
Although parts are in Scheme, C, and C++, it’s amazing how much is actually implemented in Julia,
which says a lot about the language that the core devs came up with :wink:

1 Like

About reclaiming the bitwise operators (those that are left!), I think it is false to say they are quite uncommon in modern computing, it simply depends on what sort of code you are working on.
I use the bit operators more heavily than many of the others, because of the type of code I’m writing (library code, code that uses large sparse bit arrays for implementing fast queries, code to implement different numeric and string types).
It’s already a pain that what used to be a single character (as typed) for xor is now 8 (in Emacs, \veebar<tab>) (5 in the REPL (\xor<tab>), that’s used frequently to toggle bits, and | and & are used much more heavily for building up values and masking.

1 Like

For a broader context here, I don’t feel equating clarity with English grammar is a good argument

While English uses Subject-Verb-Object word order to construct a sentence, there are many languages with different ordering, including Verb-Subject-Object order which is like LISP. That is to say, our brains aren’t wired to think is “I love Julia” is the correct order, we can easily get used to “love(I, Julia)”. Specifically, I don’t think linguists would be happy at all with this sentence in the OP: “It’s because the syntax of these (programming) languages follow natural rules: they distinguish and prioritize a direct object over verbs and indirect objects.” I might be wrong here, but I thought the only universally accepted statement of a sentence is that it must contain a verb (i.e. a function).

Regarding Julian syntax, I think the current syntax and behavior has fostered a bit of a functional approach in Julia, which seems to be positive to me. When I think of I.love(Julia), I have the vague feeling that it seems to be telling I to start loving Julia, i.e. it is a mutating operation on I. (Not that I think Julia is or should be a purely functional language, of course, but just an observation)

7 Likes

Yeah, as a Subject–object–verb language native, I Julia Love (난 줄리아를 사랑해) feels more natural to me.

For a marketing point of view. I think question is not about what is “natural” but about how many users would feel uncomfortable because Julia don’t allow I.love("Julia") notation, and how much is it going to hamper their decision to choose Julia over Python.