Hacker News new | ask | show | jobs
by enriquto 1798 days ago
can you show a simple example of that? I tend to see multiple dispatch as a mental burden, (e.g.: when I see a function call, where will it be dispatched? the answer dependa on the types that I'm juggling, that may not even be visible at that point...)
2 comments

The key to make multiple dispatch work well is that you shouldn't have to think about what method gets called. For this to work out, you need to make sure that you only add a method to a function if it does the "same thing" (so don't use >> for printing for example). To Dr the benefit of this in action, consider that in Julia 1im+2//3 (the syntax for sqrt(-1)+2 thirds) works and gives you a complex rational number (2//3+1//1 im). To get this behavior in most other languages, you would have to write special code for complex numbers with rational coefficients, but in Julia this just works since complex and rational numbers can be constructed using anything that has arithmetic defined. This goes all the way up the stack in Julia. You can put these numbers in a matrix, and matrix multiplication will just work, you can plot functions using these numbers, you can do gpu computation with them etc. All of this works (and is fast) because multiple dispatch can pick the right method based on all the argument types.
> The key to make multiple dispatch work well is that you shouldn't have to think about what method gets called. For this to work out, you need to make sure that you only add a method to a function if it does the "same thing" (so don't use >> for printing for example).

Thanks for writing this. I think it is an important concept for getting started with Julia. When I tried Julia, I was initially confused and concerned about the subtyping hierarchy, which as far as I understand is undocumented. "Apart from a partial description in prose in Bezanson [2015], the only specification of subtyping is 2,800 lines of heavily optimized, undocumented C code." [0]. Assurance that users can safely ignore the subtyping hierarchy if we maintain semantic equivalence between methods, and that this actually works out in practice, makes it easier to commit to using the language.

[0] https://dl.acm.org/doi/10.1145/3276483

This is an important point: abstractions like generic functions are only as good as how much people respect them, and enforcement of that is largely cultural. You can add technological levels of enforcement, but those are limited at best because people are brilliantly devious and can find ways to break any technological enforcement if they set their minds to it; the key to preventing that is convincing them to set their minds to respecting abstractions instead.

For a negative example of this, C++ introduced function overloading — which is a static and therefore broken version of multiple dispatch (that calls the wrong “methods” based only on static type information). They then immediately decided to abuse the bitshift operators for I/O — in the standard library, no less. (There are other abuses of overloading but this is the most famous one.) So that didn’t exactly create a culture where people respect the semantic meaning of overloaded functions. As a result, C++ has given function overloading a really horrible reputation. Partly because it is likely to be not actually so what you want because it’s static rather than based on the actual types of arguments, but even more so because there’s no culture of semantic consistency in C++, starting from the standard library itself — you cannot trust anyone to respect the meaning of anything, not even the language authors.

In Julia, on the other hand, we’ve always been very strict about this: don’t add a method to a function unless it means the same thing. We would never dream of using bitshift operators to do I/O. Since meaning is the level where human thinking operates, this makes it reasonable to write generic code that works the way you meant it to: you can call `+` on two things and just trust that whoever has implemented `+` for those objects didn’t decide to make it append a value to an array or something wonky like that. Not that people haven’t proposed that kind of thing, but because of the culture, it gets shot down in the standard library and elsewhere in the ecosystem.

But yeah, it’s hard to see how this would happen because there is nothing technical preventing the same kinds of abuses that are rampant in C++, it’s just the invisible but extremely force of culture.

> We would never dream of using bitshift operators to do I/O

Or the sum operator for string concatenation, which is the epitome of non-commutative operation.

I like your point of view, but still I'm skeptical of function and especially operator overloading. Shouldn't these semantic constraints that you mention be enforced by the language? For example, the language does not let you overload + for a non-commutative operation, and so on.

I was curious what Julia decided to use for string concatenation (+ method would be the least surprising option for me since it's common for the most of source code in use, even while the string concatenation is non commutative).

I was surprised to find Julia uses the multiplication method for string concatenation. For me it feels as wrong as using bitshift operators for I/O but maybe I'm missing something.

+ is terribly wrong for string concatenation and people who used it first were illiterate freaks. If I had to chose among my many reasons to avoid python this by far is the strongest one. I'm forced to use formatting operators and string interpolation just to avoid the stupid +

Multiplication for concatenation makes perfect sense: it is commutative, associative, and consistent with mathematical notation. A space would be even better. And the empty string would be great, but maybe difficult to implement if you want to allow multiple-letter variable names, as julia seems to do.

Technical enforcement would be nice, but it's a hard design problem and my point is that even with that, you still need the cultural aspect. Time and time again, we see that culture trumps technology for these kinds of things. In this case, the proof is in the pudding: in Julia there is no technical enforcement of consistency, but semantic meaning of generic functions is broadly respected and generic code works. It would be nice to add some kind of protocols that support enforcement, but it's (apparently) not necessary and it's a hard design problem.

> For example, the language does not let you overload + for a non-commutative operation

It does allow it? I'm not even sure how one would disallow non-commutative operations. How would you know if a definition is commutative?

(Now I realize that you are one of the designers of Julia, I feel deeply honored by your answers. I really love your job with the language!)

> how one would disallow non-commutative operations. How would you know if a definition is commutative?

I don't think that this is possible without solving the halting problem. But in practice, you can do that by documenting this enforcement and making it unfeasible to overload a commutative operator with non-commutative code. For example, the callers of the overloaded commutative operator can and do assume commutativity to optimize compilation; as in, you fill a matrix with "f(i+j)" and it may only evaluate the upper-half of the matrix (this is even more interesting for the associative case). I think that Mathematica does a similar thing, I recall several symbols for operators to be overloaded assuming certain symmetries. As another example, in C++ you must overload "<" with something that is an order relation, otherwise the sort function fails. Being more fancy, a test option of the interpreter may run each call of the operator in a random order, etc.

Also, related to Mathematica, using juxtaposition for product is very natural to mathematicians. It is indeed hard point of friction when moving from Mathematica to Maple or Matlab.

> make sure that you only add a method to a function if it does the "same thing"

But this only concerns when I'm writing the code myself. If I read some code and I see a few nested function calls, there's a combinatorial explosion of possible types that gives me vertigo.

> complex and rational numbers can be constructed using anything that has arithmetic defined.

seriously? this does not seem right, it cannot be like that. If I build a complex number out of complex numbers, I expect a regular complex number, not a "doubly complex" number with complex coefficients, akin to a quaternion. Or do you? There is surely some hidden dirty magic to avoid that case.

There's no hidden dirty magic, but you're right: Complex numbers require `Real` components and Rationals require `Integer` numerators and denominators. Both `Real` and `Integer` are simply abstract types in the numeric type tree, but you're free to make your own. You can see how this works directly in their docstrings — it's that type parameter in curly braces that defines it, and `<:` means subtype:

    help?> Complex
    search: Complex complex ComplexF64 ComplexF32 ComplexF16 completecases
    
      Complex{T<:Real} <: Number
    
      Complex number type with real and imaginary part of type T.
One other really good example is BLAS. Since it is a C/Fortran library you have 26 different functions for matrix multiply depending on the type of matrix (and at least that many for matrix vector multiply). In Julia, you just have * which will do the right thing no matter what. In languages without multiple dispatch, any code that wants to do a matrix multiply will either have to be copied and pasted 25 times for each input type, or will have 50 lines of code to branch on the input type. Multiple dispatch makes all of that pain go away. You just use * and you get the most specific method available.