Of course we simply don't have that problem in C or Objective-C: operators are built-in
parts of the language, and neither the C part nor the Objective part has a comparable
facility, which is a large part of the reason we don't have a useful number/magnitude
hierarchy in Objective-C and numeric/array libraries are't that popular: writing
[number1 multipliedBy:number2]
is just too painful.
Some recent articles and talks that dealt with operator overloading in Apple's new Swift language just heightened my confusion. But as is often the case, that heightened confusion seems to have been the last bit of resistance that pushed through an insight.
Anyway, here is an example from NSHipster Matt Thompson's excellent post on Swift Operators,
an operator for exponentiation wrapping the pow()
function:
This is introduced as "the arithmetic operator found in many programming languages, but missing in Swift [is **]". Here is an example of the difference:func ** (left: Double, right: Double) -> Double { return pow(left, right) }
How come this is seen as an improvement (and to me it does)? There are two candidates for what the difference might be: the fact that the operation is now written in infix notation and that it's using special characters. Do these two factors contribute evenly or is one more important than the other. Let's look at the same example in Smalltalk syntax, first with a normal keyword message and then with a binary message (Smalltalk usespow( left, right ) left ** right pow( 2, 3 ) 2 ** 3
raisedTo:
, but let's stick
with pow:
here to make the comparison similar):
To my eyes at least, the binary-message version is no improvement over the keyword message, in fact it seems somewhat worse to me. So the attractiveness of infix notation appears to be a strong candidate for why operator overloading is desirable. Of course, having to use operator overloading to get infix notation is problematic, because special characters generally do not convey the meaning of the operation nearly as well as names, conventional arithmetic aside.left pow: right. left ** right. 2 pow: 3. 2 ** 3.
Note that dot notation for message sends/method calls does not really seem to have the same effect, even though it could technically also be considered an infix notation:
There is more anecdotal evidence. In Chris Eidhof's talk on functional swift, scrub to around the 10 minute mark. There you'll find the following code with some nested and curried function calls:left.pow( right) left ** right 2.pow( 3 ) 2 ** 3
"This does not look to nice [..] it gets a bit unreadable, it's hard to see what's going on" is the quote.let result = colorOverlay( overlayColor)(blur(blurRadius)(image))
Having a special compose function doesn't actually make it betterlet result = colorOverlay( overlayColor)(blur(blurRadius)(image))
Infix to the rescue! Using thelet myFilter = composeFilters(blur(blurRadius),colorOverlay(overlayColor)) let result = myFilter(image)
|>
operator:
Chris is very fair-minded about this, he mentions that due to the special characters involved, you can't really infer what |> means from looking at the code, you have to know, and having many of these sorts of operators makes code effectively incomprehensible. Or as one twitter use put it:let myFilter = blur(blurRadius) |> colorOverlay(overlayColor) let result = myFilter(image)
Every time I pick up Scala I think I'm being trolled. How many different possible meanings of _=>.+->_ can one language have??
— kellan (@kellan) September 13, 2014
Like most things
in engineering, it's a trade-off, though my guess is the trade-off would shift if we had
infix without requiring non-sensical characters.Built in
I do believe that there is another factor involved, one that is more psychologically subtle having to do with the idea of language as a (pre-defined) thing vs. a mechanism for building your own abstractions that I mentioned in my previous post on Swift performance.In that post, I mentioned BASIC as the primary example of the former, a language as a collection of built-in features, with C and Pascal as (early) examples of the latter, languages as generic mechanisms for building your own features. However, those latter languages don't treat all constructs equally. Specifically, all the operators are built-in, not user-definable over -overridable. They also correspond closely to those operations that are built into the underlying hardware and map to single instructions in assembly language. In short: even in languages with a strong "user-defined" component, there is a hard line between "user-defined" and "built-in", and that line just happens to map almost 1:1 to the operator/function boundary.
Hackers don't like boundaries. Or rather: they love boundaries, the overcoming of. I'd say that overloaded operators are particularly attractive (to hacker mentalities, but that's probably most of us) in languages where this boundary between user-defined and built-in stuff exists, and therefore those overloaded operators let you cross that boundary and do things normally reserved for language implementors.
If you think this idea is too crazy, listen to John Siracusa, Guy English and Rene Ritchie discussing Swift language features and operator overloading on Debug Podcast Number 49, Siracusa Round 2, starting at 45:45. I've transcribed a bit below, but I really recommend you listen to the podcast, it's very good:
- 45:45 Swift is a damning comment on C++ [benefits without the craziness]
- 46:06 You can't do what Swift did [putting basic types in the standard library] without operator overloading. [That's actually not true, because in Swift the operators are just syntax -> but it is exactly the idea I talked about earlier]
- 47:50 If you're going to add something like regular expressions to the language ... they should have some operators of their own. That's a perfect opportunity for operator overloading
- 48:07 If you're going to add features to the language, like regular expressions or so [..] there is well-established syntax for this from other languages.
- 48:40 ...or range operators. Lots of languages have range operators these days. Really it's just a function call with two different operands. [..] You're not trying to be clever All you're trying to do is make it natural to use features that exist in many of other languages. The thing about Swift is you don't have to add syntax to the language to do it. Because it's so malleable. If you're not adding a feature, like I'm adding regular expressions to the language. If you're not doing that, don't try to get clever. Consider the features as existing for the benefit of the expansion of the language, so that future features look natural in it and not bolted on even though technically everything is in a library. Don't think of it as in my end user code I'm going to come up with symbols that combine my types in novel ways, because what are you even doing there?
- 50:17 if you have a language like this, you need new syntax and new behavior to make it feel natural. [new behavior strings array] and it has the whole struct thing. The basics of the language, the most basic things you can do, have to be different, look different and behave different for a modern language.
- 51:52 "using operator overloading to add features to the language" [again, not actually true]
In fact, going back to Matt Thompson's article from above, it is kind of odd that he talks about exponentiation operator as missing from the language, when if fact the operation is available in the language. So if the operation crosses the boundary from function to operator, then and only then does it become part of the language.
In Smalltalk, on the other hand, the boundary has disappeared from view. It still exists in the form of primitives, but those are well hidden all over the class hierarchy and not something that is visible to the developer. So in addition to having infix notation available for named operations, Smalltalk doesn't have the notion of something being "part of the language" rather than "just the library" just because it uses non-sensical characters. Everything is part of the library, the library is the language and you can use names or special characters as appropriate, not because of asymmetries in the language.
And that's why operator overloading is a a thing even in languages like Swift, whereas it is a non-event in Smalltalk.
Haskell lets us investigate the case where we can have infix operators without having to resort to special characters, and I've found that there are times when I prefer it. As a concrete example, I recently wrote a parser for a limited grammar. Imagine the "union grammar", which is matched when either of its arguments is matched, its constructor is:
ReplyDeleteUnion :: Grammar -> Grammar -> Grammar
Given two grammars g1, g2, I can either write:
u = Union g1 g2
or:
u = g1 `Union` g2
and I tended to prefer the infix variety.
Thanks Graham, that's a really interesting data point!
ReplyDeleteMakes me wonder if it would make sense to have prefix syntax as an option in a Smalltalk-like language, for example for blocks, whose parameters aren't named.