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.
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.
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 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.
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.
Even if using multiplication to join strings may be consistent with some properties of mathematical notation, it's less consistent with the usual meaning of the words (I checked the cosine similarity using GloVe 6B words embeddings, the similarity between "add" and "join" is 0.4 while between "multiply" and "join" is only 0.03), so it's probably the balance between being the general purpose and math oriented language.
> 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?
> 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.
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.
help?> Complex
search: Complex complex ComplexF64 ComplexF32 ComplexF16 completecases
Complex{T<:Real} <: Number
Complex number type with real and imaginary part of type T.