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

I started exploring Julia very recently and I like it. I’ve been chatting about it with colleagues.

Many people coming from OOP languages (Python…) like the syntax object.method(a,b,c…).
Since you can get a preview of the available methods acting on object with tab press.
Would it be possible to have that in Julia (I’m assuming it’s not already there) to improve usability?

Let say I define a IIR filter struct

mutable struct IIRfilter
	state;
	alpha;
	IIRfilter(state,alpha) = 0<=alpha<=1 ?  new(state,alpha) : error("alpha must between 0 and 1")
	IIRfilter(alpha) = 0<=alpha<=1 ?  new(NaN,alpha) : error("alpha must be between 0 and 1")
end

and a method

function filter(filter::IIRfilter, x)
	if isnan(filter.state)
		filter.state = x
	else
		filter.state = filter.alpha*filter.state + (1-filter.alpha)*x;
	end
	return filter.state 
end

I can now use my filter

begin
	N = 3000
	f = IIRfilter(0.9)
	Random.seed!(1)
	xv = randn(N,1);
	xf = zeros(N,1);
end;
begin
	for i = collect(1:N)
		xf[i] = filter(f,xv[i])
	end
end

It would be nice to be able to start typing

f.

the press a tab and get

f.state
f.alpha
f.filter(...)

as options instead of just

f.state
f.alpha

basically get a preview of all available methods for the IIRfilter type (where IIRfilter is the first argument of the method if we are thinking about standard OOP, or maybe a more general solution that is closer in spirit with multiple dispatch?)

Is there a reason why we might not want this or we would actually want to actively discourage it?

4 Likes

This is very unlikely to ever be supported in julia anytime soon. What’s more likely, is for text editors to come up with better ways to help you do autocompletion.

18 Likes

It’s definitely possible to do this already, see Jeff’s old answer here:

Whether this is a good idea is another question since multiple dispatch is strictly more powerful than single dispatch in the form of class-based OOP and designing interfaces like that is generally considered quite unidiomatic in Julia. There are other proposals to solve the tab-completion problem you mentioned, see for example ?(x, y)TAB completes methods accepting x, y by timholy · Pull Request #38791 · JuliaLang/julia · GitHub.

10 Likes

Another reason this is odd is because in Julia, the first argument is already “special” because you can construct anonymous functions using the do syntax that get placed into the first argument, i.e. filter(t -> t > 1, [1, 2, 3]). Adding this kind of syntax would make the first argument “special” in a different, somewhat contradictory way.

4 Likes

It would be great if this could make it into Julia 1.7.

1 Like

This is

d |> x->f(x,y,z) |> x-> g(x,m,n)

is easier to write as

d.f(y,z).g(m,n)

The do block

d.f(y,z) do r
    r + 5
end

goes to

f(d, r->r+5, y, z)
2 Likes

@Mason It’s not just auto-completion in a generic abstract way. And we are talking about the REPL and Jupyter and Pluto. It’s about gaining traction and lowering the entry barrier. I’m talking about a true scenario where a colleague of mine started a new project in Python even though he is an estimator of Julia because not many of his collaborator know Julia and he was too worried about their learning curve. The goal it to make people comfortable making the transition because they know they can easily get Python/C++ users on board. If you do that you have very little to lose and a lot to gain I believe.

@jzr thank you for posting the wikipedia article. It was very interesting seeing that there is already a “name” for the topic and seeing the proposals attached in the references of that article.

@simeonschaub , “fancy” solutions are interesting and a few people would get excited by the learning opportunity but in the context I’m referring to it will not help, it might actually have the opposite effect.

Imho this is too big a change if it only lowered the barrier to entry, but it may actually be better for experienced Julians.

I think “estimatore” goes to “admirer”

2 Likes

@jrz, you are right, “admirer”, writing too fast and in an uncomfortable situation, and probably using too many “estimators” :smiley:

Yes barrier to entry is just one aspect of it, I believe it would have an intrinsic value for the current community. But I would not underestimate the “power” that a low barrier to entry can give to “admirers” within organizations.

In the SO post, the person object has a fixed set of methods on it that work in the postfix position. In UFCS, all methodswith(Person) can go in the postfix position, so it’s compatible with Julian dispatch, not less powerful. x.add(y) is just add(x,y).

1 Like

Before continuing @gianmariomanca, I should welcome you to the Discourse community, I’m sorry for forgetting earlier! It’s always nice to see new faces come by.

I get why people want this. You’re not the first person to ask for this, and I’m sure you won’t be the last. This has really been discussed to death in a lot of different places, usually asked for by newcomers, so please understand that if it feels like people aren’t giving your suggestion the proper amount of consideration, it’s because it has been discussed a lot before and the language developers are pretty firmly against it.

I’m not a core developer or someone who has much of a say in the matter at all, but for my part, I think that adding new syntactic forms just to make people coming from class based languages like Python and C++ is a bad idea. That extra bit of familiarity is not going to be of much help, and may actually hurt them because a very common pattern is for people to not realize just got different julia is. It’s it’s own language that has it’s own paradigms, styles, quirks, strengths and weaknesses.

Many people try and write ‘python in julia’ and get confused and upset when it doesn’t do what they expect or doesn’t perform as well as they want.

Uniform function call syntax (the x.method(y) syntax) has some nice advantages, but I think once you get used to julia it won’t be missed too often, and ther are real advantages to not using it as well. It’s particularly important to emphasize how Julia is not a single dispatch language, and the first argument to a function is not special.

One point I’ll bring up is that Bjarne Stroustrup, the creator of C++ recently wrote a paper where he called this syntax a mistake, and advocates for a setup in C++ that’s much more like julia.

Unified function call: The notational distinction between x.f(y) and f(x,y) comes from the flawed OO notion that there always is a single most important object for an operation. I made a mistake adopting that. It was a shallow understanding at the time (but extremely fashionable). Even then, I pointed to sqrt(2) and x+y as examples of problems caused by that view. With generic programming, the x.f(y) vs. f(x,y) distinction becomes a library design and usage issue (an inflexibility). With concepts, such problems get formalized. Again, the issues and solutions go back decades. Allowing virtual arguments for f(x,y,z) gives us multimethods.

Here’s the paper if you want to read more: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1962r0.pdf

29 Likes

Chain.jl makes this very easy,

@chain d begin 
    f(x, y, z)
    g(x, m, n)
end

Creating syntactic sugar for this kind of pattern is easy. The auto-completion is the hard part, and it seems like there are some serious efforts underway to make it easier.

3 Likes
julia> length("d.f(y,z).g(m,n)")
15

julia> length("@chain d begin 
           f(x, y, z)
           g(x, m, n)
       end")
49

Though most of that cost is fixed.

@chain inserts the last argument into the first of the next call. So you are counting extra arguments. Here is a better comparison

julia> length("d.f(y,z).g(m,n)")
15

julia> length("@chain d f(y,z) g(m,n)")
22
2 Likes

@Mason , thank you for welcoming me. I think it was a mistake on my part to bring up “barrier of entry”, it seems to monopolize the attention and bring the discussion in the unproductive direction of “this language vs that language”. I did it just because that episode with my colleague attracted my attention and seemed like a missed opportunity.

In general my attitude in this comment was just “steal” as many good features as possible (which seems very much in line with the spirit behind Julia’s creation when the co-creators wanted it “all”.)

I think you might be misreading Bjarne Stroustrup comment. Yes I agree that is a bad idea to always assume that there is one “most-important” argument, but here we are talking about having more options, not less. Nobody wants to remove the standard notation, just adding a new equivalent one with some known advantages, in some of the cases. It’s up to people then to use the best one in each case.

If you ask me doesn’t even have to be only the first argument, if better or more flexible you can make it that you can use the object.method notation when the object is in any position.
Let’s say you have object f of type IIRfilter and 2 methods m1(IIRfilter,x) and m2(y,IIRfilter) then you can say f.m1(x) and it will call m1(IIRfilter,x) and f.m2(y) and it will call m2(y,IIRfilter). Maybe allow this notation only for methods with no ambiguity, i.e. only one entry of that specific type, since this notation is targeting the “one most important object” case. Just added this comment for the sake of conversation.

In general would the way I wrote the IIRfilter example considered good “Julian”? How would you rewrite it? Wouldn’t x_f = f.filter(x) look nicer in this case?

1 Like

I’m not so sure how you want to use this thing, but one useful pattern to know is that you can make the struct callable, so that x_f = f.filter(x) just becomes f(x):

julia> (filter::IIRfilter)(x) =
           if isnan(filter.state)
               filter.state = x
           else
               filter.state = filter.alpha*filter.state + (1-filter.alpha)*x;
           end;

julia> f = IIRfilter(0.9);

julia> f.state
NaN

julia> f(0.5)
0.5

julia> f.(1:3)
3-element Vector{Float64}:
 0.55
 0.6950000000000001
 0.9255

julia> f.state
0.9255
4 Likes

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