stz object instantiation
There's two kinds of object instantiation we care about - on the stack and on the heap. On the heap is an act of memory allocation using a memory allocator, while on the stack is a core concept in the language.
Currently we have no syntax for declaring temporaries ahead of time. That's deliberate. I've always disliked languages doing that to be. It was the primary reason back in ye olde days that I learnt C++ over C just so I could lazily declare my temporaries.
Ways in which we know the type: [ a: person | ... ] when we get in to the code block we have space on the stack for a person. Way back at the start we looked at the := syntax used in Jai and Odin. What if we took that seriously?
[ my-method |
jane: person
initialize-person: jane
]
Well here we have a problem. Because jane: person is indistinguishable from any other message send. May be we need a keyword, like var, or let, or const. And if we do that we're committing ourselves to English (sort of, keywords transcend and become their own language). We wouldn't need := for assignment anymore. = would be enough.
var jane: person β may be
let jane: person β may be
\jane: person β ew
jane :: person β not too bad
jane.person β no good, that looks too much like a namespace resolution
The type inferencer often means we don't need to specify the class anyway, but let's say we wanted to:
jane :: person = people find-person-named: 'Jane'
Actually. I can live with that.
Alright. We're able to bind a type to a variable name. Does that mean we should use the same syntax in the code block signatures?
[ a dot-product: b | a :: vec2-of: $T, b :: vec2-of: $T β $T | ... ]
It looks a little noisy. Given that the type declaration section of the signature is effectively inside of a map, do we even need to specify it there? are we worried about how temporary variables have their own special syntax?
Temporary variables can exist without binding them to a type. They will get a type just by using them - or throw a compilation error if they are too ambiguous. I'm going sit on :: for now and see if it becomes useful anywhere else.
Back to making objects. It sure would be nice if we could initialize their state in a simple and convenient manner. If we give it a list of statements then just like the cascade switch we could initialize an object.
{ person | name: 'Jane', height: 5 foot + 6 inches }
The only question here is how do we differentiate between making a map with keys and values; and making a person by doing message sends? Thankfully this is a non-problem because it doesn't look the same. a map of strings to values means it's not a tuple.
It's consistent. And the keys need to be quoted to actually be strings. In fact: { map-of: string to: string | a: b, c: d } is really interesting because now we're sending messages to the map object we've made. We might need a short hand to specify a key as code though...
{ map-of: string to: string | (some-code): "a string" }
Everything before the | is still just code being executed. We can put whatever we want on the stack dynamically. But back at the top we said we also want to be able to make things on the heap with an allocator. We can certainly 'make' a type in the heap and assign it to a temporary:
my-person = allocator new: person
But we've lost all the cool object initialization stuff. "Things on heaps" does tend to be type information though. What if it's that simple?
{ reference-of: person | name: 'Jane', height: 5 foot + 6 inches }
This only works if reference acts as a proxy to person. I actually like that idea but it does mean we will need to invent some kind of proxy solution; whether that's doesNotUnderstand: or something else we will have to decide in a later blog post.
Of note are languages like Odin where ^person and person both let you treat them both the same. If you want to 'de-pointer' something you send ^ to it. We'd need some way to de-reference the reference. The tricky thing here is if reference acts as a proxy then we cannot implement it as a message sent to a reference, ie:
[ a dereference | reference-of: $T | ... ]
person = person-ref dereference
Instead it would have to be a utility much like reference-of: gives you the type, we'd have something like:
[ mem dereference: a | module, reference-of: $T | ... ]
person = mem dereference: person-ref
(We're going to have to figure out modules real soon...)
Okay so we've made the type be a reference but where does it allocate memory? The stuff between { | happens at compile time so we cannot allocate memory on the heap there. It'll go poof when the compilation finishes.
We could send a message to the reference to tell it to allocate and/or deallocate. This does mean 'person' can never respond to those messages. We could make the proxy logic let the 'person' override the message if they happen to implement them. That would let you do fancy initialization logic... so may be worth while, may be not. We haven't even considered what inheritance might look like or if we even need it in stz.
{ reference-of: person | new, name: 'Jane', height: 5 foot + 6 inches }
Given this approach we can revisit dereferencing and make it a message on the reference type too. Given it's a rare thing to pull something off the heap in to the stack when you can proxy-use it as if it were on the stack, it can be a long message name:
my-person = my-person-ref dereference
my-person-ref free
It does make me wonder about the initialization pattern. If we have defaults we want on an object there's little reason not to make an initialization method:
[ self init | person |
name = 'Unnamed'
height = 0 metres
]
[ self new | reference-of: $T -> self | mem new: default-allocator ]
[ self new: memory-allocator | reference-of: $T, heap-allocator -> self |
allocator = memory-allocator
address = memory-allocator allocate: $T
init
]
[ self free | reference-of: $T | allocator free: address ]
initialized-person = { reference-of: person | new }
I just introduced the idea that the variable names in the signature can be used in the types signature too as a short-hand for 'this and this only' - it's not the type of self in those examples but self itself. There are no instantiations of self, unlike a type.
The only problem with this code is it forces $T to understand init. What if we didn't want that? perhaps we're better off not being so surprising and instead write:
initialized-person = { reference-of: person | new init }
That is clearer but it stops the very fancy Objective-C thing of returning you a different type than what you asked for during init. That, I suspect, is actually an anti-pattern. Surprises suck. If you wanted a computed-type you can use type inferencing and a message send at either compile time or run time.
Reference is almost complete. If we can specify a way to pass messages through to its insides then it really would be complete. May be that's a use case for importing. Something to explore in the next blog post where we'll hopefully also look at modules and module importing.