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

Going back to your original example, my impression is that the way of thinking it is not the natural way to think it in Julia. You defined a type for a filter, where it would be more natural to define a function directly, such as:

function IIRfilter(xv;state=nothing,alpha=0.5)
   @assert 0 <= alpha <= 1 "alpha must be between 0 and 1"
   xf = similar(xv)
   if state == nothing
     state = xv[begin]
   end
   for i in eachindex(xv)
      xf[i] = alpha*state + (1 - alpha)*xv[i]
      state = xf[i]
   end
   return xf
end

and use it with:

xv = rand(3000)
xf = IIRfilter(xv,alpha=0.9)

maybe in Julia we separe more the data (xv) from the action over the data (the filter). This is not directly related to the original post, but maybe is a way to start moving to a more function-centered programming, which will be more natural in Julia.

Another small things:

This can be written as (and collect there will create an unnecessary vector):

for i in 1:N
  xf[i] = filter(f,xv[i])
end

or as

xf = filter.(f,xv)

where the . indicates broadcasting. (maybe this does not work in your case because the operations depend on the previous state).

This is rand(N) (there is a difference in Julia between a matrix with one column and a vector, probably you want a vector there).

@Tamas_Papp , it’s more like going to Italy and saying, can you please add the expected time of arrival of the next bus at the bus stop so I don’t sit for half an hour wondering if I will ever reach my hotel or my plane on time? (You don’t have to be like Japan where they start an investigation for a 1-minute delay, but you know somewhere between Germany and Spain)

2 Likes

@leandromartinez98 if the input is not available all at once (i.e. you get one sample at a time, you update the filter, do something else, etc) you need to maintain the state between multiple calls, that’s why I used the “object” approach.

In C++, the desire to use the dot syntax sometimes leads to poor design, because users become more focused on the syntax than the design. Examples:

  • There is no way to forward-declare C++ methods. That can lead to #include bloat that slows down compilation.
  • Making all functions that can act on an object into methods leads to kitchen-sink design. I tend towards providing only enough methods to complete the abstraction.
  • The syntax works only for classes/structs. Hence it’s often inappropriate for the signature-based style used for template programming. E.g., C++ STL decouples algorithms from containers by making the algorithms stand-alone functions.

So I find myself using method less these days in C++ than when I started using C++ in 1988, and instead use more stand-alone functions.

8 Likes

I know it’s just an example, but Germany is a particularly bad analogy since in some cases trains that are delayed too much may not arrive at all and instead just turn around midway. If it never arrives, it can’t be late, making for a better statistic :man_shrugging: (Source in german)

In that case, an iterator approach (which carries its state as part of the protocol, not as part of a struct) may be preferrable.

1 Like

This metaphorical race will never stop, I realize, but I disagree that it is more like the bus analogy than the pizza analogy.

I do think this is a pretty fundamental change, along the lines of “please replace your time- and work intensive slow-cooking cuisine with convenient, affordable and cheap highly processed industrial food.”

Easy and quick to prepare, yeah. Good for you…?

1 Like

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)


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)

3 Likes

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

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

3 Likes