Control structures are functions

do, unless, retry and test are ordinary functions with a closure or lazy parameter, so writing your own control structure is nothing more than writing a function that takes one and calling it with a trailing closure.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

if, for, while and match are the only control flow built into the grammar, because they are the only forms that bind a name or jump. Everything that reads like a keyword beyond those four - do, unless, retry, test - is an ordinary function whose last parameter happens to be a closure.

Example#

fn twice(body: () => Void) {
  body()
  body()
}

twice { print "hi" }

Syntax#

fn <name>(..., body: () => <Type>) { ... }     a control structure: a function with a closure parameter
<name> <argument> { ... }                      called with a trailing closure, reading like a keyword

Rules#

  1. A control structure is a function whose closure or lazy parameter is what makes it read like syntax. unless(condition: Bool, body: () => Void), called as unless ready() { print "waiting" }, is not different from any other function call - it is a command call with a trailing closure.

  2. Writing your own is writing a function and calling it. A function whose last parameter is () => Value becomes a block-taking construct the moment it is called with a trailing closure; nothing has to be registered or declared specially.

    fn measured<Value>(label: String, body: () => Value): Value {
      print "starting {label}"
      const result = body()
      print "finished {label}"
      result
    }
    
    const total = measured "sum" {
      1 + 2 + 3
    }
    
    print total
  3. retry and test are written in ordinary TorbScript, out of the same pieces. retry recurses in tail position, and neither one is special-cased by the checker. using is not one of them: it binds a name, so it is a binding form next to const and var (see Destructors).

  4. Only what binds a name or jumps is built into the grammar. const, var, using, fn, type, trait, extend and use bind one; the declarations share one shape, modifier* keyword Name clauses* { body }, because binding a name is the one thing a function cannot do on a caller's behalf.

  5. There are no macros, because names are resolved with the help of types. A receiver and an overload both need to know what a name refers to before a macro could rewrite anything, so a macro that ran before name resolution and one that ran after would not agree on what the code even says.

What this is not#

A control structure is not a keyword you could shadow by declaring a function of the same name. if, for, while and match are the grammar, not names - there is no function called if, and a keyword is refused wherever a name is declared, down to a parameter.

fn unless(condition: Bool, body: () => Void) {
  if !condition {
    body()
  }
}

unless false { print "runs, because the condition is false" }
Kompiliert nicht
fn repeat(times: Int, if: Bool) {
  print times
}
// error: `if` is a keyword and cannot be used as a name here
Esc