Skip to main content

Operator Overloading Considered Challenging

Posted by cayhorstmann on December 5, 2011 at 10:47 PM PST

Java has no operator overloading. I always thought that was a shame. For example, BigDecimal would be a lot more popular if you could write a * b instead of a.multiply(b).

Why doesn't Java have operator overloading? Well,  C++ has it, and people kept saying that it makes code hard to read. Actually, in C++, you can have a library class vector and write v[i], thanks to operator overloading. In Java, we have unsightly v.get(i) and v.set(i, newValue). Easier to read? I think not.

Of course, someone somewhere out there abused operator overloading in C++, doing something silly like overloading % for computing percentages. The horror. It's actually pretty hard to do much abuse in C++ because you can only overload the standard operators. Personally, I never ran into anything scary

In Scala, on the other hand, you can define any operators you like. If you want to check for five star hotels, you can define a predicate *****. Unicode is fair game too: fred ♥ wilma.

Scala detractors foam at the mouth when they see

(1 /: (2 to 10)) (_ * _) 

Actually, that's unfair. Let's write it without operators:

2.to(10).foldLeft(1)((x, y) => x * y)

It still looks like magic if you don't know foldLeft. And if you do, the operator version is simpler.

(BTW, this computes 1 * 2 * 3 * 4 * ... * 10.)

So, I firmly believed that operators are a good thing when used with restraint.

I still believe that, but I came to realize that restraint is harder than I thought.

Some fellow had kvetched how the Scala collections library has too many crazy operators. And I remembered some discomfort when I wrote the chapter on collections for “Scala for the Impatient”. I put together a table with all operators for adding or removing elements, and it did seem a bit of a mess. I pointed this out on the Scala mailing list, and Martin Odersky asked what I suggested to fix it.

Well, fixing inconsistencies happens to be my forte, so I went right at it.

For starters, we have the following:

coll :+ elem // Makes a new collection, appending elem after coll
elem +: coll // The same, but elem gets prepended

It's a nifty trick in Scala that an operator ending in a colon is right-associative, and elem +: coll is really the same as invoking the +: method on coll.

You can also insert elements in bulk:

coll ++ coll2
coll2 ++: coll

Did you notice the asymmetry? Why isn't it :++ for the first one? I pointed that out and was told that ++ is prettier and shorter. 

What if the collection is a set? Then there is no intrinsic ordering, so it seems silly to distinguish between appending and prepending. You just write

set + elem

It gets a bit confusing when elem happens be a string.

Set("Fred") + "Wilma" // Set("Fred", "Wilma")
Set(42) + "Wilma" // "Set(42)Wilma"
Set(42) ++ Set("Wilma") // Set(42, "Wilma")

In the second case, it doesn't form a Set[Any] with 42 and "Wilma", but it coerces Set(42) to a string and concatenates the two. Ouch.

So far, these operators return a new collection, leaving the original unchanged. That's the functional way, and it's often good. But sometimes you want to mutate a collection. For example,

buffer += elem
buffer ++= coll

Did you notice the inconsistency? To append without mutating, it's buffer :+ elem, so for consistency's sake, it should be

buffer :+= elem

I asked why it wasn't so and was told that += is prettier and shorter. Yes, it is, but actually there is a :+=, because Scala always synthesizes an op= from any operator. And it's subtly different from +=.

What about prepend-and-mutate? The operators must have a colon at the back, so they are +=: and ++=:. Why not =+: and =++:? Gentle reader, if at this point you lost the will to live, I hardly blame you. 

Just one more thing before I come to my conclusion.

For lists, the prepend operator is ::, because, well, it's always been so. For example, 1 :: 2 :: 3 :: Nil makes List(1, 2, 3). And we don't want to change it to +: because, well, it's never been that. And we don't want to replace +: with :: because then we lose the beautiful symmetry with :+, even though we don't care about that symmetry when it comes to :++ or :+=.

No, I couldn't come up with a fix that was consistent, pretty, and compatible with the past. But I learned something from the process.

  1. Don't mess with +. String concatenation has ruined it for the rest of us.
  2. When your operator starts looking like Morse code, give up. You don't have to have an operator for everything.
  3. Use asymmetric operators for asymmetric operations. :: for cons, or | for shell pipes, are bad role models.
  4. You have one chance. Operators are powerful stuff. Once people are used to :: or | or !=, they will refuse to switch.

Is Scala wrong to have operator overloading? No, on the contrary. Operators are incredibly useful. They are just really difficult to get right.

We know this from mathematics, where of course operators abound because they are so useful.  New operators get created all the time, and many of them sink into the obscurity that they richly deserve (such as Newton's fluxion notation). Nevertheless, some awful operators survive. Consider derivatives. Input: A function. Output: Another function.  We have two operators: f' and df/dx. The first is inadequate and the second is cumbersome. As an undergraduate, I had a textbook that bravely soldiered on with Dxf, which made a lot more sense, but it was too far from the mainstream.

In summary, operator overloading is neither a mistake nor a panacea. I want it and I want people to use it wisely. As I learned, that's harder than it appears.

Related Topics >>

Comments

Scala is lucky to have you guys contributing to it.

Scala is lucky to have you guys contributing to it.

Indeed Scala is lucky to get operator overloading support ...

Indeed Scala is lucky to get operator overloading support which was missing in Java from long time, this is first time I am hearing truth otherwise lot of things has been speculated on why Java doesn't support Operator overloading .

I think there is an error. You say that ...

I think there is an error. You say that

    Set("Fred") + "Wilma"

would return

    res0: String = Set(Fred)Wilma

But actually it works as expected:

    scala.collection.immutable.Set[String] = Set(Fred, Wilma)    

Concerning foldLeft: i think it is deeply troubling that the same sad example is brought up by "Java experts" for years already.
It isn't hard to prove that bad code can be written in any language, but a sane person would have neither used

    (1 /: (2 until 10)) (_ * _) 

nor

    2.until(10).foldLeft(1)((x, y) => x * y)

but just

    2 until 10 product

You are right about Set("Fred") + ...

You are right about Set("Fred") + "Wilma". I fixed the example. And I messed up with the other example too. It should have been 2 to 10.

And I agree with you that computing the factorial with a fold is silly, but that's what the Scala detractors trot out. product is the way to go.

 Well first you're making the second foldLeft example ...

Well first you're making the second foldLeft example more difficult than need be:

(2 until 10).foldLeft(1)(_ * _)

And I definitely think that's much more readable than the other one. If you use operators enough you'll be able to recongize them, if not they're just a bunch of symbols with no meaning whatsoever. The word foldLeft on the other end always will, you just have to learn the concept once and you'll be okay, and even when you don't know what a foldLeft is you can simply look it up, on the other hand good luck googling for /: (PS: had to scroll up to see how it was written exactly)

I think it's one of the things Java did right when they said that you're going to read code many more times than write it. So shorter and "nicer looking for the experts" doesn't cut it, it has to be as easy to read as possible for everybody.

Actually, I can never remember what the "left" in ...

Actually, I can never remember what the "left" in foldLeft is, so I have to ponder whether it's foldLeft or foldRight. But with /:, I see the pattern of the computation

 

 

      *
     / \
    *   4
   / \
  *   3
/ \
1   2

so I know I need /: and not :\. That only works because the operator is memorable. Had it been <|, it wouldn't have worked at all.

Which is my point. A good operator adds value, a bad one doesn't.

I&nbsp;think operator overloading will make things more ...

I think operator overloading will make things more complicated but again providing a proper interface for operator overloading is a plus.Java Unchecked Exceptions and Throws