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

Actually its IntervalSets.jl (I always get this mixed up too)

And also more popular:
https://juliahub.com/ui/Packages/IntervalSets/2FHuv/0.7.3?page=2

@uniment that’s 5% of the ecosystem. Hopefully you can see the difference between pragmatic solutions that are not perfect but can be implemented and merged, and nice ideas that can’t happen in reality.

Also, mandatory:

4 Likes

What CBOO means.

CBOOCall.jl has particular characteristics. For example you have to sort of “register” any method you want to include via @cbooify. If you are the author of the type then this seems pretty clear; I mean it’s a good idea to make clear which are “instance methods” anyway. If you are not the author, you can still @cbooify methods. But this seems less robust, especially if CBOOCall were used widely (it’s not.)

An example of why this might be convenient:

circ = QuantumCircuit(3)
circ.x(0) # apply an X gate at position 0

Adding a gate this way, and the name, is very standard in quantum computing (mostly in Python). In Julia world, everyone says, it’s not a problem it’s just a preference, you’ll get used to it, etc… So you do

using QuantumCircuits: x
x(circ, 0)   # or circ(x, 0) or whatever

But, importing a lot of functions with identifiers like x could cause headaches. So you could name the function xgate or something instead. Or qualify it: QuantumCircuits.x. But, these are less convenient. And these gates are used a lot. Obviously, similar examples will arise in other domains. If you give up this method of calling, you are giving up something concrete; it’s not just a matter of taste; red or blue.

There could be other ways to solve this particular problem of wanting many functions with very short names. I don’t recall finding any other contenders, but I may have forgotten.

2 Likes

Because:

  • The intent is not to treat functions as properties of types. This isn’t an OO language!
  • What is desired is not piping restricted to single-argument functions. We already have that and it sucks.
  • What is being discussed is a language feature to support common thought patterns, not a dozen different libraries and macros that solve the problem in different ways and that will never become a standard.

CBOOCall is hoping to solve a very specific problem: allowing functions or objects with very short names (e.g. x, y, and z) to be conveniently accessed from a module without polluting the global namespace. It’s spiffy, but not what we’re discussing. At least not what I’ve been.

But if we were trying to address that, @jlapeyre could’ve just used Julia’s let statement. Using CBOOCall’s example:

using QCircuit # don't export x, y, and z
let x=QCircuit.x, y=QCircuit.y, z=QCircuit.z
    add!(circ, x, 1)
end

This could be more convenient if Julia had some methods for object destructuring… or maybe for broadcasting access of properties, for example

..(obj, props::Tuple{Vararg{Symbol}}) = getproperty.(Ref(obj), props)
using QCircuit
let (x, y, z) = QCircuit..(:x, :y, :z)
    add!(circ, x, 1)
end

Or even, maybe Julia could do something in the same spirit as JavaScript’s with statement, to create a block where that namespace applies and outside which it doesn’t… perhaps

with QCircuit
    add!(circ, x, 1)
end

Then any objects and methods of the QCircuit module would be available, but only within that block. I’ve seen some gnarly Julia code where this feature would substantially reduce code complexity and interdependence.

… Anyway, I digress.

Ah. Unfortunate.

Very well.

I guess -- isn’t being used for anything. Maybe?

my_object--mymethod()

has a ring to it :thinking:

Some other possibilities: **, ||,

(Although I’d prefer || to be reserved for an operator with same precedence as addition, but sacrifices must be made)

We’re already on 1.8… queue meme of scheming raccoon

1)Conventions set by OOP languages and 2) easily colliding names that shouldn’t be encapsulated and exported by the module seem like reasonable justifications. For completion’s sake, I would only ask why circ(:x, 0) doing getproperty(QuantumCircuits, :x)(circ, 0) isn’t enough. My guess is that it’s type-unstable, but I think the compiler does extra tricks with getproperty so I’m not sure.

Then you already lost me at using UFCS to mimic OOP syntax.

  1. In some OOP languages, you can access encapsulated functions a = x.f and call it later a(y). Without the properties approach of CBOOCall.jl, x.f wouldn’t even have a valid meaning, and the parser would probably lower that to a getproperty call doomed to fail.

  2. Concerning autocompletion based on the first type alone, it would only be performant with type properties/class encapsulation. Searching every method ever to find the ones with the right first argument, especially when there are supertypes to check, is excessive work. CBOOCall.jl’s use of propertynames would be autocomplete’s best friend.

  3. OOP programmers won’t like using anything but . for this; .. is already pushing it and -- will be a non-starter. R is still getting complaints for using <- and -> as assignment.

  4. And the best for last, how would the parser tell if x.f(y) should be lowered to f(x, y) or if x really does have a callable field f accepting 1 argument? In fact, those can exist simultaneously:

struct X  f::Function  end
x = X(z -> 1)
f(x::X, y) = 2
println(x.f(0), ", ", f(x, 0))

There is Chain.jl and Pipe.jl, they handle piping to any functions iirc.

I wouldn’t write all that, I’d sooner import QCircuit as QC and write QC.x everywhere.

3 Likes

The main benefits to UFCS, IMO, have nothing to do with the object oriented paradigm. It’s just nice syntax that allows for noun→verb ordering, without being as limited as the pipe |>. Equating UFCS with OOP is a mistake imo. See this post:

I’m not convinced that’s necessary. Encapsulation isn’t needed frequently, and an anonymous function can be explicitly defined when it is.

Use of . should be limited to property getting. Keep it simple.

Macbooks are pretty powerful these days… I guess we’ll see. Likely more performant than my brain anyway.

Not convinced. I prefer + for string concatenation and * for string replication, but Julia uses * and ^ respectively. It’s not the end of the world.

But also, it’s a beneficial reminder to OOP programmers that this isn’t an OOP concept; we are calling a globally accessible method with the object as the first argument, *not* calling a method that “belongs” to the object. Hence why syntax other than . is good, to disambiguate for the compiler and also for the person between property getting and method calling.

But please, characters that are easy to type without errors. |> is such an error-prone sequence of characters.

That’s why I was proposing .. to disambiguate from property getting. Or perhaps --.

Yes, there are about half a dozen packages that solve the same problem in slightly different ways, so we’re unlikely to have a standard anytime soon unless it’s incorporated into the language.

Oh. Oh my. Oh my oh my. I just learned about Julia’s new destructuring syntax. :clap: :clap: :clap:

(; prop1, prop2, prop3) = my_object # prop1 == my_object.prop1, etc.

Game. Changer. Sí se puede.

Strangely it doesn’t seem to work for destructuring modules… that seems like a bug (considering that my destructuring code above does work on modules, and supposedly they work on the same methods).

If it worked, @jlapeyre could write:

let (; x, y, z) = QCircuit
    add!(circ, x, 1)
end

Wonderful syntax :pinched_fingers:

1 Like

Your proposal is logically consistent as it knowingly eschews OOP, so pardon my misunderstanding because this thread was initially about implementing such syntax for OOP users :melting_face:, who seem to be the vast majority of people requesting this syntax.

At least do-blocks let you input an anonymous function into the conventionally first argument of higher order functions without needing to assign the anonymous function to a variable or the anonymous function splitting the higher order function call in two. By comparison, there is no benefit to having a special syntax for x--f(y) but not y--f(x) to mean f(x,y); in fact we have Chain.jl and Pipe.jl that can differentiate and do both.

What better reminder of f being a global function that does not belong to x than writing f(x, y)?

1 Like

Actually, x--f(y) meaning f(x, y) brings the benefit of encouraging what is frequently the most reasonable argument ordering, just as it’s reasonable for the subject of a sentence to precede the verb and predicate. At least that’s the case in the spoken languages I know.

Chain.jl, Pipe.jl, etc. offer flexibility over which argument the object gets placed into, rather than forcing it into the first argument. So you get the flexibility to put the predicate before the verb and subject; you can also put an adjective first, or an adverb first. But in exchange for this extra flexibility comes extra awkwardness from having to type more characters and having to think about which argument to place the object in every time. It’s a trade-off which frankly isn’t worth it most of the time, especially if argument orderings are defined well.

Well I would hope the purpose of the language isn’t just to remind me that functions don’t belong to objects! :sweat_smile:

Consider the following instructions describing a sequence of transformations to a fruit:

Take an orange. Peel it with your fingernails. Split it. Put it in a bowl. Eat it slice by slice.

Imagine if the English language insisted I present this sequence as:

Eat the (put the (split the (peel an orange with your fingernails)), in a bowl), slice by slice.

It’s in an ordering that hardly makes sense, all because we wanted a reminder that verbs don’t belong to nouns. A bit masochistic if you ask me!

And that’s where noun→verb chaining with UFCS shines. It’s piping, but with predicates, adjectives and adverbs.

I take this back. It’d be easy enough for my_object--mymethod to capture my_object in an anonymous function:

my_object--mymethod # yields an anonymous function,
(args...; kwargs...) -> mymethod(my_object, args...; kwargs...)

Okay so I tried it again, and destructuring actually does work on modules. Not sure what went wrong the first try.

So with that said, @jlapeyre give this approach a shot and see how you like it:

using QCircuits
let (; x, y, z) = QCircuit
    add!(circ, x, 1)
end

What about the following translation for your orange example?

let it = orange
    it = peel(it, fingertips)
    it = split(it)
    it = put(it, bowl)
    eat(it, slice_by_slice)
end

Given that I actually like the doto macro from Clojure – making Java interop quite nice despite Clojure strongly favouring functional programming:

(doto orange
   (peel :fingernails)
   (split)
   (put :bowl)
   (eat :slice-by-slice))  ;; only works by side-effect - doto returns final obj itself

Should be rather easy to replicate with a macro in Julia…

That looks like Chain.jl’s @chain.

@chain orange begin
peel(:fingernails)
split()
put(:bowl)
eat(:slice_by_slice)
end
1 Like

Except that doto works purely by side-effecting the object and does not pass the results along, i.e.,

begin
    it = orange
    peel(it, :fingernails)
    split(it)
    put(it, :bowl)
    it  # returns the updated object at the end
end

Clojure also has chaining macros -> and ->> which pass the results of each function as the first and last argument respectively. doto is mostly useful for Java objects with side-effecting methods predating fluent interfaces, e.g., not returning the object itself.

2 Likes

I’d much prefer

orange--peel!(:fingertips)--split!()--put!(:bowl)--eat!(:slice_by_slice)

# note that by convention, Julia methods which 
# modify their arguments have names ending in "!"

Code continually reassigning it to the newest orange transformation isn’t particularly insightful. Waste of space and time.

I didn’t know that about Clojure. Excellent example of a functional language implementing UFCS for the betterment of mankind.

As an aside, it’s an interesting idea to have syntax ->> for inserting the object as the last argument. How frequently does this end up being used?

Hah, the -- operator is growing on me.

Dot . to get properties of this object; dash -- to invoke methods on this object.

Not to be confused with Morse code.

1 Like

That’s not really UFCS: afaict, Clojure’s -> is Chain.jl’s @chain, and ->> is requested in Macro to pass last argument · Issue #48 · jkrumbiegel/Chain.jl · GitHub

Fair. It’s as close to UFCS as you’ll get for a LISP.

Point is that they felt it wise to insert the object as the first argument in a similar fashion to UFCS.

In Clojure all sequence functions, i.e., map, filter and friends take the sequence argument in the last position and ->> is handy for pipelines in that case, i.e., (->> (range 10) (filter even?) (map double)). Maybe less useful in Julia and in any case transducers have superseded much of the use for it in Clojure and Julia has a package for that as well Transducers.jl.

For what it’s worth, what about this syntax using @chain?

using Chain

°°(x) = x  # Unfortunately, -- is invalid syntax

@chain orange °° peel!(:fingertips) °° split!() °° put!(:bowl) °° eat!(:slice_by_slice) 

It’s funny that UFCS is actually coming to functional programming language given that the history in Lisp had been the reverse:

  • Multiple Lisp dialects explored OOP – with fancy dynamic features such as whoppers or flavors – in the 80s and beginning of 90s, e.g., using a syntax such as (send obj ':method args...) (see for example Flavors)
  • In turn, this was considered awkward and at odds with the usual function syntax and changed into (method obj args...) paving the way for multiple-dispatch as the first argument no longer appeared special and eventually standardized in CLOS – the Common Lisp Object System.

I.e., syntax is important and can suggest novel ideas if approached open minded. Imho generic functions with multiple dispatch feel much more natural and allow for impressively reusable code. Message-passing OOP arguably still struggles to deliver on its promises here and I keep hitting roadblocks in those languages, but maybe it’s just me.

Maybe a much simple solution is to break the syntax (in Julia 2) and reserve the usage of -> for OOP function call and use ->> for anonymous function (or a Haskell style anonymous function like \x → x + y). Anyway, if this feature is intended, we need to modify how frontend lowers this expression…

For example, in this new syntax, we have:

map(x->>x+y, array->push!(1)->push!(2)->vcat([1,2,3]))

Since -> is used also in C/C++, I think this syntax is more acceptable than other notation (except for ., but we can’t use this symbol, it’s ready abused).

In Julia, many data processing functions take the “object” they act on as the last argument, not the first. Both in Base (eg map, filter, …), in stdlib (eg Iterators equivalents), and across the ecosystem. Such functions are one of the most natural ones to use in multistep pipelines, so “pass the object as the first argument” often isn’t what one needs. Moreover, many of these functions take another “lambda” function as an argument.

In my opinion, DataPipes.jl nicely addresses this common pipeline usecase. It puts the “object” as the last argument by default, and makes writing lambda functions more convenient. disclaimer: author of datapipes

Does this convenience issue - that can easily be solved in a package - really warrant a syntax change in Julia?

1 Like

Indeed, the last argument is often more convenient in functional languages, especially in combination with partial application or currying:

partial(f, args...) = (more...) -> f(args..., more...)

[1,2,3,4] |> partial(filter, iseven) |> partial(map, partial(*, 2)) |> sum

In this case, no new syntax is necessary as everything is just function call. Furthermore, it’s just a small steps towards tacit programming

∘̈(f, g) = g ∘ f

pipeline = partial(filter, iseven) ∘̈ partial(map, partial(*, 2)) ∘̈ sum
pipeline([1, 2, 3, 4])

and who wouldn’t love that :wink:

Sure, it’s possible! But isn’t exactly convenient or easy to read.

Btw, your partial() is basically Base.Fix1 for simple cases of only two arguments.