another pass at blocks

another pass at blocks
Photo by Jim Wilson / Unsplash

I know I've painted this bike shed a few times but it dawned on me that it's iterated enough that we can swing back around to some Smalltalk syntax.

The original idea was to use [ signature | types | code ] and [ types | code ] to reflect the Smalltalk-80 block syntax [ parameters | code ]. I'd like to return to this notion with some small updates.

The first is that binding a variable to a type will remain variable: Type. We also want to keep the ability to name parameters without the type name, name types without the parameter name, and have default input/output parameters.

I also want to drop the ← syntax for returning a value. The name of the return variable should be enough to indicate you're returning.

We still need a way to separate the signature from the code. For that we use [signature] [types | code] to borrow the best bits from both. So let's look at all the combinations for methods:

// declare everything
[self my-method: arg] [self: MyClass, arg: String -> return: String |
  return `prefix ~[arg] suffix`]

// same as above but with implied return with last statement
[self my-method: arg] [:MyClass, :String -> :String |
  `prefix ~[arg] suffix`]

// declare parameter types, infer return type with last statement
[self my-method: arg] [:MyClass, :String | `prefix ~[arg] suffix`]

// declare parameter types, no return value
[self my-method: arg] [:MyClass, :String -> ø | `prefix ~[arg] suffix`]

// declare return type, infer parameters
[self my-method: arg] [-> return: String | return `prefix ~[arg] suffix`]
[self my-method: arg] [-> :String | `prefix ~[arg] suffix`]

// infer parameters and return type
[self my-method: arg] [`prefix ~[arg] suffix`]

And the same for blocks:

// declare everything
[a: Integer, b: Integer -> return: Integer | return a + b + 1]

// declare parameter names and types
[a: Integer, b: Integer -> return | return a + b + 1]
[a: Integer, b: Integer | a + b + 1]

// declare parameter names and return type
[a, b -> return: Integer | return a + b + 1]
[a, b -> :Integer | a + b + 1]

// declare parameter names and return name
[a, b -> return | return a + b + 1]

// declare parameter names
[a, b | a + b + 1]

// default input/output
[+ 1]

I'm pretty happy with these variations. They all build on each other nicely and they reflect each other. There isn't wasted syntax here either.

One thing that would be nice is declaring a parameter name to type to be used for all the methods in the scope. We could simply say any variable used in the outer scope is bound to the same type for methods in the inner scope, eg:

self: MyClass

[self my-method: arg] [arg: String | `prefix ~[arg] suffix`]
[self my-method: arg] [arg: Integer | `prefix ~[arg + 1] suffix`]

This would also works with captured variables in closure blocks as they are defined on the outer scope but aren't named in the signature. In that case they are 'passed' automatically for you.

One other thought - let's revert {Type} {code} and (Element-Type) (elements). The {} syntax is neat but the type can go back to being inside with {Type | code} and for lists (ElementType | elements).

One of the neat things about {} is you send messages to the default receiver which is unnamed. That means if you're initialising things from self you can without playing naming patty-cake:

self: MyClass
[self my-method: arg]
 [foo: Foo = {name: self name, arg: arg};
  foo compute-the-thing]

And finally for compatibility with Smalltalk-80 let's reintroduce the special ^ syntax for returning. This will state the return is at the outer most scope always.

[self my-method: arg]
 [self has-thing then: [^self thing];
  ^arg]

This does beg the question - do we swap ; for . and be really Smalltalk-80-like? Right now . has no meaning (but also ; has no meaning) and we do not include Smalltalk-80 cascades.

Speaking of cascades - we have the {} syntax for sending messages to a new instance of a type, why not allow that like the return syntax after a variable or statement to send messages to it?

person: Person = self make-person {name: 'Joe', age: 27};

Let's include that in the design for now and come back to it if it has issues. Why the lean toward Smalltalk-80 more? Well, it is called Smalltalk Zero, it makes sense to reflect that enough while also preserving the ideas and needs of Smalltalk Zero itself.