Skip to content

Pattern Matching ​

AIVI uses pattern matching for branching. There is no if / else statement layer on top of the language: values are inspected directly with the case-split pipe ||>.

Every branch is an expression, and the compiler checks that your match is exhaustive.

The case-split pipe ||> ​

Use ||> when you want to branch on a value:

aivi
type Int -> Text
func describeNumber = arg1 => arg1
 ||> 0 -> "zero"
 ||> 1 -> "one"
 ||> _ -> "many"

value sampleDescription = describeNumber 1

The _ pattern matches anything. Arms are tried from top to bottom. Within pipe bodies, . is the ambient subject; _ is only a discard pattern or discard binding.

The remaining unary examples use unary subject sugar: func name = followed by pipe stages starts from the function's argument, without arg1 => arg1.

Today ||> supports patterns only. Case-stage guard syntax is not implemented end to end yet, so the current workaround is to match first and then compute a Bool in the arm body or in a named helper.

Matching custom types ​

Custom sum types are declared with type and matched by constructor name:

aivi
type Direction =
  | Up
  | Down
  | Left
  | Right

type Direction -> Text
func directionLabel =
 ||> Up    -> "up"
 ||> Down  -> "down"
 ||> Left  -> "left"
 ||> Right -> "right"

value currentDirection = directionLabel Left

If a constructor carries a payload, bind that payload in the pattern:

aivi
type LoadState =
  | NotAsked
  | Loaded Text
  | Failed Text

type LoadState -> Text
func describeLoadState =
 ||> NotAsked      -> "waiting"
 ||> Loaded name   -> "loaded {name}"
 ||> Failed reason -> "error {reason}"

value stateMessage = describeLoadState (Loaded "Ada")

Wildcards ​

Use _ when you only care about one or two cases and want to discard the unmatched payload:

aivi
type Status =
  | Running
  | Paused
  | Stopped

type Status -> Bool
func isRunning =
 ||> Running -> True
 ||> _       -> False

value runningNow = isRunning Running

Condition-first branching ​

When the choice is really a boolean condition, calculate the condition first and then branch with T|> / F|>:

aivi
type Int -> Text
func classifyNumber = . > 0
 T|> "positive"
 F|> "not positive"

value numberClass = classifyNumber 12

That keeps the branch expression explicit without introducing a separate statement form.

Record patterns ​

Records can be destructured directly in a match arm:

aivi
type Profile = {
    name: Text,
    score: Int
}

type Profile -> Text
func profileSummary =
 ||> { name, score } -> "{name} scored {score}"

value summaryText =
    profileSummary {
        name: "Ada",
        score: 100
    }

For a condition on a single field, project it directly and compute a boolean:

aivi
type Profile = {
    name: Text,
    score: Int
}

type Profile -> Bool
func isTopScore = .score >= 100

value topScore =
    isTopScore {
        name: "Grace",
        score: 120
    }

Tuple patterns ​

Tuples let you match several values at once:

aivi
type Point = Point Int Int

type Direction =
  | Up
  | Down
  | Left
  | Right

type (Point, Direction) -> Point
func step =
 ||> (Point x y, Up)    -> Point x (y - 1)
 ||> (Point x y, Down)  -> Point x (y + 1)
 ||> (Point x y, Left)  -> Point (x - 1) y
 ||> (Point x y, Right) -> Point (x + 1) y

value movedPoint =
    step (
        Point 4 9,
        Up
    )

Nested patterns ​

Patterns can be nested as deeply as the value requires:

aivi
type Inner = A | B

type Outer = Outer Inner

type Outer -> Text
func describeOuter =
 ||> Outer A -> "outer A"
 ||> Outer B -> "outer B"

value outerLabel = describeOuter (Outer A)

Built-in sum types ​

Option and Result are ordinary tagged types, so matching them feels the same:

aivi
type Option Text -> Text
func displayName =
 ||> Some name -> name
 ||> None      -> "anonymous"

value shownName = displayName (Some "Ada")
aivi
type Result Text Int -> Text
func handleResult =
 ||> Ok value    -> "got {value}"
 ||> Err message -> "failed {message}"

value handledResult = handleResult (Ok 42)

Truthy/falsy shorthand ​

When the subject fits one of AIVI's canonical truthy/falsy carriers, T|> and F|> are shorter than a full match. Today that means Bool, Option A, Result E A, and Validation E A; the same shorthand also lifts through one outer Signal (...).

aivi
type Bool -> Text
func statusLabel =
 T|> "active"
 F|> "inactive"

value currentStatus = statusLabel True

Inside a chosen branch, . is rebound to the matched payload when that constructor has exactly one payload. Use ||> when you need a named binding, a nested pattern, or more than the canonical two-way split.

Exhaustiveness ​

AIVI checks that every constructor is covered. In practice that means:

  • list every constructor explicitly, or
  • finish with _ when you want a catch-all branch.

That guarantee is one of the reasons pattern matching is the normal way to branch in AIVI.

Summary ​

PatternMeaning
ConstructorMatch one exact constructor
Constructor nameMatch a constructor and bind its payload
{ field, other }Destructure selected record fields
(a, b)Match a tuple
_Match anything without binding
OperatorMeaning
||>Pattern match / case split
T|>Branch for True / Some / Ok / Valid
F|>Branch for False / None / Err / Invalid

(c) 2026 by Andreas Herd