Understand the pattern of multiple dispatch

Multiple dispatch is a famous feature of Julia. But I find I am a bit confused that some standard APIs do not embrace this feature as expected.

Here is one example with code:


julia> max(1, 2, 3)
3

julia> max.([1 2 3], [0 3 6])
1×3 Array{Int64,2}:
 1  3  6

julia> maximum([1 2 3])
3

Another similar example are the exp(number), exp.(array), expm(matrix) and the exp_fast(number) functions. Then here comes the question:

Instead of creating 3 calling styles, isn’t it nicer to have a single max() that works for all these 3 use cases? It makes me feel smart, powerful and relax as an API user:) The input parameter types are seemed clearly distinguishable for the multiple dispatch purpose. (I have read the “syntactic loop fusion” feature and yes it’s great and convenient to use when necessary. But I feel it does not answer my question here.)

1 Like

This blog post does a really nice job of explaining the advantages of the new dot syntax, but to answer more directly:

Multiple dispatch allows you to define different versions of a function on different argument types, and for a long time many APIs did use that to support both scalar and vector versions of functions like exp, sin, etc. This wasn’t entirely uniform though (it relied on methods being defined for each such function), so the caller had to guess whether a given scalar operation would also automatically broadcast. Because that’s such a common thing you want to do, the new dot syntax lets the caller decide when they want to broadcast a scalar operation across a vector, and the package that defines the function only needs to define the scalar version.

The max/maximum issue I agree is a stumbling block, but it’s tricky because they’re actually subtly different functions, where max gives the largest of the arguments, and maximum gives the largest element in the given collection. If we had a unified_max function, what should unified_max(x) return? max(x) returns x, but maximum(x) returns the largest element in x (assuming it’s a collection).

I suppose if splatting large arrays wasn’t a performance problem than you could write maximum(x) as max(x...), and maybe make dims a keyword argument.

5 Likes

In addition to what @ssfrr said above: yes, one could get away with

mymax(a, b) = a > b ? a : b

mymax.([1,2,3], [0, 3, 6])

reduce(mymax, [1,2,3])

So maximum would be reduce(max, ...) or something like that. The fly in the ointment would be that making maximum(::Range) and other special stuctures fast would be more difficult (if even possible) than it is now.

In general, while the Base library is work in progress, many of its features have received quite a bit of polish. When in doubt, it pays to think about it a bit, usually things have a reason.

max and maximum used to be the one function max, but was split into two because it was too confusing to cram too much distinct functionality into the same name: https://github.com/JuliaLang/julia/issues/4235

where max gives the largest of the arguments, and maximum gives the largest element in the given collection. If we had a unified_max function, what should unified_max(x) return?

The unified_max() returns the largest value of the input arguments for the first, and the largest value of the “input value collection” for the second. The return type of the unified_max() is then dependent on the input parameter type. Any problem with this convention?

I found a short but easy to agree answer from #4652:

The current naming is linguistically the least confusing: you take the min of a and b but you find the minimum of a vector or a matrix.

And by following #4235, I guess the current style is just a natural evolving process of the language design. max() is a general operation and it is coupled with many different use cases. Whether a unified_max() with reasonable keyword arguments would satisfy those use cases is not so clear, and may not be implementation-wise friendly at the moment.