stz method signatures

stz method signatures
Photo by Signature Pro / Unsplash

One of the most interesting ideas from Smalltalk was 'literate programming' which has become known also as keyword selectors. Instead of a single magic command followed by a bunch of arguments you place a keyword before each part of a method argument, eg:
aGraphicsContext displayLineFrom: start to: stop

This does get abused a lot. So many messages end up vacuously declaring something like on: a-thing for: another-thing. We want to avoid that kind of busy-work programming if we can.

Keyword selectors are both a blessing and a curse. When you're in the Smalltalk world they're wonderful. The moment you interface with the C world they're terrible.

We can solve these problems with our magic comma syntax and receiverless syntax. If we're allowed to import 'stuff' in to our current context then the following is possible:
fd := open: path, O_RDONLY

and alternate syntax for this might be:
fd := open: (path, O_RDONLY)

or perhaps:
fd := open: {path, O_RDONLY}

This forces us to think about how comma really works. If we can arbitrarily create a list of things without needing any kind of syntax around it, except perhaps a sub-expression (), then we have the ability to write near-C like calls while still also doing keyword syntax.

For now let's roll with it - an array can be made from any statements strung together by comma.

Okay, back to method signatures - an anonymous method is a block of code. Any block of code will do:
[ a, b | a x * b x + a y * b y ]

But if we want to name it we need to intersperce the keywords:
[ a dot-product: b | a x * b x + a y * b y ]

Yep. That's ugly. Let's look at a more traditional syntax where the statement body {} is directly after the signature:

a dot-product: b [
    a x * b x + a y * b y
]

Now we're starting to look like a real language. There's no assignment here. This isn't just "a statement of code" like we had hoped. But avoiding extra noise when writing code is important if anyone is ever to use it.

What happens if we add in the types though? does it still look okay? and where do they belong - inside the code block or inside the signature?
a: vec2 dot-product: b: vec2 → float [ ... ]
or perhaps:
a dot-product: b [ :vec2, :vec2 → float | ... ]
or may be cleaner still:
a dot-product: b [ vec2, vec2 → float | a x * b x + a y * b y ]

Now we're getting somewhere. It also allows us to not specify the types just like with a block of code. The code that executes between [ | produces a list of types instead of a map of name to type. It's also still very clean and the signature itself isn't filled with type information. Though may be that's a wrong-goal, may be it should be filled with type information. Let's borrow from Objective-C and try again:
(a: vec2) dot-product: (b: vec2) → float [ ... ]

May be? I'm torn. These decisions get harder and harder. What if we also put that back inside the code block?
[(a: vec2) dot-product: (b: vec2) → float | ... ]

We at least have a unified syntax, of sorts, but we're now returning a 'signature' inside the [ | area instead of a list of names to types. We're also saying "you can only use this block of code if you use this specific signature. What if we wanted to be a bit dynamic and let a code block be both anonymous and named?
foo := [ a, b | a x * b x + a y * b y ]
a dot-product: b (foo)

Not too long ago we were running code inside the compilation scope context. We were calling things like distinct: and array-of: – but now a dot-product: b would have issue because 'a' and 'b' are not defined in this scope and dot-product: doesn't exist yet. We don't want to run code. This does veer us back to the idea of putting the signature inside the code block.

And we know we want modules. So how do we export something?
pub: [ a dot-product: b | a x * b x + a y * b y ]

Almost solved. What if method signatures were a known syntax and pub: takes a type that is 'signature' ?
pub: (a dot-product: b) [ vec2, vec2 → float | a x * b x + a y * b y ]

In Smalltalk a symbolic string is denoted with the # keyword. eg: #dot-product:. We'd need some convenient syntax to describe the selector. What if we invented a new one? we already have → to specify the return value and that is a strong indicator that the list before it is part of a type-signature. We could do something as simple as adding another pipe to the code block:
pub: [ a dot-product: b | vec2, vec2 → float | a x * b x + a y * b y ]

Honestly, that's not half bad. It's nice and self contained and makes it clear the difference between anonymous and named. Since type information is optional we could also write this as:
pub: [ a dot-product: b | a x * b x + a y * b y ]

or perhaps a slightly longer form:

pub: [ a dot-product: b | vec2, vec2 -> float |
  a x * b x + a y * b y
]

Let's write some more code with this and see how it feels:
pub: [ a & b | ?, ? → ? | (u8 cast: a) && (u8 cast: b) ]

Okay, I see it now. ? is a terrible name for boolean. Let's be trendy and use 'bool' instead. ? can be used to denote an unknown type perhaps:
pub: [ a & b | bool, bool → bool | (u8 cast: a) bit-and: (u8 cast: b) ]
pub: [ a else: block | bool, ? → ? | a == false then: block ]

Our generics are going to need another special piece of syntax to be useful. Sticking a bunch of ? everywhere is not that helpful. The idiomatic way in trendy languages these days is to stick a character in front of a variable name. Perhaps a $ or a & or even a ?. Let's try again and be as type specific as possible:

pub: [ a else: block | bool, block-of: (→ $r) → union: $r, bool |
  a == false then: [ ^block evaluate ]
  ^a
]

I do wonder if it's worth adding a method for creating a union:
pub: [ a + b | a: class, b: class | union: a, b ]

Then the signature would look like:
pub: [ a else: block | bool, block-of: (→ $r) → bool + $r | ... ]

How do we specify an empty-array? We are consistently building lists of things with the humble comma. We could nominate an empty list as something like: (,) but that is a little too tee-hee. An empty statement of () might be enough.
pub: [ a else: block | bool, block-of: (()→ $r) → bool + $r | ... ]

It's important to at least specify that the else: block cannot take any parameters.

Another interesting aspect of the [ ... ] syntax is that simply existing is enough to.. exist. In Smalltalk a block that isn't sent a message is a literal that can be deleted at compile time. If we've included a signature in ours then it gets installed in to the current context. We're sending pub: to add it to the export list. Perhaps export: is a better name if we're also going to import things. Private stuff that is known only to the module therefore needs no introduction:
[ my-private-thing: a | thing | do-the-thing: thing ]

I would not begrudge people writing public API like this:

export:
[ a dot-product: b | vec2, vec2 -> float | a x * b x + a y * b y ]

Oh wait, we have a way to specify unknown types now. Let's try that again:

export:
[ a dot-product: b | vec: 2 of: $T, vec: 2 of: $T -> $T |
    a x * a x + b y * b y
]

It feels 'right' to me to make unknown values stand out like that in all capitals. But have we solved a problem or made a problem worse? If we're allowed to add unknowns in to method signatures then why not types? That would require us to invent a syntax for types instead of using message sends. Is that a good idea?

We already have the syntax for an array (a, b, c, d). We can extend that to also be the syntax of a map (a: b, c: d, e: f, g: h). This does get confusing as to whether this is a list of message sends or a list of key: value. We will need to disambiguate in a way that isn't painful to the developer.

The answer could be to use = but that will have flow on affects back to our original code block syntax, ie:
[ a = int, b = int → int | a + x ]

We could revisit the binding syntax of :: which is used for constants in other languages. (a:: b, c:: d). Things are getting weird aren't they. Perhaps lists are fine but it's maps that need their own syntax. Let's go with {} for now and see if we've made a terrible mistake later: {a: b, c: d, e: f}.

We probably haven't because if we borrow a page from our code block book we can specify the kind of map we're making using our pipe syntax. The same could be true of arrays.
{ map: string to: int | "test": 1, "this": 2, "out": 3 }
( int | a, b, c )

Great! If we don't specify types of a list we've made a tuple. If we don't specify the types of a map we've made a .. class?
tuple := a, b, c
class := { a: int, b: int, c: int }

Okay. What about the parametic generic stuff? does that work now?
[ vec: dimensions of: element-class | uint, class | { x: element-class, y: element-class } ]

Wait.. we didn't need our fancy $ syntax. What happened? We made a method and the method constructs a type for us, that's what happened. But if we weren't sure of the kind of thing we're getting for element-class we could write it as:
[ vec: dimensions of: $element-type | uint, $element-type | { x: $element-type, y: $element-type } ]

Is that useful? I'm not sure. It's certainly possible. It suggests there are things you've be passing in that aren't just classes. That might be true but it currently doesn't seem to be.