Claim (false): Julia isn't multiple dispatch but overloading

I don’t understand the purpose of this statement. What are you hoping to achieve by making it? During this topic as I said and demonstrated, I was given examples that are easily written using overloading. The fact is I have interfaced with many experienced programmers that write Julia code, hardly any of them that are not deep within the Julia community have any appreciation of multiple dispatch, because everything they see is easily written using overloading.

This is the last message I’m going to write on this topic. I’m clearly wasting my time here.

2 Likes

FWIW, there’s already a bullet point dedicated to static dispatch and virtual functions here. Perhaps it should go into more detail?

@dataSurfer, maybe you’d have suggestions on how to improve the above documentation. Additionally, re:

The fact is I have interfaced with many experienced programmers that write Julia code, hardly any of them that are not deep within the Julia community have any appreciation of multiple dispatch, because everything they see is easily written using overloading.

I think this is a good example/practical use-case of dynamic dispatch.
Specifically:

    Rpre = CartesianIndices(size(x)[1:dim-1])
    Rpost = CartesianIndices(size(x)[dim+1:end])
    _expfilt!(s, x, α, Rpre, size(x, dim), Rpost)

More broadly, here is documentation on function barriers.
Here, you dynamically dispatch on the _expfilt! method based on the runtime value of dim.

2 Likes

This depends on the purpose we envision for the of “Noteworthy differences …” sections of the manual.

When I started using Julia, I thought of these sections as a quick way to get started with the language (you just pick up what’s different and start coding). But even with experience in very closely related languages (notably, CL) I quickly realized that one really needs to learn Julia as Julia and that requires working through the manual.

These days I think that the “Noteworthy differences” section should just document

  1. the obvious obvious differences in surface syntax, but keep this to a minimum,
  2. concepts and techniques that are present and idiomatically used in language X, but not in Julia, either because they are missing, not needed, or replaced by alternatives, so no time is wasted looking for or trying to replicate them.

Specifically, I think that the most useful section of the manual for newcomers from other OO-ish languages is the methods section, in particular the design patterns.

A corollary of the above is that the “noteworthy differences” sections should be radically culled and reorganized; it is now getting too detailed and unstructured. (This is just my opinion though, I am sure some users disagree).

3 Likes

Just so you know, @dataSurfer already addressed that here:

2 Likes
2 Likes

The compiler can figure out the type sometimes, so I think that statement can be made more precise.

What if f() has more restrictive signature than g(), so some arguments that are acceptable to the latter aren’t for the former?

this code is defining f, and doesn’t have any type signature on f… therefore f is just g by definition.

" since the signature of f has absolutely no effect on the dispatch"

Implies that it’s not a complete definition.

Then it will error, or dispatch to other methods. I am not sure how this is relates to the original example above though.

I am not even sure what you are saying here, a lot of context is missing. In any case, Julia’s dispatch semantics are well-defined and documented in detail.

in Julia when you call the function with a new type it will compile the new code… this is an implementation detail, not a semantic definition… the function f just means do whatever the function g does. The function is defined by it’s meaning not the machine code it compiles to. otherwise Everytime the compiler got revised your entire program would change meaning.

1 Like

No that’s irrelevant, it’s an optimization which I did mention in the previous sentence. So the “more precise” part is in the context that you removed.

It still has absolutely no effect on what g(a, b) is dispatched to. Unlike in C++, which I did mention in the sentence you quoted, where changing the type in the signature will change the dispatch even if the same arguments are passed in.

What’s “it”? In my example code, I have the complete (i.e. runnable) definition for both f and g. The only one mentioned is f here and by all mean f(a, b) = g(a, b) is a complete definition of the function.


And again, as I’ve said above and as @dlakelan just repeated. A lot of what people are comparing here, including the comparison that started the thread, is the optimization in julia and the semantics in other language (say, c++). That’s not a fair comparison for either languages.

It’s true that we don’t talk about semantics vs optimization in this regard very much and we makes looking at optimizer behavior much easier. And that’s why the confusion is understandable. But that’s because,

  1. There’s less overlap between th two. (Unlike in c++ where there are non-virtual function is already a thing on its own without devirtualization)
  2. We have a REPL and a “JIT” so we can do better and provide better tools
  3. Allowing people to look at optimizations helps them write better code
  4. The distinction is not that useful for understanding the language itself.

And not because the two are the same thing.

8 Likes

" since the signature of f has absolutely no effect on the dispatch"

What’s “it”?

The definition given.

In my example code, I have the complete (i.e. runnable) definition for both f and g .

It does not specify the signature of f(), however, you state

The only one mentioned is f here and by all mean f(a, b) = g(a, b) is a complete definition of the function.

It is a complete (meaning, runnable) definition of the function, but does not define its signature.

For f(a, b) = ..., its type signature will be Tuple{Any,Any}. Eg see

julia> f(a, b) = g(a, b)
f (generic function with 1 method)

julia> methods(f, Tuple{Any,Any})
# 1 method for generic function "f":
[1] f(a, b) in Main at REPL[3]:1
5 Likes

That’s vacuous.

Well, that’s what it is. :man_shrugging:

4 Likes

Not any more than defining a C++ function for a base class reference. Also note that in the same reply just below it, for better comparison with C++ I did specify a signature corresponds to what one can do in C++.


I did just realized that I flipped f and g :man_shrugging:

I’d like to chime in and second that statement.


I used to think like you, as a user trying to learn the language. Teaching Julia led me to change my view on this, though. I’d say the main reasons for that are:

  1. Developers who already have a previous experience with a language (sometimes dozens of years of experience) tend to try and translate everything to the language they know. (Maybe in a similar way to what happens when you learn a foreign language: at first you do not think in that language, but rather translate everything back and forth between that language and your mother tongue).
  2. This is especially true of developers with a C++ background, who already know about all these notions: static dispatch and dynamic dispatch both exist in C++. Static dispatch on multiple arguments is possible via overloading. And these tools allow them to do whatever they want to do, in most situations. Or at least they have come to a point where they do not see the limitations of single dynamic dispatch any more (especially in the communities of developers that I know the most, where people look for performance above all, and don’t use dynamic dispatch much if at all). Such people are not easily convinced that multiple dynamic dispatch is that useful.
  3. It is surprisingly difficult (at least for me) to come up with real-life examples where multiple dispatch is actually used in a plain, easily readable way (like in the contrived example of the race between animals). There are a few cases where the algorithm to be used depends on the types of two arguments, and multiple dispatch is really useful. But there are also lots of cases where we try and separate the concerns, so that in practice we would write only one generic function, that itself performs several calls to methods where only single dispatch will occur. As an example of what I mean by this, a slightly different version of the race between animals could have been written in a way that only ever involves single dispatch:
abstract type Animal end
struct Lizard <: Animal name :: String end
struct Rabbit <: Animal name :: String end

climbing_speed(::Lizard) = 2
climbing_speed(::Rabbit) = 1

running_speed(::Lizard) = 1
running_speed(::Rabbit) = 2

race(l::Lizard, r) = race(l, r, climbing_speed, "wall climbing")
race(r::Rabbit, l) = race(r, l, running_speed,  "a normal race")

function race(a, b, speed, title)
    sa = speed(a); sb = speed(b)
    if     sa > sb "$(a.name) wins in $title."
    elseif sa < sb "$(b.name) wins in $title."
    else           "$(a.name) and $(b.name) run forever"
    end
end

function meet(a::Animal, b::Animal)
    println("$(a.name) meets $(b.name) and they race!")
    println("Outcome: $(race(a,b))")
end

We could probably argue about the compared merits of a single-dispatch implementation (like this one) and a multiple-dispatch implementation (like the original one). But I guess my point is that in a lot of cases, one strives for such a separation of concerns, so that adding new types does not involve exploring all possible combinations of argument types. And the optimal solution in such cases is often (in my experience) a series of single-dispatch method calls.

I would also say that what makes Julia special here is not really the possibility to use multiple dispatch. It is rather IMO the possibilility for us to trust the compiler to actually combine all these small definitions into a set of highly optimized meet functions (possibly one for each possible combination of argument types), so that we don’t have to write them ourselves. (EDIT: this is probably clear to anybody reading this, but the Julia compiler is smart enough to propagate the constants here, and avoid producing code that actually compares the animal speeds at runtime).

3 Likes

I am surprised to hear this. Base, the standard libraries, and many prominent packages are full of examples. “Easily readable” is of course subjective — multiple dispatch is most useful when you are organizing complex code, so perhaps this will make most real-life examples a bit more complex than the pedagogical ones.

These are independent features of Julia, but with an important synergy: the compiler cleverness is amplified by multiple dispatch. Still, without multiple dispatch, Julia would be a much less powerful language.

What I actually find surprising is how few contemporary languages use multiple dispatch, given how powerful a concept it is. I would speculate that this happened because despite clever hacks (method caches etc), it has a performance cost without a compiler like Julia’s.

4 Likes

There are many easy cases for writing generic code that you have to use dynamic dispatch to get the correct behavior, including basically all of the examples I’ve posted above. Simple f(a, b) = g(a, b) is such an example. You can’t write that in c++ using a normal function. It is such an trivial example that you won’t realize you are using it.

I would say that generic functions are indeed, in terms of using “runtime types”, very similar to c++ template. The main differences are then in many other related properties of the language,

  1. It works for type unstable code. (You can’t write everything in C++ using templates since that still require you to know all the types at compile time but you write all code using generic functions in julia.)
  2. They have better defined and easier to use dispatch rules. C++ template dispatch uses SFINAE and it is much harder to extend than the dispatch rules in julia.

Note that the “runtime types” are in quote since they are still compile time types, they are simply more “transparent” and doesn’t limit the type further.

4 Likes