simple classes

simple classes
Photo by Markus Spiske / Unsplash

Sometimes you don't want to write out a full class description with types annotated. You don't care that there's inefficiencies in memory, that the type of the object needs to be stored in memory with its value. Sometimes you just want to smash out a class quickly.

Person :: {name, age, height}.

That would be nice. But right now in STZ that will create an enumeration. Let's try that again utilising our syntax as it is right now and see if we're okay with it:

Person :: {name, age, height: Any}.

It's now clear that we're making a class because we have key(s): value instead of just key. I don't mind this. It's not a big enough deal that we should go and change how enums are defined.

There is a cost to this. Any should be able to store any kind of class. Most things you want to store are pointer-sized, as they are a pointer to another object. But often we're dealing with 'fat' objects, like a List which is made up of an array, and length; and the array is made up of a pointer and a capacity. That's 24 bytes instead of 8.

Any would have to be as big as the biggest structure in your program. That could be disasterous. If you have one big class, say, something with a buffer in it of 128 bytes, our Person class becomes 384 bytes big when it should have been 24 bytes big (on a 64-bit platform).

One approach would be to make automatic subtypes of Person based on the type of thing you're storing in to it. Any becomes a generic type that turns in to a concrete type and the getters/setters keep the interface the same. If you happen to only store 64-bit integers in to those fields it will end up taking 24-bytes as you'd expect.

The benefit of this approach is you don't have to store the type information in memory at runtime. The downside is the subtypes could explode out wildly. It also means you cannot put Person in to an List because how big is each entry in the List? suddenly List has to create variants of itself too.

Another approach would be to have flexible sizing for these kinds of structures. In otherwords, it is not a regular structure it is a lookup with dynamic allocation. The object would always be on the heap managed by a handle on the stack.

The dynamic structure could also use the trick above to have a descriptor for each combination of types being used. It would be, essentially, a tuple. If this is sounding unwieldy to you, it is to me too.

A tuple is easy to make in STZ: String, Integer, String. Done. We can now access the bits of it like an array with mything[0], mything[1], mything[2], etc. The tricky part is recognising when to declare the tuple automatically in the compiler and what happens when someone writes something different in to the slot treating it like a truly dynamic object.

Person :: {MagicDynamicStructureClassThingy | name, age, height}.
bob: Person.
bob name: 'Foo'.
bob name: (ExternalString id: "Bob-Name").

The answer is that every slot is to another dynamic structure. It's a DynamicStructure :: List[DynamicStructure]. At some point we need to get some data though. That means it is a tagged union.

Object :: List[Reference[Object] / Reference[Any]]

Getting the value out of the list does require knowing the type it should be. It is clear, at this point, that the compiler needs special support to have a 'black hole' of a reference to 'anything'. In other words, dynamic runtime casting.

We need the ability to try to dereference it in to a particular type from a tagged union of 'anything'. That is what each member of an Object is. We can call this an ObjectSlot.

This would absolutely require compiler support. Is this even wanted? we'd suddenly have generic objects that can be anything at runtime - but also a huge mess in memory with heaps of small heap allocations and deallocations. This is excellent dynamic Smalltalk aka Smalltalk-80 and Smalltalk-ANSI but it is terrible Smalltalk Zero.

Let's humour the idea though and see what is current possible versus what we'd need to make it useful:

bob name then: [bobs-name: String | ].

That's possible, but it's not useful.

bobs-name: String = bob name.

That's not possible, but it's useful. To do anything with any of the slots in bob you have to dereference it. There is an implicit panic here if it's not a String. It really demonstrates what you shouldn't be doing in STZ.

I'm somewhat convinced now that this is the cost of having types. You cannot easily add this kind of behaviour in without adding a heap of complexity to the compiler.

You're paying down the road in your code having to dereference things back to String and Integer and anything else later because you wanted to write less at the beginning.

Person :: {first-name, last-name: String, age: Time, height: Length}.

It's not a lot of typing to have typing and now you never have to specify these types again when using a Person. You don't even have to specify them when making a method:

[person set-lastname: new-last-name and-first-name: new-firstname]
  [person: Person,
   new-last-name: person last-name,
   new-first-name: person first-name
   -> ΓΈ |
    first-name: new-first-name.
    last-name: new-last-name].

Why does this work? at compile time person is the type that is being passed in. We know that person is a Person so we can send last-name to Person to get the type of the last-name which will be a String.

My position is that dynamic Smalltalk ANSI exists and there are many awesome implementations of it. Both commercial and open source. They're great. There's no need for STZ to recreate that. It lives in a different field, it's a Systems language and it should embrace that.

On that note, this is a feature we won't likely ever see in STZ. Never say never of course, but it's well outside the scope of the initial design.