1. 15
  1.  

  2. 12

    Operator overloading is interesting to me in that it demonstrates how much the programming culture around a language can affect whether the language’s features turn out well in practice.

    No statistics to back this up, but I think a lot of people who recoil in horror at the prospect of operator overloading probably arrived at that opinion after working with C++ code. Operator overloading seems to be widely thought of as abused in idiomatic C++.

    But other languages have operator overloading too and their idioms have evolved to use it more sparingly. To take the article’s specific example: Kotlin on JVM overloads the + and * operators for the Java BigDecimal class. Idiomatic Kotlin code adds and multiplies BigDecimals using those operators just like the author wants to do. But I have yet to run across any Kotlin code that, say, overloads bit-shift operators to perform I/O operations. Nothing in the language prevents you from doing that kind of thing if you choose, but it’s not considered good style by the community.

    Of course, as a language designer you’re rolling the dice to some extent. You can’t know if your language’s community will take some new language feature and run with it in a horrible direction. My point is mostly just that it’s not a given that things like operator overloading will be commonly abused just because they can be.

    1. 5

      The answer to this might be as simple as whether you overload operators by name (e.g. the “inc” method) or by symbol (operator ++(..)). The former discourages you from changing the operation’s semantics.

      1. 2

        I think this is one of C++’s biggest issues.

        In my opinion, you should provide the language with information about how to do something — this is how you add two of this type together, this is how you move this type, this is how you copy this type, this is how you dereference this type — and the language should decide when to do those things. But instead, you tell the language what should happen when the + operator is used, that should happen when the -> operator is used, when the * operator is used, you provide the T(&&T) constructor and the T(const &T) constructor and the =(&&T) operator and the =(const &T) operator.

        Not only does this result in a lot of boilerplate (two ways to move, two ways to copy, two ways to add, two special ways to add 1, two ways to subtract, two ways to subtract 1, ….); it also encourages operator overloading abuse because you’re overloading operators and not semantics.

        Who said using “<<“ for writing to a stream is wrong? You’re just overloading the symbols “<<“; nothing is suggesting that “<<“ should be any kind of left shift. (Except that the precedence rules for “<<“ as a stream write operator are completely bonkers.)

        1. 3

          It’s worth noting that there are sometimes cases where you want to define ++ and not + (for example, ++ on an object representing a date would mean ‘the next date’, but you can’t sensibly add dates) or + but not ++ (matrices, vectors).

          1. 2

            I think that’s more a matter of wanting to define + on a Date only if the right hand side is an integer and not a date, ++ meaning +1. C++ does this kind of overloading very “well”.

      2. 3

        Operator overloading is one of those features that is not intrinsically evil, but which doesn’t compose well with some others. In particular, it really doesn’t play well with operator precedence. a + b * c as an arithmetic expression may make sense to execute as a + (b * c) because that’s how people are taught arithmetic, but what if a and b are strings, operator+ on string means concatenate, c is an integer and operator* on string and integer is repeat? Should "foo" + "bar" * 2 evaluate to "foobarfoobar" or "foobarbar"? The latter is consistent with arithmetic, but is probably surprising to a lot of readers who know the types and don’t think of the + and * as related.

        In Verona, we are supporting operator overloading (and any word or symbol can be an infix operator) but not precedence. Any sequence of the same operator is applied left to right. Any sequence of different operators is a parse error and requires explicit brackets.

        1. 2

          I don’t know for sure, but I suspect the opposite is true: language features determine the culture. There are different ways to implement operator overloading, and they lead to different results.

          One of the core design tenants of C++ is that user-defined types should do everything that a built in types do. Hence, it’s operator overloading supports everything. You can overload ,, ->, =, and implicit conversions.

          In Scala and Haskell, one of the goals is to be able to “write abstract math”, so there you can define your own custom operators (and even custom precedence, in Haskell).

          In Kotlin and (to a lesser extent) Python, operator overloading is very tame and scoped only to overload enough syntax to make BigInt and the like work.

          But I have yet to run across any Kotlin code that, say, overloads bit-shift operators to perform I/O operations.

          Counter-example: a bunch of pre-Compose UI frameworks which didn’t have access to compiler plugins overrode unary plus or call without parameters to mean “add the current thing to the enclosing UI container”.

          1. 1

            I think Rust got it right in this respect - have just a bunch of overloadable operators represented by traits covering the basic arithmetic operators that would help reduce boilerplate (as in the case of Java’s BigInteger and BigDecimal classes), but not unbridled overloading like in C++.

          2. 6

            One of the reasons Lisp works so wells with extensibility (including operator overloading) is that you always have the AST so you always know that the car position of the forms and procedures you’re calling is the… uh, unless you use continuations. Then RIP.

            (+ 13 14 (quadruple 4))
            ⇒ Hello, snan!
            

            Because earlier:

            (define (quadruple num)
              (* 4 num))
            
            (+ 28 (quadruple 4))
            ⇒ 44
            
            (string-append
             "Hello, "
             (list-ref
              '("there" "you" "me" "mom" "snan")
              (call-with-current-continuation
               (lambda (k)
                 (set! quadruple k)
                 0)))
             "!")
            ⇒ Hello, there!
            

            And now:

            (+ 13 14 (quadruple 1))
            ⇒ Hello, you!
            

            You’re not just overloading the quadruple operator, you’re paving over the entire context of the equation the operator is used in under three layers of fresh asphalt.

            Best language ever♥

            1. 3

              Patching over inadequacies of type system by spilling can of worms of operator overloading on it is never a great solution.

              1. 3

                I agree for most cases, but only most. If an operator and a function are fundamentally different just for the sake of the parser, that’s a loss of orthogonality (maybe that’s what you mean by inadequacies of the type system). In languages where an operator is a function, you can sum a list by passing + to map, which is good. However such a language may or may not let you define your own function with a symbol name and then use it as an infix operator. Swift does, but it’s a rarely used feature for two reasons.

                1. Compiling under type inference is faster with less overloading. Custom operators lure us to create expensive type inference operations in unexpected places: Inferring the type of x in let x = a + b + c + d + e is naively O(n^4) where n = the number of overloads of +. This is true for functions as well, but functions don’t often have 10+ overloads, and you don’t call as many in a statement as operators.

                2. For cases that aren’t built into the language or standard library, functions are clearer to the reader. They say what they do. This is sticky with us perhaps because of the language and libraries’ emphasis on clarity at the point of use.

                Operator overloading helped the standard library add + to new floating point types, but before best practices emerged we did play with it too often and get into trouble. These days we know to just prefer functions.

                But every once in a while, I’m glad operator overloading is there. If I were to rely on a numerics package, for example, in such a way that it’s really central to the program, I do want it to provide that custom +. I feel the same about DSL syntax support: I usually don’t want it because it has a cost and it’s less clear than plain functions. But when that’s the domain the program is about, as in SwiftUI, it’s worth it.

                1. 1

                  It was quite a revelation for me when I realized that typed functional languages don’t have operator overloading, they just allow you to define your own infix functions, and “overloading” is simply shadowing.

                  1. 2

                    Coming from languages like Haskell and similar… C++’s overloading seems similar to me. You define/shadow what << means for stream types, or w/e, which is why cout << hello.

                    1. 1

                      Haskell absolutely has operator overloading, or at least something morally equivalent to it.