stz are generics real?

stz are generics real?
Photo by Jon Tyson / Unsplash

So far we've been using a stand-in syntax of $name to indicate an unknown type. This being borrowed from languages such as Go, Odin, Jai, etc. But if we have an interpreter helping us build our compilation unit is it really necessary?

Let's try and create the basis of an unordered list. The unordered list holds on a reference to an array of $T (for want of a better name right now), keeps track of how many elements are in the list (so we have free space on the end - this is our capacity size) and what memory allocator to use.

We would, currently, describe the class as:
list = { elements: &(array-of: $T), length: int, allocator: &memory-allocator }

When we use a list as an argument in a function we'd take &(list of: $T) but we've never described how this of: method would work.

Something interesting happens if we try to implement it:

[ list-of: element-class | class |
  subclass-of: abstract-list
    variables: {
      elements: &(array-of: element-class),
      length: int,
      allocator: &memory-allocator
  }
]

The method creates a new class and returns it based on the parameter to the method element-class which is of type class. Suddenly all the unknowns are known. We even now know to inherit from a class called abstract-list which would not be parametric. It's likely private.

That's awesome - but how do we describe the type generically when we're implementing a method, such as:
[ list add: element | &(list-of: $T), $T | ... ]

[ list add: element | &(list-of: (class-of: element), (class-of: element) |
  capacity == length then: [ grow ]
  elements[length] = element
  length += 1 ]

Instead of saying $T we get the class of the thing passed to us. The list element-class must match this. At the call site we would fail to match this method if we did something like this:

list = { list-of: person | new }
list add: 'A person name'

The list passed to it won't match the list returned for the first parameter, thus we have type safety.

There's one more piece of the story here – what if we don't care about the specifics of the types and we just want 'a list' ? Let's implement capacity:

[ list capacity | &(list-of: $T) -> int | elements length ]

We can remove the generic parameter because we know all lists, irrespective of their element-class, must have elements:

[ list elements | abstract-list -> array | subclass-responsibility ]
[ list capacity | abstract-list -> int | elements length ]

This code won't work with a abstract-list but it will work with a list-of: because elements does exist in the subclass as more than just a method. Its signature has changed though. Instead of returning array it will return an array-of: ... but that's the magic of inheritance. The capacity method calls elements not elements that returns an array.

In terms of compiler implementation, we'd copy all the methods from list in to the new subclass because this is a compiled language, it won't be changing at runtime. Any methods that end up being identical in implementation after compilation get merged together again.

If we add inheritance though we might need a way to call the super implementation when we override it. In Smalltalk there is a special variable called super for exactly this case.

In our case we accidentally already have a syntax for doing this. Remember we have receiverless methods so we can do things like opengl.attach-shader: – though technically the receiver there is the module. We're only defining methods as receiverless to default to the package as its receiver.

The weird thing is we could implement namespacing without the . syntax at all. After all opengl attach-shader: works just as well. Even core string works just fine instead of core.string. If it's just a message send then the compiler and language is simpler. That sounds ideal even if it does leave us without a syntax for calling super.

You can't simply cast the list-of: as a list because any subsequent calls cannot see the list-of: method overrides. There has to be a way of calling the super method while keeping the object class the same.

Previously we used :: to bind a type to a variable. Let's use it again since it's the outlier syntax right now. Let's implement a spurious example to try it out:

[ list capacity | &(list-of: class) -> int |
  abstract-list::capacity + 1 ]

So long as we can see the abstract-list::capacity implementation in our compilation scope we can call it. This is true for any method called from anywhere. The receiver here is the implicit first parameter, in this case the list.

The only other thing we might be interested in here is 'traits'. Traits are a way of grouping together types that conform to a specific kind of behaviour. We could group classes together in ways that aren't inheritance. That could be as simple as an array of classes that we check with (class-of: element) in: fancy-classes or we might make a method that checks certain implementation details, eg:

nothing = { {} | new }

[ length-trait: lengthable | class -> union: {class, nothing} |
  (lengthable implements: 'private-ength') else: [ ^nothing ]
  ^lengthable ]

[ list length | length-trait: list | list private-length ]

A contrived example but it'll probably be useful down the line. But it does complete our story on generics and parametric types. The answer is - we have them without needing to implement anything in the compiler at all.

On another note I'm starting to doubt the use of & everywhere. We might need to look at all our use cases and see if they make sense. list-ref-of: is just as easy to use and write and is clearer and doesn't require any special treatment in the language.

(an interesting note - if we know all this information at compile time as about what classes a variable is, we may not need the ability to access it at runtime - any of that information can be stored to be used at runtime as necessary. This means reflection and type information won't be a burden on the runtime size)