Control flow

Which forms run, and how many times. Every control form is an expression with a value.

Branching

Layout if gathers elif and else peers and reads as one cond:

if (> score 90):
  print "excellent"
elif (> score 70):
  print "solid"
else:
  print "keep practicing"

cond is the form underneath. Clauses are tested from the top and the first true one wins; with no match and no else:, the value is None:

(cond
  [(> score 90) "excellent"]
  [(> score 70) "solid"]
  [else: "keep practicing"])

Because an if has a value, one var can take a whole choice:

var label:
  if (> score 90):
    "excellent"
  else:
    "ordinary"

when and unless are the one-armed forms. when runs its body if the test is true, unless if it is false, and both yield None otherwise:

when (> n 0):
  print n

unless ready:
  return "not yet"

Tests use Python truthiness: 0, "", #[], and None are false.

Choosing on a value

switch compares one value against candidates with ==. match destructures it against patterns: sequences, mappings, classes, typed captures, with guards. The pattern language has its own page, Match patterns.

match msg:
  case #[(tag :: keyword) *: args]:
    (handle tag args)
  case #{"kind": "job" "url": u}:
    (fetch u)
  else:
    (ignore msg)

Boolean operators

and and or short-circuit and return the deciding operand, as in Python; not returns a boolean:

if (and (>= age 13) (< age 20)):
  print "teen ticket"

Comparisons chain: (< 0 x 10) is 0 < x < 10.

while

var n 3
while (> n 0):
  print n
  decr! n
else:
  print "liftoff"

The optional else: body runs when the test becomes false. A break skips it.

for-each

for-each [name names]:
  print "hello" name
else:
  print "all greeted"

for-each walks any Python iterable. The binding can destructure: for-each [[k v] (d.items)]:. The else: peer runs when the iterable is exhausted without a break.

break and continue

continue jumps to the next iteration. break leaves the loop, and may carry a value, which becomes the value of the loop:

var found:
  for-each [v values]:
    when (> v 2):
      break v
  else:
    None

Both are calls, (break) and (continue), and both have a bare form on a layout line. Inside a loop’s else: clause the loop has already finished, so a break or continue written there belongs to the enclosing loop, as in Python; with no enclosing loop it is an error.

Comprehensions

list-each, dict-each, and each produce a list, a dict, and a generator from the same [var seq] binding, with a when: or unless: clause on the header as the filter:

list-each [x (range 10)] when: (== 0 (% x 2)):
  (* x x)

They are for values; for-each is for effects.

Early exit from a function

return inside any of these forms leaves the function. It is a call, so (return x), and a bare return on its own line is (return). Functions covers the multiple-value forms.

Exceptions

try is control flow too, and an expression. It is on I/O and effects.

What runs when

These forms all run on the same stackless evaluator, so a loop body that awaits suspends the whole function and resumes in place. Nothing about while or for-each changes under async code; the detail is in Demand, async, and defer.