OOP-like dot notation in Julia

This is a naive question, but given the widespread use of the object.method notation in OOP languages, I struggle to find a sound reason as of why it can’t (or shouldn’t) be supported in Julia? I assumes this has something to do with code parsing, but what?

Consider the following:

  • dot notation to access variable’s property is already well established (in a similar fashion as in C), using foo.bar
  • this is just a sugar syntax that does method(object, otherargs...) … but really helpful to chain calls, discover code, and think about the code (the latter is admittedly subjective)
  • Getting this notation to assign the left hand side of the dot to the first argument of the method seems quite arbitrary … but so is the do ... end block
  • there is not risk of clash with the classical broadcasting dot notation for regular functions, for in this usual case, the dot is between the function’s name and a left parens
  • There is no risk of clashing with the classical broadcasting dot notation for operators either, because colon is required when calling it from e.g. Base.:+, while broadcasting isn’t. In a similar fashion, using variable.function would just requires operators to be preceded by a colon, as in foo.:+(bar)
  • I don’t see any potential breaking change than would require a hypothetical Julia2.0

I feel I’m missing a simple yet not obvious to me case that would prevent this notation to be properly parsed and interpreted.
Or else, it is not supported because of other-than-parsing reasons? (and other than manpower)

… and if not … then wouldn’t it be great to have it available in Julia? :slight_smile:

1 Like

I agree that it would be nice to have this feature. I would like it because it makes method discovery easier in the simple case that dispatch happens on the first argument.

1 Like

a.b in Julia means getproperty(a, :b)

julia> Meta.@lower a.b
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope`
1 ─ %1 = Base.getproperty(a, :b)
└──      return %1
))))

Users can, and often do, overload getproperty for their types.

4 Likes

I don’t see why you would necessarily need dot notation for code discovery. Instead of typing object.<TAB><TAB> to get a list of methods, you could also (I imagine) add a keyboard shortcut (e.g. object<CTRL + M>) which then lists all applicable methods.

For chaining, the pipe operator |> exists (I’m not claiming this is flawless, but neither is dot notation in OOP).

But would that conflict with the suggested new functionality?

I don’t think so.

I think one issue with the object. approach is that say you had foo.bar to access a property and then you had a bar(foo) method. Those would then clash. (On second thought maybe this wouldn’t clash? You wouldn’t ever try to pass the function that way with foo.bar, so maybe julia would be able to distinguish between the two)

I think some sort of piping syntax is needed for better method discovery. The current syntax is fine, it just needs a little more power. The cool thing is, I think it could even be more powerful. Say you had an object foo that had bar and baz as some of its functions but bar had two different methods that foo was an explicit argument (like bar(foo, arg2) and bar(arg1, foo)). When you first do foo |> you would get the list of functions like bar and baz, but once you complete foo |> bar( you then get the list of available bar methods explicit to foo. I think this could be awesome.

4 Likes

This was already discussed at Allowing the object.method(args…) syntax as an alias for method(object, args …) - Internals & Design - Julia Programming Language (julialang.org) . And a link to demo at Allowing the object.method(args...) syntax as an alias for method(object, args ...) - #218 by xgdgsc

3 Likes

Oh, thanks. I’ll try to search a bit longer next time.

You can always add syntactic sugar yourself via a macro (though it is of course slightly less clean than if it were natively supported, as you need to use @).

using MacroTools: @capture, postwalk

macro dm(ex)  # dot method
    return postwalk(ex) do x
        if @capture(x, obj_.f_(args__)) && !in(f, fieldnames(typeof(eval(obj))))
            # (I'm not too fond of the eval here, but I don't see how else to get the list of fields of a symbol representing an instance.)
            return :($f($obj, $(args...)))
        else
            return x
        end
    end
end
julia> struct S{F <: Function}
           a::Int
           f::F  # To have s.t(x) both for when t is a field function, and when it is an external function
       end;

julia> plus1(s::S{F}) where F = S{F}(s.a + 1, s.f);

julia> plus(s::S{F}, t::S{F}) where F = S{F}(s.a + t.a, s.f);

julia> F = typeof(sin); s = S{F}(1, sin); t = S{F}(2, sin);

julia> @dm s.plus(t)  # becomes plus(s, t) --> .a == 3
S{typeof(sin)}(3, sin)

julia> @dm s.plus1()  # becomes plus1(s) --> .a == 2
S{typeof(sin)}(2, sin)

julia> @dm s.f(pi)  # remains s.f(pi) (== sin(pi) == 0), requires the fieldnames part in the macro to not change this into f(s, pi)
0.0

julia> @dm s.f.([0, pi])  # (dot broadcasting still works)
2-element Vector{Float64}:
 0.0
 1.2246467991473532e-16

julia> f(x) = 4; @dm s.f(pi)  # again sin(pi), not f(pi) == 4
0.0

If this helps you to think about code, you could use such a macro (if necessary tweaked to be more robust). This might also help you with discovering any possible shortcomings, if they exist.

I agree, using macro is always possible as a last resort solution. But this is slightly beside the point, which is to have a native support, and wouldn’t be compatible with tab completion. Thanks for the proposal, though.

Hmm, I’d be surprise if it were possible in Python to declare a property and a method of a class with the same name. Clash would be prevented.

Unfortunately, each time I give a chance to this approach, I end up creating anonymous functions at each piping step, which is very teddious. This is usually to do currying using the return value of the previous step. To me, this is a very common pattern for which I have hard times trying to find reasonable alternatives.

I agree (and mentioned it in the OP), but don’t see how this would prevent this other meaning of dot. It’s possible to propose a list of match, and to specify whether this is a property or a method (for tab completion).
The only issue I see is when doing c = a.b, where the intend is to store a function b into c. Quite convoluted, and could be forbidden by design, to be favored to the property extraction accompanied with a warning. As for soft scopes when dealing with ambiguity.

Following this thread of links, I ended up at: oop - How to create a "single dispatch, object-oriented Class" in julia that behaves like a standard Java Class with public / private fields and methods - Stack Overflow
This is almost what I was thinking about, except that it’s a very OOP-specific solution.

1 Like

This is pretty great. Have you been using this for some time now? How would I go about using this?

I guess there is somewhere that the Julia vscose plugin lives on my computer and I would just replace it with your version?

There are plenty of packages providing macros to solve this exact inconvenience, e.g. Chain.jl, Pipe.jl, Underscores.jl.

julia> using Underscores

julia> @_ rand(10) |> filter(0.4 < _ < 0.6, __)  # equivalent to rand(10) |> x -> filter(t -> 0.4 < t < 0.6, x)
2-element Vector{Float64}:
 0.4728989701850077
 0.4825616604480252
3 Likes

Yeah you wouldn’t be able to. That’s because the method lives inside the class. I think it would be perfectly fine in python to do if the function lived outside the class but you wouldn’t get the . capabilities. All functions/methods live outside the class in Julia so it would be harder to check maybe was my thought, but yeah I think it could be okay anyway since you wouldn’t ever need to pass that function that way as an object.

Anyway, I am perfectly fine with . over |> if that works. I’m glad you made this thread!

Oh, right. I eventually addressed it in my edited response above (tab discovery vs compile check).

Of course it would, how would you know whether a.b() means getproperty(a, :b)() or b(a)?

1 Like

Well, if there is no such property it could look for a function call with this signature and vice versa. If there is both it could throw an error.

No that does not work, you cannot generally know whether the property exist before you have actually executed the getproperty function. It would also be a hugely breaking change to start introducing an error for a propert access that was previously working.

5 Likes

For function discovery, a better autocomplete would indeed be useful!
Does it have to be object.<tab>? Maybe, but also consider alternatives that mesh better with the existing Julia syntax – eg, ?(object<tab> or similar.
Anyway, autocomplete is not currently limited by the lack of syntax, and could definitely be improved.

Many Julia functions that one commonly wants to pipe take either a function or some model object as the first argument, while the natural “object” goes as the second/last argument.
DataPipes.jl (discussion) is specifically designed for zero-boilerplate piping in this very common scenario, while still supporting functions that take the “object” as the first argument.
Not sure if using . for piping is much more convenient to warrant such major changes…

1 Like

Indeed, this may be seen as two distinct issues. My feeling is that is common and convient enough in other languages that there’s no need to reinvent the wheel, and would help “welcoming” newcomers. But we should not refrain ourself to think outside the proverbial box.

Thanks for the link, didn’t know this package.

Well, I don’t see how this would be an issue, given that this can be checked at compile time. getproperty is how the REPL implements it. But the compiler can do otherwise.
Also, in vanilla Julia, you can already try to do a.b, and get an error if it b doesn’t exist (with the compiler).

Same answer here, I’m not sure whether this would be hugely breaking … if at all: consider to give a higher precedence to getproperty over a possible “syntax replacement” to call b(a). And raise a warning if there’s an ambiguity, stating that getproperty is done by default (this is not new: see soft scope).

The only issue I see so far is when b is a type with a defined functor, such as:

struct SomeInnerType
   val::Int
end 
bar = SomeInnerType(1)

struct SomeOuterType
   b:: SomeInnerType
end
a = SomeOuterType(bar)

b(x) = x + 2 # defines a function with potential name conflict

@assert a.b(3) == 5 # OK, no ambiguity

c = a.b
@assert typeof(c) == SomeInnerType # + ambiguity warning, could be function b, but still works

(b::SomeInnerType)(x) = x + b.val

@assert a.b(3) == 4 # + ambiguity warning

This still works, is not breaking. Admittedly, this would be hard to debug in case of a heavy reliance on dynamic creation of functions and/or macro. But there is a trade-off to be made here, regarding potential gains/pains. As discussions on the transpose operation have shown, full consistency and perfection is not always possible. Sometime, it is worth being a bit pragmatic.

Maybe another simple solution would be to simply call a..b() for methods instead of the classical a.b() for properties? (open to other symbols, of course)

It’s generally not possible to determine available properties without executing propertynames(obj) or getproperty(obj, propname). Remember that these functions may contain arbitrary Julia code.

For example, consider types like GitHub - JuliaCollections/PropertyDicts.jl – the list of properties depends on the value, and cannot be determined from the type alone.

1 Like