stz parameters and slices

stz parameters and slices
Photo by Henry Be / Unsplash

In a previous post I wrote this piece of code:

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

The signature has three sections:

  1. the pattern that will be used when calling this method
  2. the types that must match the parameters
  3. the code block to run

I'm satisfied with part 1. It's very clear and concise what it is and how to use it. You can literally copy and paste it in to your own code and if you have a list and element variable it'll work as-is.

Part 3 is the code itself. That's fairly straightforward and I don't think it's worth touching on right now.

But Part 2 has a problem. The parameter element - what is it? At runtime it might be a string or a person or a database-row. But at compilation time - what is it?

I believe there's only one thing it can be. The class of whatever element is. The only other information that could be useful would be the calling site and its name at the calling site (very useful for compilation errors). It might not have a name though.

If it's the class or a class-like-thing that includes that other meta data then the code changes to look like this:

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

This makes me happy. There's no noise. In fact the only special thing in there is the &. Let's remove it to make my brain happier still:

[ list add: element | list-ref-of: element, element |
  capacity == length then: [ grow ]
  elements[length] = element
  length += 1 ]

It's easier to look at but is it better? What if we keep the & but remove the need for the brackets. There'd therefore be two cases of & here:

  1. &class-name – a reference to the class
  2. &method-call – a reference to whatever class the method returns
[ list add: element | &list-of: element, element |
  capacity == length then: [ grow ]
  elements[length] = element
  length += 1 ]

Yeah. & is too darn useful. We'll keep it. But we might need to expand on it because slices are extremely useful too. I love slices but I think the name is terrible. We have references, we have memory addresses, and we have lists. An array points to any place in memory and has a length. If we want to 'slice' in to that array safely we could add some API, eg:

files-list slice-from: 4 to: 8

Other languages that support slices tend to provide a syntax for it. I've also hinted at using the C-like [index] syntax and [index] = syntax for accessing and modifying elements in a list. I've spent enough time writing at: and at:put: in Smalltalk. It's not one of the best parts of the language.

The usual slicing syntax is [start:stop] or [start:] or [:stop] and for doing start to: length you can combine them [start:][:length]. I'd go a step further there and add [start::length] to avoid the bonus brackets. May be :: isn't the best choice but we'll go with it for now.

files-list[4:8]
files-list[4::5]

Note that start and stop are inclusive. These two statements are equivalent. What's inside the brackets is effectively a range object. We could be tempted to allow this syntax anywhere to make a range.

[0::10] do: [ i | stdout print: i ]

It's shorter than writing { range | from: 0 length: 10 }. It does overload the meaning of [ in the syntax though. variable[ is easy to understand but [ on its own has, until now, always meant the declaration of a method. A method declaration must have at least two parts [ signature | code ] so we might be okay when we see [ firstthing : secondthing ] and there's no |.

[:stop] and [::length] are also very convenient by themselves. Looping N times now looks like this:

[::io events length] do: [ i | ... ]

That's not terribly useful because you can more conveniently iterate the events themselves. We'll have to wait and see if [:] and [::] become useful separate from variable[:] and variable[::].

This does effectively give us two ways to pass things as pointers. & is a short hand for a reference which is an array of size 1. [:] and [::] is a short hand for array of pointer and length.

We looked at the idea of us using traits to define types. One such example here is "either an array or a list" because so many methods there do the same thing. The alternative to traits is for array and list to both inherit from "something sequenceable". Right now we've introduced inheritance syntax. If we needed diverging inheritance from multiple parents then traits would give us that.

It's worth noting that traits don't need to be provided by the language as a facility in any way or shape or form. They already just are. The question is more of library design - is it better to embrace them immediately or to lean on inheritance.

Is it better for list-of: to inherit from list or for capacity to be implemented using the trait capacity-trait:. If we embrace the existence of traits right now then we probably don't need inheritance at all.

Smalltalk inheritance allows you to inherit shape (variables). I've always considered this to be a mistake. The best class hierarchies only add shape to the leaf nodes. In the rare cases where you need shape you'd be able to do something like the following anyway:

fancy-class = { foo: int, bar: int }
composed-class = { baz: int } * fancy-class

I'm leaning in to composable types here. Creating a union of types should be as simple as first-class + second-class so combining two classes together might as well use multiplication.