Structuring larger scripts

A handful of handlers can sit side by side, but once a script repeats logic or grows past a screen, you will want to factor it. This chapter covers the tools for that: named helpers, named constants, blocks passed as values, the threading operator, and the flow-control forms that let a routine exit early or span ticks. Throughout, a line beginning with # is a comment.

Named helpers — def

def name { … } binds a block to a name in the command namespace, so you call it like a built-in. Use it to name a computation you repeat:

# yields x + 1; the bare [+ $x 1] is an expression statement,
# so its value becomes the block's value
def inc { <x> [+ $x 1] }

after command (add) {
  do "say [inc 4] and [inc 41]."     # says: 5 and 42.
}

A block's value is its last statement's value, and that statement must be a value-producing call — a [...] substitution or a query. Use return to exit early or make intent explicit:

def classify { <c>
  if [ge [level $c] 20] { return [upper "elite"] }
  return [upper "common"]
}

after command (rank) {
  do "say You are [classify $actor]."
}

A helper can take several parameters — list them all inside the angle brackets:

def max { <a b>
  if [gt $a $b] { return $a }
  return $b
}

after command (bigger) {
  do "say The larger is [max [level $actor] 20]."
}

def is top-level only, and a def sits at the outermost scope — it does not see a handler's injected names like $actor. A helper that needs the actor must take it as a parameter, as classify does. All defs are hoisted before any code runs, so they may call one another regardless of source order.

Named constants — top-level const

At the top level, const name value binds an immutable value in the variable namespace, readable as $name anywhere in the script — including inside a def:

const HOME 3001

after command (recall) {
  walkto $HOME
}

Blocks as values

A block is a first-class value. Bind one with let and invoke it by putting it in command position inside [...]:

after command (twice) {
  let shout { <msg> do "say $msg!" }
  [$shout "here"]
  [$shout "and here"]
}

Closures and the counter idiom

Because a block keeps access to the scope it was defined in, and set reaches outward, a block returned from a def can carry private state:

def make_counter { let n 0 ; return { set n [+ $n 1] ; return $n } }

after command (count) {
  let bump [make_counter]
  do "say [$bump] [$bump] [$bump]"     # says: 1 2 3
}

The returned block still reaches n through its enclosing scope, so each [$bump] advances the same n. (Statements inside a block are separated by newlines or semicolons, as in make_counter.) Note the explicit return on the inner block: a bare { … } written as a statement is invoked, not yielded as a value, so to hand a block back you must put it in argument position — here, as the argument to return.

Passing a command by name — &

The & sigil takes a command as a value, bridging the command namespace into the variable namespace. Its main use is handing a named predicate to a consumer instead of writing an inline block. For a built-in predicate, pass it directly — &isplayer is exactly the block { <c> [isplayer $c] }, only shorter:

after command (census) {
  let players [select [/> $self room creatures] &isplayer]
  do "say [count $players] players present."
}

The same works for a predicate you define yourself — useful when the test is more than a single built-in:

def is_veteran { <c> [and [isplayer $c] [ge [level $c] 20]] }

after command (veterans) {
  let vets [select [/> $self room creatures] &is_veteran]
  do "say [count $vets] veterans present."
}

&name resolves to your def if there is one, otherwise the built-in of that name. In command position it is redundant — [is_veteran $c] and [&is_veteran $c] are the same — but as an argument it is how you pass the function itself.

Variadic helpers — the rest parameter

The last parameter may be prefixed with ..., collecting any extra arguments into an iterable, each argument a separate element:

def announce { <prefix ...rest>
  each $rest { <item> do "say $prefix: $item" }
}

after command (list) {
  announce "Wares" sword shield "healing potion"
}

announce binds prefix to "Wares" and rest to an iterable of the three remaining arguments — note "healing potion" stays one element because the rest parameter splits on arguments, not whitespace. With no extra arguments the rest binds an empty iterable, never null.

Threading a value — />

/> value step1 step2 … feeds a value left-to-right through a chain of single-argument callables, each result becoming the next call's argument. It is sugar for nested substitution:

after command (announce) {
  do "say [/> $actor name upper]!"     # = [upper [name $actor]]
}

This shouts the actor's name in capitals. A step may be any compatible callable: a bareword naming a built-in, system routine, or def; an &name reference; a $var holding a block; or a literal { … } block. Each step takes exactly one argument — the running value. A step invoked as a block value (a &ref, $var, or literal block) yields its result the usual way, as the block's last expression.

Loop control — break and continue

Inside a loop body (each, select, and friends), continue skips to the next element and break ends the loop. They affect only the nearest enclosing loop:

after command (countoff) {
  each [words "1 2 3 4 5"] { <n>
    if [streqi $n "3"] { continue }    # skip three
    if [streqi $n "5"] { break }       # stop before five
    do "say $n"
  }
}

This says 1, 2, 4. To leave a routine entirely, use return; to stop the whole script on the spot, use halt.

Spanning ticks — pause

A handler normally finishes within one tick. pause n suspends it for n ticks and resumes right where it left off, locals intact — the one way a script stretches across game time:

after enter {
  do "bow $actor"
  pause 3
  do "emote straightens and points to a notice on the wall."
}

(This is the mechanism walkto and driveto expand into.)

Taking sole control — nuke

nuke cancels every other script currently running on the same owner — including ones suspended on a pause — while the script that called it keeps going. Use it when a routine must seize the owner and stop competing handlers, such as an alarm overriding a paused idle sequence:

handle command (alarm) {
  nuke
  do "shout Intruder! All routines suspended!"
}

See also