default parameters

default parameters
Photo by Steve Johnson / Unsplash

Now that methods and blocks both use the same syntax for describing the arguments, it dawned on me it's trivial to add in default parameters.

my-block := [ a: int = 1, b: int = 2 -> r: int = 1000 |
  a > 0 then: [ r <- a + b ] ]

my-block evaluate: (4, 4) // answer 8
my-block evaluate: (,) // answer 3
my-block evaluate: (0, 5) // answer 1000
my-block evaluate: (10,) // answer 12

It's also possible to pass parameters to a block using their names:

my-block evaluate: (a: 20, b: 30) // answer 50

The same is true of methods, mostly, where the signature is a set of a keywords. You cannot default in such a way that the signature ends up looking like another existing method. That would be a compilation error:

a add-with: b | a: int = 1, b: int = 2 -> r: int = 1000 [
  a > 0 then: [ r <- a + b ] ]

There is no way to break this down. You can only default the 3rd and on parameters. The return value can always have a default.

a add-with: b and: c and: d
| a: int, b: int, c: int = 0, d: int = 0 -> r: int = 1000 [
  a > 0 then: [ r <- a + b + c + d ] ]

1 add-with: 2                // 3
1 add-with: 2 and: 3         // 6
1 add-with: 2 and: 3 and: 4  // 10
0 add-with: 5                // 1000
gc draw-line-from: start to: stop color: color
| start: (vec2 of: integer), stop: (vec2 of: integer),
  color: rgba32 = gc current-color
  -> ø [
  ... ]

This is a little trickier though. Anything that is written inside the type section runs at compile time so that (vec2 of: integer) is resolved at compile time. Therefore gc current-color would also run at compile time. We want it to run at runtime if the parameter is excluded.

To do this we need some way to indicate that it's a runtime value. This would be true of even trivial things like:

my-block := [ a: int = 1, b: int = 2 -> r: int = a + b | ]

(for chuckles, specify the whole method as the return value default)

The simplest solution would be to put it in a block:

gc draw-line-from: start to: stop color: color
 | start: (vec2 of: integer), stop: (vec2 of: integer),
   color: rgba32 = [gc current-color]
   -> ø [
  ... ]

The problem with this is the compiler will create a block that can run gc current-color at compile time and pass that as a parameter. But gc is going to be bound to the type of the variable gc.

There is only one real solution here. Using the assignment syntax inside the type area has to do something very fancy - it has to divide the compile-time from the run-time:

variable-name: compile-time-statement-resulting-in-type = runtime-default

This isn't as tricky as it might seem at first. The parser doesn't run anything or even resolve anything specifically. Once the type section is found and parsed in to a tree it then decides which bits run when.

Normally you don't have to think about "this runs at compile time" because it looks like a normal type definition anyway. But if you know, you know, and you can do very fancy things if you wanted to.

And in reflection, any use of var: type = value works exactly that way. The type is always 'one layer deeper' than the eventual execution. To get a little headachy about this, at compile time the type comes from the built-in and already resolved types.

There's no reason we can't do the same thing for classes too:

rgba32 := (r: uint8, g: uint8, b: uint8, a: uint = 255)

But for now we've just made a decent api for drawing 2d lines:

// default parameter color = black
gc draw-line-from: 20, 35 to: 10, 25

// default member a = 255
red := (rgba32 | r: 255, g: 0, b: 0)
gc draw-line-from: 10, 25 to: 20, 35 color: red

I love how once all the fancy syntax is buttoned away actual code in the language looks remarkably Smalltalk-80 like. Yet it's all low level programming still. This is the desired goal.