Not Growing a Language
kevingal.com
kevingal.com
My programming experience is that that these "mathy" objects are about the only unambiguous use case for operator overloading. Given the limited use case, I'm not sure whether it's worth the added complexity for language designers.
I wrote the simulator engine code for CircuitLab (https://www.circuitlab.com/). We have complex numbers, sparse matrices, and extended-precision (more than a 64-bit double) floating point classes, all of which we use extensively within the simulation engine. Yes, it can be a bit annoying and verbose to do X.add(Y) instead of X+Y, but I tend to just leave an end-of-line comment to aid readability later. And this mathematical core code tends to be infrequently changed and well covered by tests.
Here's a not-so-mathy application that I had recently. I was implementing a units system in Python. Operator overloading allowed me to define joules like so: `J = KG * M**-1 * S**-2`. Then I could define grays (a radiation-related unit) like `Gy = J/KG`. Repeat for hundreds of units. If I had to do it in Java, I'd be frustrated by the verbosity and it would be easier to make mistakes.
My point -- Guy's point, actually -- is that if you don't give people the ability to grow a language, then their expressive power will be limited. And you can't anticipate ahead of time all the ways they'll want to express themselves.
Admittedly, my application is kinda mathy under the hood, because the units exist in a vector space. I guess that's to be expected when the operators come from maths.
Presumably exponentiation rather than multiplication by a constant, right?
This is cool and reminds me of how the Python Z3 binding uses operator overloading to let you express arithmetic and logical constraints
>>> import z3
>>> s = z3.Solver()
>>> a, b, c, d = (z3.Int(x) for x in "abcd")
>>> s.add(a*b == c+d)
>>> s.add(a+b == c+d)
>>> s.add(a!=0,b!=0,c!=0,d!=0)
>>> s.check()
sat
>>> s.model()
[d = 6, c = -2, b = 2, a = 2]
>>> s.reset()
>>> s.add(a>1, b>1, c>1)
>>> s.add(a*a + b*b == c*c)
>>> s.check()
sat
>>> s.model()
[c = 17, b = 8, a = 15]
(It's limited in some ways for some reasons, like for some types and relations you may have to use a Z3 function to express a relation even though Python has syntax for it -- the example that I know of is that you have to use z3.Or(), z3.And(), etc., with boolean variables instead of Python's builtin boolean operators. Not sure why.)*That's cool too! How do you define the variables?
... there we go.
You use Z3 objects that represent unknowns of particular types, like Z3.Int, Z3.Bool, etc. Each one returns an object representing a variable of that type, which can then be used in (Python-formatted!) expressions that you give to a solver.
Rust's ability for smart pointers to overload the dereference operator works pretty well, though there is still some potential for abuse.
Part of what makes it reasonable is the required function signature: It can only ever produce a reference to some other type, which rules out some of the crazier possibilities.
Yes. Most other uses for operator overloading are really chains of functions. There's better syntax for that.
I once wrote some overloads for C++ so that you could write
result = fileobject | item | item | item | item;
and then apply either "read" or "write" to "result". This allowed marshalling without writing the item list more than once. Bad idea.Python is notorious for problems that stem from combining operator overloading and implicit conversions. "+" is arithmetic for some types and concatenation for others.
[1,2,3] + [4,5,6]
is[1,2,3,4,5,6]
which you might not expect. If you add arrays from numpy, those add arithmetically. You can add a numpy array to a built-in array. Trouble comes when you pass a numpy array to a function that expects a built-in array, and the wrong semantics of "+" are applied.
Rust has operator overloading without implicit conversions. That avoids such ambiguities, at the cost of requiring many explicit conversions.
In Lua, `+` is always addition, and can be overloaded with an `__add` metamethod, while `..` is concatenation, overloaded with a `__concat` metamethod. `..` is also right associative, which means that `a .. b .. c .. d`, a normal enough string-building pattern, can be optimized into a single allocation for the new string, without having to create `a ..b` and then `ab .. c` and so on.
So I don't see this as a problem of operator overloading, I see it as a problem of a missing operator. Implicit conversion makes that problem worse, but `"12" .. 13` in Lua will give you "1213", as you would expect, and `"2" + 3` gives you 5, again, as you would expect.
Moving deeper into Unicode is probably not the answer. Although, if a language needed a concatenate symbol, ⊞ (Squared Plus) might be a good choice. It's not used for much else.
Rather than using context to differentiate bitwise NOT from concatenation, just rule that ~~ is always concatenation. What you lose is just "NOT NOT", which (without this operator) is legal but pointless.
(Aside: C (or the C preprocessor?) is the only piece of software that I know that supports that , but only for string literals)
The problem is almost always not the overloading of + but the implicit type coercion rules
>>>'a' 'b' 'c'
>>>'abc'It seemed obvious to me as soon as I saw it in Perl that the Perl approach ("+" for adding numbers, "." for concatenating strings) is a much better idea than using "+" for both operations.
I'm not sure why people keep wanting to use + for concatenation.
(My other language pet peeve is using backslash as the escape character for your mini-DSL that gets parsed from strings, when backslash is also the escape character for strings. You want to use separate escape characters for each context; stacking the same one in several contexts is the worst possible approach.
C was aware of the problems: it uses `\` as the escape for strings and `%` as the escape for printf commands. But somehow by the time we were writing regular expression libraries, this simple, obvious technology had been lost.)
The D community has generally agreed, and we haven't had much trouble with people overloading operators to do a regular expression DSL, for example.
Of course, if we just use s-expressions, then there's no difference between operators and functions and this whole thing becomes moot. Although, with the caveat that your nice infix math expressions are still not nice infix math expressions. Then again, if we are using sexps, then we're likely using a language with macros and writing an infix macro for those mathy parts wouldn't be such a bad idea... ;-)
Maybe its hard to achieve absolute best performance, but I'm not arguing for that, I'm only arguing that operator overloading leads to better programmer ergonomics. I can benchmark and convert to optimised functions after I have it working nicely. Ergonomics first, optimisations after.
The problems I had read about applied to operations on larger matrices, where the difference between n^3 and n^2.5 or whatever becomes extremely noticeable, and makes it worth to write mulAndAdd(a, b, c) rather than a*b+c.
It's a little bit of a kludge! But a clever one, which uses the precedence of the existing operators to match the expected precedence of concatenation and the `/` ordered choice operator from PEG grammars.
In general I'm suspicious of arguments that boil down to "people might use this language feature badly". I'm not sure I want the full ability to add my own infix and postcircumfix operators, the way e.g. Raku allows, but there's no denying that being able to write `if foo ∈ test_set` is expressive and cool.
Maybe expressive and cool isn't a terminal value for language design, maybe `if test_set.element(foo)` is good enough. I kinda like it though.
In languages which only let you overload existing operators; those operators often get used for a bunch of different things.
I mean, how many languages use `+` for both addition and string concatenation? … and maybe also set union?
Its kind of interesting that Raku’s own docs think of '=' being used for both item assignment and lost assignment as an exception to this, because it illustrates how narrowly Raku defines “one thing”. (As does the use of different postcircumfix operators for positional and associative access, which most languages call indexing, and use one construct for, which may not even technically be an operator.)
But its not just freedom to create new operators that allows this; for it to work Raku has a huge number of built-in basic operators, plus the hyper- and meta-operators.
my @a = 1,2,3;
is almost the same as my @a;
@a.VAR.STORE((1,2,3));
---As for separate positional and associative access:
Raku has separate positional and named arguments to functions.
An argument list is actually a singular thing called a Capture. Even if it usually pretends to not be.
my \capture = \( 'a', b => 10 ); # Capture litteral
say capture[0]; # a
say capture{'b'}; # 10
sub foo ( |capture ) {
say capture[0]; # a
say capture{'b'}; # 10
}
foo |capture; # give it the above capture litteral
# foo 'a', b => 10
sub bar ( $a, :$b ){
say $a; # a (positional)
say $b; # 10 (named)
}
bar |capture; # give it the above capture litteral
# bar 'a', b => 10
To be able to easily extract out those parts, it helps if there is a different way to get at each.The same thing also applies to extracting information out of regexes
'a10' ~~ / (.) # $0
$<b> = (..) # $<b>
/;
say $0; # a
say $<b>; # 10
say $/[0]; # a (positional index into $/)
say $/{'b'}; # 10 (named index into $/) scala> 1 +: List(2, 3) ++: List(4, 5) :+ 6
res0: List[Int] = List(1, 2, 3, 4, 5, 6)
where +: is right-associative and implemented by the list rather than the element.For example, there’s boost Spirit, a library to write parsers. You describe a grammar in something that looks a lot like a EBNF grammar (https://www.boost.org/doc/libs/1_67_0/libs/spirit/doc/html/s...)
- Vectors, matrices
- Immutable containers (lists, sets, maps, etc.)
- Domain specific languages (regular expressions, constraint languages, etc.)
And without good support for matrices, the data science and ML people will just use Python instead of Java to build their libraries. And then everyone will use the language with the best libraries, even if they don't use matrix math in their code.
For regular expressions, you have to use a DSL embedded in Strings in Java instead of using operators like | as operator on regular expressions directly.
People go nuts with language features because they can, not necessarily because they have a good reason. this it the big problem I have with Scala, for example.
Sure arithmetic operator overloading with numeric types makes sense. Even the parenthesis operator overloading in C++ for functors makes sense.
Then you have the Scala SBT build system and percent operator overloading.
I actually like how Rust does this. You don't overload the operators per se. You implement traits with named methods like Add that will allow you to use the operators. This, to me, is the best of both worlds.
I personally think Java has been just fine without operator overloading so there's no pressing need to add it now but, if they do, please follow the Rust model.
SBT is a fractal of bad design, the funny function names are just the most superficial of its problems.
> I actually like how Rust does this. You don't overload the operators per se. You implement traits with named methods like Add that will allow you to use the operators. This, to me, is the best of both worlds.
It's still just an extra unnecessary indirection where you have to remember which is which. What Scala does is much simpler - the + method is called +, the / method is called /, and so on. Yes, defining a function called %++< is probably a bad idea, but defining a function called YqqZ is probably a bad idea too, and few programming languages feel a need to ban that.
But even without strong constraints, the language's culture is a huge aspect of whether it becomes a problem or not. For example, in python, the language has almost[^2] no constraints on overloading, but people generally use it in very reasonable ways.
[^1] the typeclass/trait has an interface that is enforced, but there's usually also some implied laws that the type checker can't verify for you
[^2] the big exception being comparison methods like `__leq__` which throw a runtime error if the result isn't a boolean, much to the consternation of DSL writers
Haskell should have enforced that every operator must have a `name`.
Firstly, Hoogle is your friend
https://www.stackage.org/lts-17.8/hoogle?q=%3E%3D%3E
Secondly, you can Google them these days
What's the difference supposed to be? Why do I care whether my "add two abstract data types" method is named "__add__" or "operator+"?
String concatenation probably isn't the best example, but it's the first one that came to mind.
https://doc.rust-lang.org/std/string/struct.String.html#impl...
Is putting a method on a class called 'method overloading'?
I want to be able to define `plus` on my datatypes and I want to be able to define `+` on my datatypes.
`+` on my datatype overloads `+` on `Integer` no more than `get()` on my datatype overloads `get()` on someone else's datatype.
I am not taking an existing Integer type and giving it a new definition of `+`, which is what 'operator overloading' would be.
Concretely, in defining a + operator on your BigInt class that can only accept another BigInt, you haven't overloaded the + operator. It would only be overloading if you defined a second form that accepted some other type of object.
(Which I'll note most languages that support operator overloading support)
Personally, I've always wanted languages to be stricter about that kind of thing. It's not that hard to explicitly express the desired conversions to make the types on both sides of your binary math operators the same and it feels a lot more "controlled" to me to do so.
It most certainly is.. _if_ the method you are adding has the name of a functionality that is either inherently part of its spec or effectively so if by community conventions.
e.g. when you make a method with signature `boolean equals(Object other)` in java, it is literally, as in the very spec itself calls it this, 'overloading'.
The term is appropriate. `*` is commonly understood to be some sort of numeric operation with the properties that it is reflective, commutative, etc. If you decide to add a definition for a class you're writing, the term 'overloading' is entirely appropriate.
The problem with this approach is that, just as defining `plus(int)` on a class `C` allows you to call `c.plus(1)`, defining `+(int)` allows `c + 1` but not `1 + c`.
Whereas the approach of adding overloads to a global `+` operator can support both `c + 1` and `1 + c`, by allowing you to define `+(C, int)` and `+(int, C)`.
So, I take it we agree that expressing the quadratic formula for BigDecimals as "x.pow(2).multiply(2).plus(x.multiply(3)).minus(5)" is awkward.
Do you think that "x.^(2).(2).+(x.(3)).-(5)" is any better? Because if you were hoping to support "x^2 * 2 + (x * 3) - 5" then you'll need more than just allowing non-alphabetic characters in names... you will ALSO need special support for infix notation (which Java lacks today).
> In this post, I will use the same schtick.
This is a great and apparently unintentional illustration of why the belief in short words being simple is misguided. In its own introductory paragraph, it messes up in multiple ways by multiple standards.
Most obvious: nobody can understand "two nought nought nought". We'd have a much easier time with "two zero zero zero", even though the word "zero" is notionally 100% more complicated than the word "nought".
That was so obvious that the author helpfully glossed his own simplified language, knowing that we couldn't understand it. "two nought nought nought" means "2000". Good thing someone was there to dumb that ...up... for us.
Except... "2000" is "two thousand", and "thousand" is a word of more than one syllable. The author forgot to tell us what it means!
It turns out absolutely everything is very simple by this ludicrous standard. "Curl" is a non-evil one-syllable word, and it can be defined, also very simply, as ∇ × F (F: ℝ×ℝ×ℝ ⟶ ℝ×ℝ×ℝ). Look how easy that is to understand!
What does curl mean? Well, that must be a totally different question from "how simple is the concept?" I didn't understand what curl meant even when I was taking a class in it.
> In truth, the words of one syllable form quite a rich vocabulary, with which you can say many things. You may note that this vocabulary is much larger than that of the language called Basic English, defined by Charles Kay Ogden in the year one nine three nought[10]. I chose not to use Basic English for this talk, for the cause that Basic English has many words of two or more syllables, some of them quite long, handed down to us from the dim past of Rome. While Basic English has fewer words, it does not give the feel, to one who speaks full English, of being a small language.
Using one-syllable words is a fun way to make the structure of the talk reflect its thesis, with a rule that's simple enough to explain in one sentence. It's not meant to be a serious example of a simple language! If you did it seriously you'd get something like Simple Wikipedia, which reads pretty well[1]:
> A programming language is a type of written language that tells computers what to do. Examples are: python, ruby, jupiter, html, css, java, javascript, C, C++, and C#. Programming languages are used to write all computer programs and computer software. A programming language is like a set of instructions that the computer follows to do something.
That reads well and it's a good example of defining specialized words as you need them, but it doesn't feel all that different from "normal" English and it's much harder to explain what "simple" constitutes in that context. It works better in reality, but worse for a talk.
Limit operator overloading to classes that extend Number and 90% of the excesses of operator overloading are impossible.
You probably shouldn’t be using += for your concatenation code anyway (interpolation is often more powerful). The only other place you will “miss” it is in data types that are vector values, and you could probably figure out a rule for that in a later iteration.
As for abuse prevention: it would be impossible to use the common operators like +, -, +=, <<, >> and so on for anything that is not numbers, and therefore prevent C++ levels of overloading abuse.
class MyString extends Number
and you get access to operator overloads. Only thing that makes this annoying is Javas lack of multiple inheritance, but that is pretty minor thing.In short... math isn't just arithmetic, isn't even arithmetic of complex tensors. Math is way bigger than most programmers seem to comprehend. I'm on the other side of the DK curve; I know that I've only scratched the surface despite a decade of dedicated study
let check_exp (g : Elliptic_curve_pt.t) (x : int) (y : int) = let xy = x * y in let open Elliptic_curve_pt.Num_syntax in (g ^ x) ^ y = g ^ xy
This lets you avoid making hard decisions about what can be added/multiplied/etc. at the language level -- can a list of numbers representing the coefficients of a polynomial be added? -- without jumping to the extreme of allowing anything implicitly. This also lets the typechecker catch more of your mistakes by having stricter types, and means you know exactly where to go to find out what the syntax is encoding.
Elliptic_curve_pt.Num_syntax.( (g ^ x) ^ y = g ^ xy )
instead of let open Elliptic_curve_pt.Num_syntax in ...I agree. That's why D uses a separate operator for concatenation, ~=
That removes the temptation to overload +=.
But I agree that C# proves that we all learned our lesson and operator overloading is now just mostly used for numbers.
I think the messy part added by overloading specifically is that if `Complex`, `RealVector`, `List` etc all override addition, then there's some common class or interface which provides the declaration which they all override, and same for subtraction, multiplication, etc, and soon you have a large pile of interacting abstract definitions, which some language users will find daunting.
The problem for me is it's not clear if a.add(b) is "a+b" or "a+=b". With the regular operators it's always obvious which mutate the value and which return a new value, whereas the methods can do whatever they want. Yes, the operator is a method call and can do anything, but in practice everyone makes them do the obvious thing and follows the convention. There is no convention that "add" should return a new value - it's a verb, verbs usually mutate the object.
1) They are infix (often in languages where functions cannot be)
2) They can use one of a handful of special characters in their name
3) In languages with methods - which you could argue are called infix - they lack the extra ceremony of .()
These are all pretty much syntax concerns, and it seems like they could be solved at a fairly superficial level. I don't see much reason why operators need special treatment under the hood.
Edit: Now that I think about it, they're also polymorphic in some languages that lack polymorphic functions. But that's somewhat of an edge-case if you count methods.
C++ introduced operator overloading to the wide world, and it was promptly abused (with the stream operators, among other things).
But I believe the lesson was learned.
Few libraries hoping to achieve general usage will abuse operator overloading too badly, and if they do, potential users will resist adopting it widely.
There's an interesting class of language features, of which operator overloading is one, that are tempting to abuse which should really mostly only be used for what are essentially language extensions provided by a library.
I think what is more useful than even more language features to restrict the usage of these, is strong documentation and evangelism/marketing around the feature to help ensure it will be used appropriately.
Perhaps this is true for some language ecosystems and less true for others. But for the sake of argument, I’ll assume that this is generally true. Even in that case, if each library “abuse”s operator overloading in a new and interesting way, the problem still exists for the library ecosystem as a whole.
> I think what is more useful than even more language features to restrict the usage of these, is strong documentation and evangelism/marketing around the feature to help ensure it will be used appropriately.
I can’t imagine this scaling as well as building (or specifically choosing not to build) something into a language.
Full interoperability with Java, all the flexibility you could ever want.
a = b + c
rather than a.assign( b.add(c))Metasynactic operators (like quotes) are a different matter but you need so few it's no big difference.
I do write `c = a + b` in c++ but simply as it's the local convention. `=` is a non-intuitive assignment operator and no more natural than `assign`.
(+ 1 2)
Also, I don't think that a.assign( something )
is particularly natural or understandable. Am I assigning something to a, or assigning a to something?> Am I assigning something to a, or assigning a to something?
Seems to me that it would be causing a side effect on a, but as I said these are just arbitrary labels that you have to learn.
I find operator precedence weird and in 40+ years of continuous programming and doing math I have never been able to develop an intuition about them. But it's a thing you learn.
Personally I'm starting to lean more towards the opinion that infix operators/functions are not worth their complexity even for primitives.
The real reason people seem to like operator overloading is that it’s a feature that scratches a certain itch. We all love our code to look elegant and simple, and often times this need inspires developers to set down the path of language design. This is a long dark path that’s not for the feint of heart. Many who start down this road dabble for a bit and then turn back quickly when things get rough.
Operator overloading is a low barrier of entry to language design, so developers looking to make simple elegant code reach for it in the name of aesthetics. They get to feel like language designers by playing with syntax, while staying within the safety of a language they know and love. But in my opinion, the aesthetic gains are not worth the increase in complexity for the language. Both for writers and readers of code.
How is it any different in any way from any other overloading (i.e. generic functions or allowing two classes to have methods with the same name?)
Nevertheless I agree with your point.
But shouldn’t overloading be used where its results are intuitive thus lookup should rarely be needed? /s
On one hand it's a question of philosophy -- do you provide the user with tools to better express themselves, if they might possibly shoot themselves in the foot and create a crappy interface?
On the other hand, it could be an engineering issue. Maybe it's not worth the technical challenge or the increase in complexity for the language designers. I'd be more sympathetic to this reason. Then again, Python and C++ manage it.