parameter binding
Consider this innocent method:
[list add: element] [List of: element, element -> list |
...code goes here...]
And now ruin its day:
list :: 1.
element :: 2.
What does this mean? We've now effectively locked the method signature to only one use case: 1 add: 2 which will fail because the type Integer is not a List of: element.
We have two options here, 1- the scope of a method signature ignores the scope defining it. Or 2- we roll with it and see what happens. Option 1 is the easiest one and probably makes the most sense. But we can delve in to some very funky stuff if we roll with it. So lets roll with it and see what happens.
[List of: element-class] [Class, Class -> Class |
{...List, elements: (Array of: element-class)}]
Did we just move class specialisation outside of the class definition without hurting our brains? ... maybe? that depends a little on your brain.
Let's rewind a little and look at how maps work:
my-map := {String, Integer | 'foo': 123, 'bar': 456}.
If we wanted to copy my-map but change one of its keys we can do the following:
my-second-map := {...my-map, 'foo': 789}.
The first and second map share the same key-value for 'bar' but differ with 'foo'.
When we define a map using keys that don't exist we're making a Structure used to define a Class, rather than making a regular kind of map. Think of it like a fixed-keys map.
This is the class structure for a List:
List ::
{allocator: MemoryAllocator = context memory-allocator,
elements: (Array of: Any)}.
So if we wanted to make a new version of List and change what elements was we could do this:
StringList ::
{...List,
elements: (Array of: String)}.
We make the assumption that if we create multiple classes with the same shape that the compiler will alias them together. You may want to make a distinct type that doesn't alias like that. For that, a single member class is what you want, eg:
MyDistinctStringList :: {list: (List of: String)}.
Now that we know we can make subtypes of classes we can rewind back to our method. We have a parameter called List which is declared outside that scope to be a class. It is therefore an object that exists and not a binding to be resolved later. Any time we call of: where the receiver is a List we will call our new method instead of a generic one.
Inside the method we create a subtype of List and return it. The parameters can be specific - ie: a particular kind of element-class only. In this example we're saying any class is fine (which in reality it is) so long as it is a Class and not Any kind of object.
There's one gotcha left. Here we're subtyping List and replacing what elements is. What if we wanted something that was a compile time variable? It's useful to be able to ask a list type what its element-class is.
For that lets reuse the # prefix on an element. It has no purpose now that we've removed specialisation syntax. Lets use # to indicate a generic parameter. This will allow us to make classes that must be specialised to be used, or we can provide a default value so that we don't have to write busy-code to make it work:
List ::
{#element-class: Any,
elements: (Array of: #element-class),
allocator: MemoryAllocator = context memory-allocator}.
[List of: element-class] [Class, Class -> Class |
{...List, #element-class: element-class}].
What else can we do with bound parameters like this? Any singleton or immediate can be used like this. An empty list could be used like this.
[numbers sum] [Array of: Integer -> Integer |
numbers[0] + numbers[1:] sum].
[{} sum] [Array of: Integer -> Integer | 0].
This is very functional like programming. There may not be a good reason to impose this on ourselves - but the class specialisation tells us its at least a good idea to see where this takes us.
This is more than a compile time test. It requires runtime tests. It's substituting a boolean test for a compiler generated hidden boolean test. I don't think I want that as part of STZ. If it can resolve it at compile time, great, but having hidden logic at runtime makes me feel uneasy for a low level language like this.
The door still exists and can be opened again later, nothing is closed for good. So I will let this last feature sit in the maybe bin for a while.