stz array and map literals
When we reviewed the syntax so far we ran in to a problem with arrays and maps. We don't have syntax for them.
What we have so far is the ability to make an object on the stack:{ the-class | messages to send to the class... }
If we stick an array type in there we aren't creating an array pre-built with things - we're sending messages to it, eg:{ array-of: string | add: 'Hello', add: 'World'; insert: ' ' before: 'World' }
The current way we make lists of things is with , and it should work for the scenario above: 'Hello', ' ', 'World'
. But the element-type of the array must be inferred. What if we wanted them to specifically be utf-16-string instead of unicode-string.
('Hello', ' ', 'World')
is equivalent. We could use our | operator again to specify a type for the array:( array-of: utf-16-string | 'Hello', ' ', 'World' )
This works nicely because until we hit the |, we're in a subexpression any way so the first piece of code is going to be called - we're suddenly saying we want to run it at compile time instead of runtime. After all this should work with variables too:( array-of: utf-16-string | a, b, c )
The type inferencing is still there allowing us to define a tuple type with:class-a, class-b, class-c
. With or without brackets. If we wanted to be specific about it we could state it clearly: ( class | string, number, boolean )
.
We no longer need to use { string, number, boolean } to define a tuple. The statement without sending any messages to it is like returning a new stack allocated instance of the type.
That makes me wonder about the :: syntax we previously used to bind a variable to a type. If we can create a person on the stack using the following code: jane = { person | name: 'Jane', height: 5 foot + 6 inches }
and we can create a pointer to a person with the following: jane = { &person | new, name: 'Jane', height: 5 foot + 6 inches }
then it stands to reason that if we send -no- messages to it we have bound its type:
jane = { person }
jane name: 'Jane'
jane height: 5 foot + 6 inches
jane = { &person }
jane new
jane name: 'Jane'
jane height: 5 foot + 6 inches
We can abandon :: to bind a type to an object. The use of :: exists solely for slicing arrays now. That solves that weird edge case.
Let's see if we can apply all this to maps too. Right now we are back to our instance creation syntax: { a: string, b: number, c: boolean }
which no longer makes sense. We want to do the equivalent of making a tuple but instead making a structure.
If we revisit arrays briefly and allow them to be sparsely defined then we end up with stuff like this: ( int | 0: 100, 2: 200, 4: 400 )
would create an array of 100, 0, 200, 0, 400. This only works in the context where you've defined a type. If you haven't defined a type we're sending messages to the implicit receiver, eg:
stdout print: (this: thing, that: thing, the-other: thing)
We're sending this: then that: followed by the-other: to the compilation scope context with a parameter of thing. But that doesn't actually make sense. The return value is.. which one? the last one? that's a bit arbitrary. We can disqualify this kind of thing based on that strangeness. If we flip the concept around we can see a slightly more sensible use-case of comma statements:
stdout print: (this: thing), print: (that: thing), print: (the-other: thing)
Now we know it must be a map if there's a ( ) brackets and at least one comma. The only exception here would be if the key is meant to be from a variable or an expression. For that we put the key in brackets:
a = 0
b = 2
c = 4
( int | (a): 100, (b): 200, (c): 400 )
This creates the same array as before: 100, 0, 200, 0, 400. Note that this only works because we specified it was an array of int. If we left the type off it would type-inference it to be a map.
Making a map literal: ( 'name': 'Jane', 'height': `5'6"` )
or with a type specified: ( string, int | 'no-error': 0, 'mild-error': 1, 'bad-error': 2 )
.
This does leave us with a completely unambiguous way of specifying a structure which is almost identical to where we started, but instead of {} we're using ():( name: string, height: (unit-of: metre) )
If we want to make a literal array or map with only one item we stick a comma on the end to make it unambiguous: ( 123, )
and ( 'foo': 'bar', )
and a structure type: ( name: string, )
as well as a tuple: ( string, )
.
This leaves the empty array and empty map as ambiguous though. What does () mean? I'm going to allow this to remain ambiguous. In this case () would mean an empty array with no way to determine its type. You can't get anything from it because it's empty. The only thing you can do with it is ask it how many elements it has - which is 0. You can't distinguish this from an empty map in any useful way.
This () concept is interesting. Because its contents are typeless it doesn't matter if its an array or a map. It doesn't matter what kind of type it is at all. You are providing no information what so ever. It might actually be an error.
If you're going to have an empty array or an empty map you likely need to specify the types at the very least because they cannot be inferred: ( int | )
and ( string, string | )
.
In summary:
code block:
[ ...code... ]
[ types | ...code... ]
closure:
[ captures | ...code... ]
[ captures | types | ...code... ]
method:
[ signature | types | ...code... ]
sub-expression:
( method-call )
array literal:
( a, )
( a, b, c, d, ... )
( type | )
( type | a, b, c, d, ... )
map literal:
( a: b, )
( a: b, c: d, e: f )
( key type, value type | )
( key type, value type | a: b, c: d, e: f, ... )
tuple:
( &person, )
( &person, string, number, boolean, ... )
structure:
( name: string, )
( name: string, number-of-arms: int, ... )
class definition:
class: (
name: string,
number-of-arms: int,
height: (unit-of: metre),
... )
object creation on stack:
jane = { person }
jane = { person | name: 'Jane' }
jane-ref = { &person }
jane-ref = { &person | new, name: 'Jane' }
And as an added bonus we didn't have to resort to < and > as another bracketing type. (They are notorious because < is less than and > is greater than and thus the compiler has to be extra smart about determining if you meant to make a bracket or a magnitude test)
Both of these are backed by data in the code segment made at compile time. You cannot change them. To make sure that cannot happen these two are not array and map but instead literal-array and literal-map.
All arrays are fixed size, but maps are meant to be dynamic. literal-map is fixed size but it'd be nice to have a fixed size map too that isn't dynamic. For that we have fixed-map.
Classes summary:
array -- an array of element with a fixed length
literal-array -- a compile time array of element with a fixed length
list -- a dynamic list of elements with no fixed length
map -- a dynamic map of key to value with no fixed length
fixed-map -- a map of key to value with a fixed length
literal-map -- a compile time map of key to value with a fixed length