String concatenation, operator overloading
Posted: December 14th, 2009 | Author: Mars | Filed under: Design, Progress | Comments OffI’ve just added a concatenation operator, using the ampersand character. It is a simple bit of syntactic sugar:
foo = bar & baz
foo = bar.concatenate(baz)
The string type implements a Concatenate method, which returns a new string, as you’d expect. This operator is intended for sequences, generally; specific objects may implement specific optimized concatenations, but you should be able to concatenate any two sequences.
I’ve been intending to implement some kind of overloading for binary operators in general, but wanted to think about multimethods for a while first. I’ve decided not to go that way; multiple dispatch would simplify certain semantic problems at the expense of a much more complicated design, and my principle here is very much “build what you need and defer the rest”. So I expect to reimplement the other binops in the same style: the parser will take care of precedence, but the implementation is just an ordinary method call.
There is a tension in the design here. Objects should be simple, based on a single concept, and each method on the object should be fundamental, an indispensable tool for manipulating that concept. But method calls should be similarly simple: why should you have to know, when you want to concatenate one object with another, where the concatenation code is actually located? The logical operation is the concatenation; the implementation may be specific to the object, or it may apply generally to a wide range of objects, but you shouldn’t have to make that decision when you invoke it.
One solution might be some kind of extension-method system, as found in REALbasic and C#: utility libraries can declare methods which “extend” some existing type. A class’ built in methods take precedence, but if a class lacks a “foo” method, the compiler falls back to any applicable extension method named “foo”. This is particularly useful when combined with interfaces: you can declare methods which work for any instance of that interface, regardless of its implementation type.
Another, less elegant solution would be to define some method corresponding to each operator, located in the standard library, which calls the object’s method if present and falls back to some generic behavior otherwise. This would work for the binary operators, where the actual calling mechanism is hidden, but it wouldn’t help for named methods (what if you wanted to sort some list, for example, which didn’t define its own sort method?).
Well – as always I’m going to do the simplest thing first, and expand on it later if it becomes necessary. For now a simple method call will do the job, so that’s all I’m going to implement. I’ll revisit the issue later if it becomes necessary.