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.
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 keywordRules#
A control structure is a function whose closure or
lazyparameter is what makes it read like syntax.unless(condition: Bool, body: () => Void), called asunless ready() { print "waiting" }, is not different from any other function call - it is a command call with a trailing closure.Writing your own is writing a function and calling it. A function whose last parameter is
() => Valuebecomes 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 totalretryandtestare written in ordinary TorbScript, out of the same pieces.retryrecurses in tail position, and neither one is special-cased by the checker.usingis not one of them: it binds a name, so it is a binding form next toconstandvar(see Destructors).Only what binds a name or jumps is built into the grammar.
const,var,using,fn,type,trait,extendandusebind 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.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" }fn repeat(times: Int, if: Bool) {
print times
}
// error: `if` is a keyword and cannot be used as a name hereRelated#
- Trailing closures - the syntax that makes a control structure read like one.
- Command calls - the call form every control structure is written in.
- std/core -
do,unless,retryandClose. - Declaring a function - the one thing a control structure cannot replace: binding a name.