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

Just to be fair, I don’t actually think it’s totally impossible to make the syntax object.method(...) both consistent and clear within Julia within a package, e.g. creating a Class abstract type or trait and an interface that allows one to specify which properties are to be treated as methods. That could be either specified within a macro that creates the class-type (so the macro expansion would contain a definition of getproperty) or explicitly by a function add_method_property.

I have no objections to anyone creating a package like that. It would not be a trivial task, though, and only newly created types could also be classes (since you can’t redefine getproperty on existing types without breaking things).

However, AFAIK, people who tried to do that gave up, and if it’s already hard to get that right in a separate package, it’s even more complicated to do it in Julia.

3 Likes

I can’t spend much time on this unfortunately, but 2 quick comments in case they are helpful: since we already have an example where the compiler seem to be doing just fine, see example above, is there any issue in simply having that generated automatically with something like

mutable struct IIRfilter
	private state;
	private alpha;
    filter::Function(f::IIRfilter, ...);
	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

or something equivalent, that gives enough information, so the function is added automatically to any instance created with that struct, and avoid recompilation issues (which someone mentioned but I’m not familiar with)? You can just declare it there and define it outside, so that we know it’s an internal method that is always allowed to access private properties.

Or if you are worried about composability maybe define an “internal” or “friend” keyword that you put in front of the type when you define a function, to declare that the function has to be granted access to private properties, and it will make it also obvious to a code reviewer.
Then you can add it in front of any argument/type not just one argument, or the first argument, etc

mutable struct IIRfilter
	private state;
	private 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

see internal added in front of filter::IIRfilter below

function filter(internal 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

This seems also backward compatible.

These two “proposals” may also give helpful information to an autocomplete tool, trying to use the object.method notation.

I can’t claim that this is anywhere close a detailed proposal, it’s still just a question.

1 Like

About the first “proposal”, my objection to it would be: you don’t need to put filter::Function inside the object, you can simply overload getproperty to return the filter function when the key is :filter. It is cleaner, simpler and don’t allocate a new function for each instance of IIRfilter. That doesn’t need any change in Julia.

About the second, you are suggesting a syntax to distinguish private versus public properties, which is a different and independent matter. My objection: enforcing that only “internal” methods have access private fields makes sense in static languages (C++, Java), but I don’t know any dynamic language with such restriction.

I wonder if there are situations where disallowing external access to private properties could allow compilers to optimize more. (But yeah it’s a separate issue from the main postfix proposal.)

Well yes, the discussion went well beyond the title of the thread in previous comments. Not sure what the policy is in that case (change the title by adding “etc, etc” :slight_smile: ?) , but it extended to a general discussion of syntax + auto-completion / discoverability + encapsulation in modules + encapsulation in structs for multiple instantiation etc etc, since they are connected in various ways.

Regarding the other 2 points

this as mentioned above is addressing only the syntax in isolation from all other aspects touched in the thread, at least without further changes. I understand that the policy might be one topic per thread but the discussion already evolved past that (at least on my side).

Matlab?

Just for reference


but as we said it doesn’t address all aspects of the discussion.

Ruby has protective and private methods. You can check the Ruby tutorial here.
It does make sense, with the price of more runtime checking (it must validate current context to ensure that the method is called from inside objects). But this is unacceptable to a high performance language, though I think type inference can remove most of this kind of checking.

That makes sense, though in that case methods are private to a module / namespace (instead of some fields of a struct being private to internal methods). I wish there some way to tell which properties are private or public (whether in a module or struct), though IMO it makes more sense to allow a different syntax (e.g. object$internal_field or MyModule..private_method) than to prohibit access to internal/private properties.

Whatever the case, IMO the distinction of private vs public property should be relative to the object that contains such properties, not relative to (some) methods.

1 Like

What do you think about this kind of syntax (similar to golang)?

function (obj::MyType) myMemberFunction(x::Int)
   return obj.mymember*x
end
myObject::MyType=getObjFromSomewhere()
y=myObject.myMemberFunction(6) # y==myObject.myMenber*6

It would be quite similar to callables and pretty explicit…

4 Likes

@Mason,

It’s particularly important to emphasize how Julia is not a single dispatch language, and the first argument to a function is not special.

The do statement would say otherwise, but apparently we limit first-argument “specialness” to anonymous functions only. :stuck_out_tongue_winking_eye::thinking::pensive:

@gianmariomanca,

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.

I sympathize with this.

There’s a fair bit of anti-OOP pearl clutching in this thread, seemingly fearing that any privileged treatment of data will unleash the OO demons. I’m not suggesting to make methods “belong” to objects or to adopt JavaScript’s prototype inheritance model! But providing syntax sugar to allow UFCS does not an OO language make. The fear that angry hoards of OOP brutes will descend from the hills to defile Julia’s functional innocence is overblown imo.

Fun note. While the first argument is not *always* the most “important” (whatever that means), it frequently is. That’s why we choose to put it first in the argument list! :sweat_smile:

@jzr,

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) .

I love it. Notably, this is still multiple dispatch, just with syntax sugar. Methods do not need to “belong” to objects as in OO languages.

I don’t think there’s much danger of users thinking that a function has an owner since that concept doesn’t exist in Julia.

Bingo. Spot on.

To disambiguate from property getting, something like .. could be used to denote invoking a method. For example, my_object.property versus my_object..mymethod().

Why have syntax sugar for UFCS? Because sometimes thoughts flow as verb→noun (e.g. read(book)), but sometimes they flow as noun→verb (e.g. book..read()), depending on the context. The purpose of a language of course is to allow thoughts to flow as naturally as possible out of the meatbrain and into the computer, from left to right, so whichever order makes most sense in the context the language should try to support.

It’s frequently the case that we will transform an object several times before continuing on; UFCS simply acknowledges this. Example was given, dish = egg..crack()..scramble(bowl)..cook(:high), rather than dish = cook(scramble(crack(egg), bowl), :high). In the case of chaining, the noun that is being worked on as it undergoes transformations is front of mind; being forced instead to think first about the verbs (in reverse order of course) is frequently unnatural.

Julia already provides the pipe operator |> for exactly this reason—although unfortunately it’s very awkward to type and it’s underpowered, being unable to pass more than one argument. In other words, the authors of Julia already acknowledge the benefits of noun→verb ordering and operation chaining, but nobody likes the implementation and people keep complaining because UFCS already provides a better solution in other languages.

Benefits to UFCS:

  • the ability to think of the noun first and the verb next,
  • the ability to chain operations on successive transformations of an object (similar to function composition or piping but with more arguments), and
  • autocomplete on what methods are available on that object while typing the method name, ideally showing the most-type-specialized functions at the top of the list. This seems significantly easier and more convenient than the ?(x, …) notation being worked on.

Julia’s beloved features of composability, multiple dispatch, functional style, etc. are left unchanged. UFCS might encourage people to place the most-important argument (if one exists) first, but that doesn’t seem like a bad practice anyway :man_shrugging:.

Examples:

my_object.mymethod # ERROR: type MyStruct has no field mymethod

my_object..mymethod(1, 2, 3) == mymethod(my_object, 1, 2, 3) # true

[1, 2, 3]..push!(4).^2 == [1, 4, 9, 16] # true

"Hello, world!"..split(",")..replace.("o"=>"e")..join(":") # "Helle: werld!" (chaining & broadcasting)

document = parsexml(xml_string) # using EzXML 
document..root()..firstelement()..setnodecontent!("Hello, world!") # Navigating tree

Thoughts welcome :thought_balloon:

2 Likes

That’s indeed useful, and pipes already give the ability.

That often requires putting the “main” argument as the last function argument, not the first. Think of common data manipulation functions map/filter/mapreduce/etc: they wouldn’t be convenient with your proposal.
Meanwhile, one can use a helper piping package, for example with DataPipes:

@p X |> filter(isodd) |> map(log) |> findmax(abs)
@p "Hello, world!" |> split(__, ",") |> replace.(__, "o"=>"e") |> join(__, ":")

Autocomplete relates to tooling, not the language syntax itself. It doesn’t seem fundamentally more difficult to autocomplete x |> (tab) than x..(tab), the assumption of a specific “main” argument position is there either way.

1 Like

See (I believe better than “Jeff’s old answer”):

but also from its author: Translating OOP into Idiomatic Julia · ObjectOriented.jl

This article aims at telling people how to translate serious OOP code into idiomatic Julia. […] For most of tasks involving OOP, the translation is straightforward and even more concise than the original code.

Using that package and OOP isn’t an alias, but:

The PR you linked is now merged. I need to look into it, and see it in action in the REPL or VS Code.

Before we jump to new syntax, just autocompleting pipes accurately would be a huge improvement:

obj |> # hit tab

Currently you get a bunch of functions that don’t actually have methods for typeof(obs). This should be an easy, actionable fix?

14 Likes

I’ve been thinking about this, so fleshing it out a bit more into something that can be a Base feature request, or a new package if someone wants to write it:

The first, seemingly easy solution (above) is for basic pipes:

obj |>  # hit tab to make `obj |> func` 

Where func accepts single argument obj. Probably not much controversy here.

Next, a bit less straight forward, is tab completing pipes into anonymous functions:

obj |> x -> # hit tab to make `obj |> x -> func` 

Where func accepts obj in any position

This could even insert the x in the right spot, and let you tab through filling in the other arguments.

And last, from tuples/named tuple syntax we can hit tab after writing the arguments to complete the function:

(a, b, c)#hit tab to make `func(a, b, c)`
(a, b; kw)#hit tab to make `func(a, b; kw)`
e(a, b, c)#hit tab to make `e...(a, b, c)` (a func starting with e)
(a,#hit tab to make `func(a, ` and show hits for the remaining args 

This happening backwards feels kind of unusual, but it can match the types of objects in args and the names and types in kw, which would cut the list down a lot. You could even put the first letter before the bracket where the funciton name normally goes to get the search started.

Once you add a space after the bracket normal tab completion would resume.

Thoughts?

3 Likes

let’s do (2) for both (1) and (2) as a starter, namely,

obj |> #tab

would show functions that take obj at any position, cuz I can imagine user not remember which position or there’s a default argument they usually don’t care, this way at least users can see more functions and decide if they need to make an anonymous function instead

2 Likes

Makes sense. It could even fill out the anonymous function for you automatically if you choose something with multiple arguments.

2 Likes

|> is actually quite awkward to type. Maybe some shortcut is possible?

1 Like

|> is part of the language. But I guess you’re echoing OP’s proposal for just having obj.f() to mean f(obj)? (like Rust)

Since this thread is revived, I’ll link to CBOOCall.jl

Copying from the README:

module Amod

using CBOOCall: @cbooify

struct A
  x::Int
end

@cbooify A (f, g)

f(a::A, x, y) = a.x + x + y
g(a::A) = a.x

end # module Amod

Then both Amod.f(a, 1, 2) and a.f(1, 2) work. I wrote this mostly to avoid importing many, very short, method names like x (we had to use x, can’t be another name).

There is no runtime cost to using this notation.

EDIT: I can’t demand, but I can respectfully request; please don’t reply saying that this is a stupid idea. There are many, many threads (for example this very thread) where this opinion is expressed in great detail. So adding it as a reply here isn’t useful, but rather adds noise. EDIT: I mean a stupid idea in general. If you have a problem with the approach or implementation, that’s fair game.

1 Like