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

As much as the syntax would be really nice, “obj.somename” already means getproperty(obj, somename)

What happens if there is a property and a function with the same name?

Methods are not in the objects namespace (unless you use the JavaClass hack linked above), so this would happen all the time, with confusing results.

11 Likes

@mcabbott , thank you. Very interesting pattern when there is only one method. Happy to have learned something new. I’m already impressed by how responsive the community is.

1 Like

The power of multiple dispatch allows for any number of methods to be added this way:

julia> mutable struct IIRFilter 
         state::Float64         
         alpha::Float64         
       end                      

julia> (filter::IIRFilter)(state, alpha) = begin                          
          filter.state = state                                            
          filter.alpha = alpha                                            
       end                                                                
                                                                          
julia> (filter::IIRFilter)(x::Float64) = if isnan(filter.state)           
           filter.state = x                                               
       else                                                               
           filter.state = filter.alpha*filter.state + (1-filter.alpha)*x  
       end                                                                
                                                                          
julia> (filter::IIRFilter)(x::Int64) = begin                              
           filter.state = filter.alpha*filter.state + (1-filter.alpha)*x  
       end               
                                                              
julia> f = IIRFilter(0.5, 0.7)                                            
IIRFilter(0.5, 0.7)                                                                                            
                                                                          
julia> f(1)                                                               
0.65                                                                      
                                                                          
julia> f                                                                  
IIRFilter(0.65, 0.7)                                                      
                                                                          
julia> f(1)                                                               
0.755                                                                     
                                                                          
julia> f                                                                  
IIRFilter(0.755, 0.7)                                                     
                                                                          
julia> f(0.01)                                                            
0.5315                                                                    
                                                                          
julia> f                                                                  
IIRFilter(0.5315, 0.7)                                                    
                                                                          
julia> f(NaN, 0.3)                                                        
0.3                                                                       
                                                                          
julia> f                                                                  
IIRFilter(NaN, 0.3)                                                       

The problem of course is that you lose some sense of what each combination of arguments actually does. Moreover, you can’t distinguish between two methods with the same number of arguments and the same types of arguments but different behaviour this way.

In julia, there’s a convention that functions that modify one of their arguments are ended with an exclamation mark !. So I’d write the above like this:

mutable struct IIRFilter
    state::Float64
    alpha::Float64
end

# with the (filter::IIRFilter)(...) syntax, this can't be distinguished from the modifying `updateFilter!`
function queryFilter(filter::IIRFilter, query)
    filter.alpha*filter.state + (1-filter.alpha, query)
end

function updateFilter!(filter::IIRFilter, nstate)
    filter.state = if isnan(filter.state)
        nstate
    else
        filter.alpha*filter.state + (1-filter.alpha)*x
    end
end

function updateFilter!(filter::IIRFilter, nstate, nalpha)
    filter.state = nstate
    filter.alpha = nalpha
end

So from a usability point of view, I personally prefer to be explicit about modification, and I can’t do that with (f::IIRFilter)(...). You have to either guess, or look it up for a specific combination of arguments.

2 Likes

@Raf can you write an explicit example of the ambiguity?

Wouldn’t you get
f.p for the property and f.p() for the method? or something along those lines? Is it something related to the property itself being a function, or the issue is even more basic in Julia?

This didn’t seem to worry Bjarne Stroustrup too much for the same proposal for C++
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4174.pdf
“The basic suggestion is to define x.f(y) and f(x,y) to be equivalent”

(this is one of the references from Uniform Function Call Syntax - Wikipedia suggested by @jzr )

1 Like

(Taking the example from above:)

mutable struct IIRFilter
    state::Float64
    alpha::Float64
end

julia> f = IIRFilter(0.5, 0.7)
IIRFilter(0.5, 0.7)           

# This could of course be a much more complicated function, making use of `f.state`
julia> state(f::IIRFilter) = f.state                         
state (generic function with 1 method)                       
                                                             
julia> f.state()                                             
ERROR: MethodError: objects of type Float64 are not callable 
Maybe you forgot to use an operator such as *, ^, %, / etc. ?
Stacktrace:                                                  
 [1] top-level scope                                         
   @ REPL[20]:1                                              

The problem arises when f.state is callable - which call do you choose?

julia> struct Metafilter                                  
         filter::IIRFilter                                
       end                                                
                                                          
julia> filter(m::Metafilter) = m.filter                   
filter (generic function with 1 method)                   
                                                          
julia> (filter::IIRFilter)() = filter.state * filter.alpha
                                                          
julia> m = Metafilter(f)                                  
Metafilter(IIRFilter(0.5, 0.7))                           

# would you have expected this here? It's basically the same as (m.filter)(), which is (::IIRFilter)()
julia> m.filter()                                         
0.35                                                      
                                                          
julia> filter(m)                                          
IIRFilter(0.5, 0.7)                                       
4 Likes

This is what I was referring to when I mentioned “only one method”. I should have been more specific.

In Julia any object can be called like that.

julia> struct Callable end

julia> (::Callable)(x) = 2x

julia> obj = (a=Callable(), b=Callable())
(a = Callable(), b = Callable())

julia> obj.b(5)
10

So there is so syntactical distinction between fields and functions… anything can be called as a function.

6 Likes

One recent thread Is it reasonable to mimic a Python class with mutable structs?

This is the first I’ve heard of such efforts, do you have more information to share about them?

Good completion is probably the holy grail of editor tooling support in Julia land (alongside an integrated hoogle/@which/methodswith search). One avenue that might be interesting to explore is Intellij-style postfix completion, which would allow one to type something like x.[ctrl+space]foo but actually write out foo(x). I noticed that there are extensions that do this for VS Code, but I’m not sure if it’s possible to hook them into the language server or not.

1 Like

Probably this is implicit (or explicit) in the answers above, and I’ve missed it, but consider this example:

julia> struct A
         x::Vector{Int}
       end

julia> f(x) = x[1]
f (generic function with 1 method)

function f clearly does not work for type A, thus I suspect one would not want that given a=A([1,2,3]), typing a.f(... returned anything, or that f was listed in any list of methods that can be applied to type A.

Yet, if we do:

julia> Base.getindex(a::A,i) = a.x[i]

suddenly f is a function that can perfectly work for type a:

julia> f(a)
1

Thus, my impression is that while some subset of the functionality you propose can be obtained (methods that explicitly annotate type A for example), in general that will not be very useful, in particular if developers follow the more or less sensible guidelines that functions should be type-annotated as flexible as possible (see Over-constraining argument types).

2 Likes

No problem, sorry if I ranted at you. I definitely agree we should be stealing good features! However, I think what I’ve been trying to argue and what others have been arguing is that the costs are greater than the benefits.

One other cost that hasn’t really been brought up yet is that julia already has a lot of special syntactic forms that are hard for beginners to learn. We need to be conservative about adding more. I think it’s important to try and come up with powerful syntax extensions that can be generalized to many situations, rather than adding more single purpose syntax.

Fair point. I guess though that as others have already pointed out, there’s a lot of potential for conflicts here between that names of properties and the names of methods laying around in the namespace that makes me very wary of this.

To my eye, I guess I’m just so used to julia that I don’t see what’s so nice about

x_f = f.filter(x)

and why I wouldn’t want to just write

x_f = filter(f,x)

which I think is visually much cleaner.

One other thing I should mention is that there are various proposals for a new anonymous function syntax so that writing filter(_, x) is equivalent to f -> filter(f, x). That way, you could write

x_f = f |> filter(_, x)

if desired. I’m not sure this is a great win, but it’s being considered.

2 Likes

Just because I’m bored. Don’t do this :slight_smile:

macro bless(T, fs...)
    fswitch = map(fs) do f
        :(s === nameof($f) && return (args...;kwargs...) -> $f(t, args...; kwargs...))
    end

    quote
        @eval Main begin
            function Base.getproperty(t::$T, s::Symbol)
                $(fswitch...)
                return getfield(t, s)
            end

            Base.propertynames(t::$T, private::Bool=false) = private ? ($fs..., fieldnames($T)...) : $fs         
        end
    end
end

julia> struct AA
       x::Int
       end

julia> f1(a::AA, x) = a.x + x
f1 (generic function with 1 method)

julia> f2(a::AA, x) = a.x * x
f2 (generic function with 1 method)

julia> f3(a::AA, x, y;z) = z*(a.x - x^y)
f3 (generic function with 1 method)

julia> @bless AA f1 f2 f3

julia> aa = AA(2)
AA(2)

julia> aa.f # type aa. and press tab
f1 f2 f3
julia> aa.f1(6)
8

julia> aa.f2(6)
12

julia> aa.f3(2,3;z=4)
-24

Yes, the name of the macro is a reference to a language which tucked on OO though it probably shouldn’t.

Anyways, I agree that one drawback with functions first is discoverability.

9 Likes

We should really add this it the FAQ
https://docs.julialang.org/en/v1/manual/faq

It would be good if someone could summarised this thread including the downsides and upsides, and make a PR to the docs.

11 Likes

Thank you. Yes this was also apparent from @mcabbott example.
Still C++ has the same issue but that didn’t seem to be a sufficient reason to immediately reject the idea.

( PS One think I appreciate about Bjarne Stroustrup from the few talks I saw and the linked documents is that he is always very pragmatic and he seems to try to always look for the greater good. I’m not claiming I know what the greater good is, I just felt like complimenting Stroustrup :smiley: )

This seems to apply to any auto-completion effort including the one linked earlier in this thread (?(x, y)TAB completes methods accepting x, y by timholy · Pull Request #38791 · JuliaLang/julia · GitHub). But I don’t think its a valid reason to give up on discoverability for the cases where it’s possible to have it. Unless I misunderstood your comment. In fact I would add the discoverability should be “smart” and whenever possible not too verbose.

Nim describes its disambiguation procedure.

https://nim-lang.org/docs/manual.html#templates-limitations-of-the-method-call-syntax

Sometimes people travel halfway around the world to visit an exotic country, then spend the first afternoon asking the locals where they can find a decent hamburger restaurant. It’s understandable because it’s a safe choice (who knows what those crazy foreigners are cooking) but it’s also understandable that the locals are a bit saddened and try to convince the visitor to sample the local cuisine.

So please, have a taste, you may find it delicious. We have a vibrant culture with many master chefs who’ve developed some unique dishes over the years. Some visitors are so taken that they end up moving here permanently. And even if you’re not one of them, you may find when you get home that your tastes have broadened and you can cook burgers in exciting new ways.

19 Likes

See this PR, linked to earlier in the thread.

2 Likes

What I meant is that in principle all functions defined everywhere which do not have type annotations excluding the new type could be applied to a new type. This is what it makes possible I being able to define a new type of Matrix and use everything that is defined in base or other packages that define methods to work with matrices.

Some of them will not make sense and error, of course, but that possibility is one of the strength of multiple dispatch and general programming.

I am inclined to think that a package + macro that provided a list of methods the author of the type would like to be listed is a reasonable choice to improve the experience of the user in the sense OO programers are used to.

2 Likes

@NiclasMattsson , I know it can be fun trying to come up “catchy” analogies, but I won’t take it personally since I’m the farthest thing you can find from the guy the eats hamburger in an exotic country :wink:

To play your game, now that I think about it: there is a recurring discussion in Italy of how come Italy is not the first touristic destination in the world, and gets “beaten” by Spain, France, you name it… when on paper it could “win” against anybody by a huge margin (history, food, weather, music, friendly locals…). The reason is that they rely too much on their “intrinsic” value and ignore completely all organizational aspects that make a vacation pleasant and not stressful. Sad, but true.

(PS just playing a game here, this comment doesn’t really matter for the overall informative discussion)

5 Likes