stz traits vs inheritance
I posed the question - if we have the ability to do traits then should we not favour it over inheritance?
The answer is about simplicity of understanding the code and also writing it. Freedom of expression is great but freedom from other peoples excessive expression also has its merits. Let's consider the class quantity:
For the traits version, the signature for adding two quantities together is easy: a + b but that's where the simplicity ends. If you want to understand the types of a and b you have to really squint hard at it. It's not at all clear.
For the inheritance version everything is crystal clear and easy to understand. Is there any difference between them besides how many characters we had to type to define it? (and likewise how many we'd have to type for every other specialisable class)
Sadly, yes. Inheritance implicitly requires us to store the actual class type along side its value because it is union of all its subclasses. Instead of quantity being (for example) an s64 + u64 in memory it's suddenly a class-ref + s64 + u64. Possibly even worse depending on whether unit is a superclass too.
There is another alternative to both traits and inheritance though. We create the union of things that are number but we tell the compiler that this is a generic parameter and therefore calling the method requires expanding the generic parameters to their real parameters; instantiating a new version of the method if it doesn't already exist.
This is the equivalent of doing $T that we were doing before. The only difference is we're declaring $T as a type in its own right which means we get the best of both generics and traits all rolled in to one.
One way of making number is to declare it is a superclass of, say, u64; another way is for u64 to say it is a number. A third way would be to enumerate them like an enum - though this precludes extensions. Some argue that implicit lists are bad and they should be declared. For now I'm going to err on making the list open-ended.
Instead of saying a class inherits from another class we're going to say the class implements certain traits. This is effectively an interface. We will need a way to declare the list of things the trait must implement though.
(Just as an aside, while we're at it, if you're writing a method types list it must include a return type. That return type can be 'nothing' though which is denoted by the delightful null symbol ø, eg: int, int → ø)
I have certainly noticed that [ signature | types | code ] is the odd one out and that there isn't a way to specify a method signature outside of this structure. It has struck me as a little odd. In Smalltalk you can use # to do it, eg: #from:to:do:.
A signature doesn't make sense in stz without types though. It has to be more than a simple # symbol. I'm hesitant to do it but I would like to explore the idea of using <> to define a signature:
<a + b | quantity, quantity -> quantity> = [
{amount: a amount + b amount, unit: a unit + b unit} ]
Ugh. What have I done. This won't do. Let's try again:
Nope that won't do either. What about:
I like the uniformity that we're assigning a thing to a thing but you can't ever assign a signature to anything but code. You also can't put this thing inside of a subexpression: (a + b | quantity, quantity → quantity)
would try and make a list of whatever a + b is. This doesn't make sense at compile time nor in our brains.
Another way would be to allow signatures to be declared without any code forcing them to be abstract and uncallable. They wouldn't complain that they're not returning the intended return type:
quantity_add_signature = [ a + b | quantity, quantity -> quantity ]
Let's double check with the other forms of square bracket before we commit to this:
code block:
[ ...code... ]
[ types | ...code... ]
closure:
[ captures | ...code... ]
[ captures | types | ...code... ]
method:
[ signature | types | ...code... ]
method-signature:
[ signature | types ]
Given above in brackets I said you have to declare the return type for a method's types we cannot have ambiguity here because the types section will always include an arrow →.
We might better write that set of examples as:
code block:
[ ...code... ]
[ arg types -> return type | ...code... ]
closure:
[ captures | ...code... ]
[ captures | arg types -> return type | ...code... ]
method:
[ signature | arg types -> return type | ...code... ]
method-signature:
[ signature | arg types -> return type ]
Great! Now we have a way to specify a method signature without the body to go with it we can define trait requirements:
number-t = trait: (
[ a + b | number-t + number-t -> number-t ],
[ a - b | number-t - number-t -> number-t ],
[ a * b | number-t + number-t -> number-t ],
[ a / b | number-t + number-t -> number-t + number-error ],
)
signed-number-t = trait: number-t * (
[ -a | number-t -> signed-number-t ],
)
I thought it would be nice to add in a convention to know that we're not dealing with a class here but a trait. So for now I stuck a -t on the end.
Next we add the sprinkling of magic sauce to the class definition:
quantity = class: number-t * (amount: number-t, unit: unit,)
Now we can use number as a type and the compiler will auto-expand the generics for us. We also don't have to worry about $T being given a string instead of something that actually is a number.
We can also make distinctions between things that are sequenceable and things that are collections 'of stuff':
collection-t = trait: (
[ collection elements | &collection-t -> object ],
[ collection length | uint ]
)
sequenceable-t = trait: collection-t * (
[ collection[index] | &collection-t, uint -> ø ],
[ collection[index] = element | &collection-t, uint, object -> ø ]
)
I'm sure it'll be useful to break this down in to mutable and non-mutable variants too but now we have a picture of how we compose types together in to concrete classes.
How do we make a quantity on the stack? the unknown trait types have to be resolved. They will be resolved either by literals:my-height = { quantity | amount: 10, unit: foot }
Or by a generic method parameter expanding to known types. This works great until you want to bind a variable to a type without filling its values in:my-height = { quantity }
So close! if solve this one last bit we're set. This is where the angular bracket syntax in C++ steps in quantity<int> or Go, Jai, Odin with quantity(int). What is right for STZ though?
What we really want is to define a new class that has the trait of quantity and bind it to my-height. It would require the ability for type to override the type information in the original, not just add to it. The long form of that will be quite messy but here's how it would look:
my-height = { quantity * (amount: f64,) }
Wait a second. That doesn't look messy at all??! We're multiplying a trait with a structure to get a new structure where the amount field has been overloaded with actual type information.
How often will we need to do this? I'm not sure yet. This might be terrible syntax and we'll hate it really quickly. There might also be a big hole in this plan. Let's just accept it for now and see what happens...