Documentation / Branching

Branching

Use a normal if for one decision. Use a full match when one value has more than two meaningful cases. Both forms stay explicit about scope, fallback behaviour and the values they produce.

No mystery fallthrough. No wildcard punctuation pretending to mean three different things.

Choose the explanation level: Statement if

Statement if

Use if when code should run only under one condition.


    if ready:
        io.line("Ready!")
    ;
    

Add else for the other path:


    if ready:
        io.line("Ready!")
    else
        io.line("Not ready yet")
    ;
    

One semicolon closes the whole structure.

Moth does not have an else if chain. Nest another if inside else, or use pattern matching when several cases belong to one value.

A statement if runs a block when its condition is true.


    if ready:
        io.line("ready")
    else
        io.line("waiting")
    ;
    

Contract

  • The ordinary condition must resolve to Bool.
  • else is optional.
  • The if body and optional else body are child scopes.
  • One final semicolon closes the complete if and else structure.
  • Statement if is non-exhaustive when no else is written.
  • A statement branch does not produce a value merely because its body contains expressions.

No statement-level else if

A same-line else if chain is not syntax.

Use a nested if inside the else body:


    if first_condition:
        io.line("first")
    else
        if second_condition:
            io.line("second")
        else
            io.line("fallback")
        ;
    ;
    

Use a full match when one value is being compared against several patterns.

Option statement capture

A statement can test an optional value and bind its present payload:


    if maybe_name is |name|:
        io.line(name)
    ;
    

It can also test for absence:


    if maybe_name is none:
        io.line("missing")
    ;
    

Without else, the unmatched case does nothing.

Choose the explanation level: Value-producing if

Value-producing if

An if can choose a value.

The short form fits on one line:


    label = if ready then "ready" else "waiting"
    

The inline form also handles option capture:


    label = if maybe_name is |name| then name else "guest"
    

Use block form when a branch needs more room:


    label = if ready:
        then "ready"
    else
        then "waiting"
    ;
    

then provides the value for the surrounding assignment.

Both branches must provide the value or leave the function in another valid way.

The current compiler has a known issue with block value-producing if that uses then; use the inline form for now.

A value-producing if sends values to an immediate receiving site.

Inline form


    label = if ready then "ready" else "waiting"
    

The inline then and else form stays on one logical line.

Inline option and choice predicates

The inline form also supports option-present capture and choice-variant predicates:


    label = if maybe_name is |name| then name else "guest"
    score = if status is Ready then 1 else 0
    

Block form


    label = if ready:
        then "ready"
    else
        then "waiting"
    ;
    

then sends one or more values from the active branch to the receiver.

The current compiler has a known issue with block value-producing if that uses then; use the inline form for now.

Receiving sites

Value-producing control flow is accepted at closed receiving sites:

  • declarations
  • assignments
  • multi-bind
  • returns
  • nested then

It is not a general expression form.

A value-producing block is invalid directly inside:

  • function arguments
  • operator operands
  • constructor arguments
  • collection literals
  • template interpolation
  • expression statements

Completeness

  • A value-producing if requires else.
  • Every reachable branch must produce the required values or terminate.
  • At least one reachable branch must produce values.
  • Every producing path must match the receiver's arity.
  • Produced values must be compatible with the receiver's types.

    left, right = if swap:
        then second, first
    else
        then first, second
    ;
    

Option-present and choice-predicate inline forms use the same receiving-site model and are covered by the pattern references.

Choose the explanation level: Pattern matching

Pattern matching

Pattern matching is useful when one value can take several meaningful shapes.


    if score is:
        < 0 => io.line("invalid")
        0 => io.line("zero")
        >= 90 => io.line("excellent")
        else => io.line("other")
    ;
    

Each arm begins with a pattern and =>.

There are no semicolons between arms. One semicolon closes the whole match.

Use else => for everything that was not matched earlier. It can have an empty body when the remaining cases should do nothing.

A full match checks one value against a sequence of patterns.


    if score is:
        < 0 => io.line("invalid")
        0 => io.line("zero")
        >= 90 => io.line("excellent")
        else => io.line("other")
    ;
    

Match shape

  • The header is if value is:.
  • An arm is <pattern> => <body>.
  • A guarded arm is <pattern> if condition => <body>.
  • The guard must resolve to Bool.
  • Arm headers begin at the start of a logical line.
  • The next line-initial arm or the final semicolon ends the current arm body.
  • Match arms do not use individual semicolons.
  • One final semicolon closes the complete match.

Default arm

else => is the only catch-all arm.


    if value is:
        0 => io.line("zero")
        else =>
    ;
    

In a statement match, bodyless else => explicitly ignores every remaining case.

_ => is not wildcard syntax. else => _ is also invalid.

Value-producing match

A match can produce values with then at a closed receiving site.


    label = if status is:
        Ready => then "ready"
        Waiting => then "waiting"
    ;
    

Every selected value-producing arm must produce the receiver's required values or terminate.

Choose the explanation level: Patterns and exhaustiveness

Patterns and exhaustiveness

Patterns can match exact values, ranges, choice variants or option shapes.


    if status is:
        Ready => io.line("ready")
        Waiting => io.line("waiting")
    ;
    

A choice match does not need else => when every variant is covered.

Payload variants expose their fields through captures:


    if response is:
        Success => io.line("done")
        Err(message) => io.line(message)
    ;
    

The capture name must match the field name.

Use as when the local name should be different:


    Err(message as error_text) => io.line(error_text)
    

Options have two complete shapes: absent and present.


    if maybe_name is:
        none => io.line("missing")
        |name| => io.line(name)
    ;
    

Most other value matches need else => because the compiler cannot list every possible Int, String or other scalar value.

Match patterns describe the values an arm accepts.

Literal patterns

A literal pattern must be compatible with the scrutinee type.


    if value is:
        1 => io.line("one")
        2 => io.line("two")
        3 => io.line("three")
        else =>
    ;
    

One statically typed match can't mix unrelated Int, String and Bool literal patterns.

Choice variants


    if status is:
        Ready => io.line("ready")
        Status::Waiting => io.line("waiting")
    ;
    

Choice payload captures


    if response is:
        Success => io.line("done")
        Err(message) => io.line(message)
        Pending(retry_count, message as pending_message) =>
            io.line(pending_message)
    ;
    

Option patterns

Options support:

  • none
  • literal present-value patterns
  • relational present-value patterns
  • |name| to capture any present value

An option match may omit else => only when it has both:

  • an unguarded none arm
  • an unguarded |name| present-value capture

    if maybe_name is:
        none => io.line("missing")
        |name| => io.line(name)
    ;
    

Literal or relational option arms don't replace the unguarded present-value capture when else => is omitted.

Relational patterns


        < value
        <= value
        > value
        >= value
    

Relational patterns support Int, Float and Char.

The catch-all arm

else => is the only full-match catch-all. A bare unknown name in choice pattern position is treated as an unknown or invalid variant. A bare name in other full-match pattern positions is invalid syntax.


    if value is:
        0 => io.line("zero")
        else => io.line("other")
    ;
    

Implementation gap

The accepted language removes general bare-name capture from every full-match position and removes String from relational pattern subjects. The current compiler still accepts some of these removed forms. Stage B of the language documentation plan removes the obsolete parser and analysis paths. See Progress for the recorded drift.

Choice payload capture contract

  • Payload captures must list every declared field.
  • Captures stay in declaration order.
  • The source name before as must exactly match the declared field name.
  • as local_name changes only the arm-local binding.
  • Duplicate local bindings are invalid.
  • Wildcard payload captures aren't supported.
  • Named assignment syntax such as Err(message = text) isn't payload-pattern syntax.
  • Nested payload patterns are deferred.

Exhaustiveness

  • An ordinary non-choice full match requires else =>.
  • An option match may instead use unguarded none plus unguarded |name|.
  • A choice match may omit else => only when every variant is covered.
  • Any guarded choice arm requires else =>.
  • The same choice variant can't be matched more than once.
  • Payload exhaustiveness is tag-level. One valid payload capture arm covers every value of that variant.
  • A statement match may use bodyless else =>.
  • A value-producing match must produce the required values on every selected path.