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

That is fine. The approach that @mcabbott roposed there (the functor) may the best one, then. Anyway, this is what it could look like:

using Parameters

@with_kw mutable struct IIRfilter
  @assert 0 <= alpha <= 1 "alpha must be between 0 and 1"
  state::Union{Float64,Nothing} = nothing
  alpha::Float64
end

# Apply to a single number
function (f::IIRfilter)(x::Number)
  if f.state == nothing
    f.state = x
  else
    f.state = f.alpha*f.state + (1-f.alpha)*x
  end
  return f.state
end

# Apply filter to a vector
function (f::IIRfilter)(x::AbstractVector)
  xf = similar(x)
  for i in eachindex(x)
    xf[i] = f(x[i]) # this calls the definition above
  end
  return xf
end

# set data
n = 3000
xv = rand(n)

# set filter
filter = IIRfilter(alpha=0.9)

# Use on a vector
xf = filter(xv)

# Use on a single data point
x = filter(1.0)


1 Like

Generally, multiple dispatch allows packages to expose a small and concise API. People just read the docs, use the functionality in InteractiveUtils, or grep the source code.

In most cases you don’t have to navigate tons of methods, just a small vocabulary. For example, consider arrays, one of the core building blocks of the language: a user would learn about size and [] (getindex, setindex!), and cover 90% of use cases. The remaining few verbs (similar, axes) complete the picture.

Or consider Optim.jl, a powerful optimization package written in native Julia. It exports two functions: optimize and maximize.

Julia is noun-, not verb-heavy.

4 Likes

In Italy we have the idea that everything in Germany is perfect and trains are on time. Having actually lived in Germany for some months I learned that Germany isn’t as perfect as we like to think and trains aren’t necessarily more timely than in Italy (it’s pretty much the opposite)

6 Likes

@giordano , sorry for using a stereotype, it’s never great, but to give some background I’m Italian (born in Sardinia), studied in Pisa and Parma, I lived and worked in Germany in Munich and Berlin (great public transportation) mostly doing research in theoretical/computational Physics and Astrophysics at the Max Planck Institute, traveled quite a bit, I lived in LA, and now live in Silicon Valley, as a US citizen married to an Armenian. Last vacation was spent in agriturismos in Assisi, Gubbio, Bologna plus sometime on the beaches of Sardinia: I highly recommend visiting Italy, don’t get me wrong :slight_smile: (and I recommend agriturismos, doing some research with a bit of luck you’ll find a great host and have a wonderful experience)

5 Likes

14 Likes

It’s true that at the bottom there are often only a few functions that a library needs, but new programmers can be misled by this. Because of multiple dispatch, there are often many higher-level functions that can automatically use the few core functions. And, I believe that proliferation of verbs (e.g. IterTools.jl) that work on a simple interface is a very good thing. The alternative to many functions is few functions with messy semantics, and that should be avoided.

The way to have clear and composable APIs is to have a lot of distinguishable functions with clear contracts. In Julia, those functions can all act on many data types automatically, but that should not obscure the guiding principle that a function should do one thing clearly.

I want more verbs!

3 Likes

@leandromartinez98 see Allowing the object.method(args...) syntax as an alias for method(object, args ...) - #23 by gianmariomanca

PS thank you for the example, still interesting.

One random thought I had today

struct PseudoClass{T}
    data::T
end

(o::PseudoClass)(f, args...; kwargs...) = f(o.data, args...; kwargs...)

This lets you write o(f, args...) the way you’d write o.f(args...) in class based languages:

julia> let x = PseudoClass(1)
           x(+, 1)
       end
2

julia> let s = PseudoClass(["apples", "bananas", "pineapples"])
           s(join, ", ")
       end
"apples, bananas, pineapples"

Interestingly enough, it also synergizes with julia’s do notation:

julia> let v = PseudoClass(1:5)
           v(1) do v, x
               z = 0
               for y in v
                   z += (y - x) / (y + x)
               end
               z
           end
       end
2.0999999999999996

And because it’s functions you’re sticking in the first argument to the pseudoclass, tab completion will work (though it won’t be method-aware):

julia> v(fi<TAB>)

fieldcount  fieldoffset  filemode     fill!        finalize     findall      findmax      findmin!     first
fieldname   fieldtype    filesize     filter       finalizer    findfirst    findmax!     findnext     firstindex
fieldnames  fieldtypes   fill         filter!      finally      findlast     findmin      findprev
julia> v(fil<TAB>)

filemode filesize  fill      fill!     filter    filter!
julia> v(fil)
2 Likes

Even more fun is that on version 1.6 and up, you can define a postfix operator that does this:

julia> var"'ᶜ" = PseudoClass
PseudoClass

julia> 1'ᶜ(*, 3, [4, 5, 6])
3-element Vector{Int64}:
 12
 15
 18

julia> [3, 4, 5]'ᶜ(dot, [6, 7, 8])
86

Another example using getproperty.

struct Class{T}
    __data__::T
    __module__::Module
end

macro class(data)
    :($Class($(esc(data)), @__MODULE__))
end

function Base.getproperty(class::Class, key::Symbol)
    if key == :__data__
        getfield(class, :__data__)
    elseif key == :__module__
        getfield(class, :__module__)
    elseif key == :get_class_data
        () -> class.__data__
    elseif hasproperty(class.__data__, key)
        class.__data__.key
    else
        try
            func = getproperty(class.__module__, key)
            if func isa Function
                return (args...; kwargs...) -> Class(func(class.__data__, args..., kwargs...), class.__module__)
            end
        catch
        end
        Class(getfield(class.__data__, key))
    end
end
julia> @class(1).:+(2,3)
Class{Int64}(6, Main)

julia> @class(1).:+(2,3).get_class_data()
6

julia> plus = +
+ (generic function with 190 methods)

julia> @class(1).plus(2,3).get_class_data()
6

julia> @class(1).bar()
ERROR: type Int64 has no field bar
Stacktrace:
 [1] getproperty(class::Class{Int64}, key::Symbol)
   @ Main ./REPL[40]:18
 [2] top-level scope
   @ REPL[50]:1

Tab completion can also work implementing a propertynames method.

My 2c. I honestly don’t understand why people think that object.method(args...) is simpler syntax than method(object, args...). Of course, that must be because I first learned C, then Matlab, Common Lisp, Scheme, Python, then Julia (not including languages which I learned but didn’t use much). The Python syntax does make some sense in Python, specially for simple functions in which the first argument is special. But it becomes a headache when what you want is two or three-args methods (e.g., the __add__ method and friends).

I don’t understand why it is natural for some people that methods to belong to objects. That makes absolutely no sense to me. Methods are functions, functions don’t belong to their domains. Of course, it is possible, in Python, to create methods after the class definition, but that is not idiomatic. However, quite often it makes sense to define new methods that work with classes defined by someone else (e.g. math functions with number classes).

IMHO, what Julia lacks is some built-in simple support for chaining, but I would prefer a syntax that makes clear that the method doesn’t belong to the object (like the @chain macro above, or like x |> f(_, y, z) |> g(_, w) which is being proposed).

15 Likes

By the way, you can easily do in Julia what you do with Python’s methods, even if it takes more characters. It is not so easy the other way around. There are modules which implement multiple dispatch in Python, but they are not as complete as Julia’s dispatch.

1 Like

It would be nice to have a way that’s

  • just syntax sugar for normal function call
  • very short (e.g. 1 character, normally used without spaces)
  • built into the language
  • considered idiomatic
julia> "x |> f(_, y)" |> length # pipe proposal
12

julia> "x.f(y)" |> length # Nim style 
6

julia> "x⤷f(y)" |> length # unicode character
6
1 Like

I agree it would be nice to have a syntax sugar, but I don’t agree shorter is always best (e.g. Perl scripts). IMO, clear, flexible and consistent is better than short (I think 1 char is not enough for clarity, flexibility and consistency).

5 Likes

I agree that the shortest symbols should be reserved for operations that are fundamental, and that those symbols should offer a visual mnemonic of their function. The clarity of a symbol is always dependent on the audience (e.g. Julia users know that . means broadcast; Mandarin users know that 地图 means map), but if you have too many then they’re hard to remember. > is a good example of a symbol that’s fundamental and mnemonic. A chaining operator may be sufficiently fundamental as well, probably some more experimentation is needed.

Yes, of course, clarity is relative, not only to people, but also to conventions of programming languages. In pretty much any language that accepts the syntax x.f(y), it means taking a field f from object x and applying it to y. In Python, this field is also called method, which is a function in which x is curried as its first argument, but that is detail. E.g.:

# python3
Python 3.9.5 (default, May 11 2021, 08:20:37) 
>>> func = "abcdefghij".replace
>>> func('a', '1')
'1bcdefghij'

So, the syntax the OP is proposing would be confusing to Julian users and to newcomers. Because, in Julia, methods are not fields and Strings don’t have a field called replace.

Now, the syntax x⤷f(y) is not confusing and it could be made clear and consistent. However, you need to remember the shortcut for ‘⤷’, which requires more than one char. Another problem is it’s not flexible - if you want to apply map and friends to collections, you would need to specify that the incoming object is the second or third argument of your method, so you would need some marker to specify in which place the object is curried - like _.

In the end of the day, vec⤷map(sqrt, _) and vec |> map(sqrt, _) requires that you type around the same number of chars, but the latter contains only one new syntax to understand - and that new syntax is useful in much more contexts than chaining.

11 Likes

Those are all good points, and it helps me clarify my own thinking.

x⤷₂map(sqrt) would work for that purpose.

x⤷₂map(sqrt)             # 12 char
x ⤷₂ map(sqrt)           # 14 char
x|>map(sqrt, _)          # 15 char
x |> map(sqrt, _)        # 17 char

I’m not advocating for this currently, still exploring the design space. Cheers for the thoughts.

1 Like

I think one of the possible reasons of the misunderstanding is that I can see 2 different use cases. One is seeing objects as something simple and primitive, like a mathematical object that requires some memory allocation but not much else, associated with a type and a variety of external methods (mathematical operations) that can be in common with many other types. And one where “objects” are large, very specialized blocks, with complex states and state transitions. Like blocks in signal/image processing pipeline. One block that does image segmentation, one that does classification, one that does temporal filtering, etc etc. In this second case there is an obvious “most-important” object, and people take comfort in having the complex internal state isolated from the external world, having the methods that can alter this internal state clearly declared in one place to “fix the API”, etc, etc.
I understand that this is an even larger discussion, and I’m mentioning it mostly to “get help” in clarifying my own thought process.
From “outside” it seems that Julia is focusing a lot on the first use case and not so much on the second. And it seems that there is some kind of understanding in the community that you cannot have both in the same language and from there a lot of discussion of which one is better “classical” OOP vs multiple dispatch, etc etc.

Is my (approximate) mental model of Julia incorrect? What am I missing? Is there a fundamental obstacle of having one language support both use cases. Is it a sociological risk?

1 Like

Not exactly. I do not believe Julia focus more on one case than the other (i.e., primitive vs complex objects). It is just that Julia design does not need the same machinery the OOP languages often provide, and providing this machinery would only lull programmers into a false sense of familiarity. Julia is not OOP, this does not mean there is any lack of support to large complex objects, and any OOP syntax/machinery would only be a crutch to people coming from OOP languages confusing the rest and, in the long run, probably harming even the people coming from OOP languages.

Sincerely, I am very tempted to just say “OOP is just a worse paradigm, no reason to tarnish Julia with that” but this would both disregard a lot of nuance and start a flamewar.

The thing is, the way Julia is, there is a lot of things that other languages have primitive mechanisms to enforce (and that enforcing things is seen as a good practice) but in Julia these things are done informally, this is, documentation say you should not and then you don’t. I prefer it this way and, unless the compiler can do more instead of less by allowing these extra constraints, these extra constraints end up being training wheels and nothing more.

1 Like

It’s not just a different syntax. In Julia, x.f already has a meaning "take a property from x which is named f". It cannot mean “take a function f from the global scope and curry f into it”, that would create ambiguities, inconsistencies and a lot of confusion.

See also my following comment:

6 Likes