forms of [] and the ~
I added the ability to declare methods using signature ~ types [ code ] recently and it hasn't sat well with me since. The short hand for doing inline methods remained [ signature | types | code] and the way you write blocks is either [ types | code ] or [ code ].
I'm going to call a mulligan on ~ and let's re-explore the simplicity of square brackets for everything code (except object creation which still uses {}). But I'm also going to add in an alternate syntax to the pipe and we will see if it looks any good.
Let's reset expectations first:
[_ backup | module -> ø |
files := '/important-data' directory files
... files close
files ~> [copy-to: '/backup'] ~> ø]
Now let's introduce some new syntax. Instead of [ signature | types | code ] and [ types | code ] and [ code ] let's break it up in to parallel blocks. Three is a method, two is a block, one is a block with default argument and return:
[_ backup] [module -> ø]
[ files := '/important-data' directory files
... files close
files ~> [copy-to: '/backup'] ~> ø]
What this does is free up | to be part of the language rather than the syntax. It's a small thing to do but it creates a simpler parser too. The less magical syntax the better.
From the parsers perspective it is three blocks of code; but the compiler recognises that the 3/2/1 versions interprete the use-case of that code differently. In the case of a method the first block is a template of how to use the code that follows (ie: the method signature). The second is to be run at compile time to get a set of types to assign to the signature and the third is the code to be compiled for runtime.
It also reminds me of LEGO. You are plugging the blocks together to form something greater. I'd love it if that analogy could be transported further in to the syntax for iterating. Speaking of iterating, let's ditch ~> and go with the more traditional pipe | as it is quite well known in the programming world.
[_ print-people: people] [module, &people] [people | sort | print | ø]
We suddenly get this bizarre situation where a Smalltalk like language's code section looks a lot like a shell script 😀 which I don't entirely hate. The core principle of Smalltalk is messages and pipes are just another message.
I'm feeling adventurous so let's revisit the idea of capitalisation. What if we capitalised types? just how noisy does our code look then?
[_ print-people: people] [Module, &Person] [people | sort | print | ø]
I admit it's not much different. It's probably because the types are so well separated from the code and signature that it doesn't matter too much. It does look funky when you mix them with code though:
files: (&Array of: File) = '/important-data' directory files
Huh, may be it's not so bad after all. Gotta pain that bike shed a little bit more of course.
Is it better to have an end-of-line syntax? it makes parsing a great deal easier as you don't have to guess when something carries over to the next line or not. A line ends with a . in Smalltalk-80. Lines often end in ; in other languages... or in a newline (as seems to be the new fashion).
The . is part of a "sentence" ideal in Smalltalk. That ideal is that code should look like an English sentence: hand pick-up: food. But command sentences are a strange thing in English. They're kind of rare written but common in speech. When we're coding we're writing in future tense, always. It is implied. That sets it apart from English written and spoken.
We're not currently using . for anything else, nor ; for that matter. Let's be flexible and allow either and see what shakes out of it.
[_ backup] [module -> ø]
[ files := '/important-data' directory files ... files close.
files | [copy-to: '/backup'] | ø ].
backup.
[_ backup] [module -> ø]
[ files := '/important-data' directory files ... files close;
files | [copy-to: '/backup'] | ø ];
backup;
Is it heresy of me to say I kinda like the semi-colon more than the full stop? A like no end-of-line delimiter the best but I also like my sanity. A concise language syntax that leaves no ambiguity is a better one. I'm also trying to reach out to a broader audience than Smalltalk so ... for now, let's embrace the semi-colon;
If we're removing | from the syntax of the language then () and {} will also need to be updated. Let's follow the same pattern, multiple times()'s and {}'s, eg:
(uint | 1, 2, 3)
becomes:
(uint) (1, 2, 3)
{Person | name: 'Jane', height: 5 foot + 6 inches}
becomes:
{Person} {name: 'Jane', height: 5 foot + 6 inches}
Another corner case to cover is a method that has inferred types versus a method that has no argument or return type:
[_ inferred-method] [] [...do stuff...]
[_ no-args-or-return-method] [->] [...do stuff...]
Of course the inferred method might also have no args or return but that would be the job of the type inference to figure out. It does mean that these two following syntaxes are equivalent:
my-block := [...do stuff...]
my-block := [][...do stuff...]
I'm okay with that redundancy. It's an important syntax otherwise declaring an array of integers at compile time would always look weird: ()(1, 2, 3).
And finally, how do you declare a block-type? It's a little bit weird but I suppose if we're telling the compiler to infer the types but we give it no code then it must come to either of the following conclusions: a) this is an empty method, or b) this is a block-type. An empty method/block is perfectly reasonable so we need some way to state that this is a type and not a method/block. Is it enough that the block is in the type-section? Possibly...
my-block-or-method: [a: uint, b: uint] = [a + b + 1]
You can pass a method where a block would be accepted.
Then there's one final piece of the puzzle - method signatures for declaring interfaces. Actually there's nothing special to do here - we defined them as being full methods that can actually run to fill in the missing type information for the specialisation itself. Recall:
Array
(#specialise: [-> Class] [element-class = Any]
#specialise: [of: element-class] [Any -> Class] []
#specialise: [of: element-class length: length] [Any, uint -> Class] []
#traits: (Iterable of: element-class, Orderable of: element-class,)
public length: uint
private origin: (MemoryAddress of: ElementClass))
Capitalising the types does leave is in the sticky situation of uint and int, etc. The "primitive" types don't lend themselves well to capitalisation. We could attempt to embrace it by using a single letter and a number instead: U8
and S8
but then how do you specify the platform/language default? just U
and S
?
Just how weird would it be for the language to declare the following types: U S F B
and their bit-sized counterparts: U8 U16 U32 U64 U128 U256 U512
, S8 S16 S32 S64 S128 S256 S512
, F16 F32 F64
. It's certainly concise. But is B just a little too weird?
Person (name: String, age: U8, has-drivers-license: B)
We could also consider being super literate with our type names. Signed would be the default and Unsigned the special case:
Person (name: String, age: Integer, has-drivers-license: Boolean)
There's a wisdom to writing it out as you intend it. That's a well earned Smalltalk wisdom.
Of course we don't really want an integer age. We want a time period.
Person (name: String, age: Time, has-drivers-license: Boolean)
Time as opposed to Timestamp and Timespan. Time is a SI base unit measure, while seconds is a base unit.
bob := {Person} {name: 'Bob', age: 24 years, has-drivers-license: true}
That's enough bike shed painting for now.