Skip to content

Tutorial 02: Types and Pattern Matching

Algebraic data types are the core of ll-lang. This tutorial covers sum types, exhaustive matching, Maybe/Result, and shows the token savings vs TypeScript and Python.

Defining a sum type

module Shapes

Shape = Circle Float | Rect Float Float | Empty

Shape is a sum type with three constructors: - Circle wraps one Float (the radius) - Rect wraps two Floats (width, height) - Empty carries no data

No type keyword, no angle brackets, no | on the first constructor. Uppercase name → type declaration.

Pattern matching

area(s Shape) Float =
  match s
    | Circle r     -> 3.14159 * r * r
    | Rect w h     -> w * h
    | Empty        -> 0.0

The compiler checks exhaustiveness. If you remove the Empty arm:

E003 4:1 NonExhaustiveMatch type:Shape missing:Empty

One line, machine-readable, no stack trace. Add the arm, recompile, done.

Clause sugar — shorter match syntax

When a function body consists entirely of match arms, you can drop match s:

area(s Shape) Float =
  | Circle r  -> 3.14159 * r * r
  | Rect w h  -> w * h
  | Empty     -> 0.0

The compiler implicitly matches on the last parameter (s). Same semantics, fewer tokens.

Maybe and safe operations

ll-lang has no null. Return Maybe A instead:

safeDivide(a Float)(b Float) =
  if b == 0.0
    None
  else Some (a / b)

Callers must handle both cases:

showDivision(a Float)(b Float) =
  match safeDivide a b
    | Some result -> strConcat "Result: " (floatToStr result)
    | None        -> "Division by zero"

The compiler enforces this — you cannot pass Maybe[Float] where Float is expected (E001).

Result for errors with detail

ParseError = EmptyInput | InvalidChar Char | Overflow

parseAge(s Str) =
  if s == ""
    Err EmptyInput
  else
    match strToInt s
      | None   -> Err (InvalidChar 'x')
      | Some n ->
        if n < 0 || n > 150
          Err Overflow
        else Ok n

Result A E = Ok A | Err E is in the prelude. Chain results with resultBind:

validateUser(ageStr Str)(nameStr Str) =
  resultBind parseAge ageStr (\age.
    if strLen nameStr == 0
      Err EmptyInput
    else Ok (nameStr, age))

Parametric types

Types can take type parameters:

Pair A B = MkPair A B

fst(p Pair[A][B]) =
  match p
    | MkPair a _ -> a

snd(p Pair[A][B]) =
  match p
    | MkPair _ b -> b

Type parameters use brackets: Pair[Int][Str] at the call site, Pair A B in the declaration.

Token comparison

Same Shape type and area function in three languages:

ll-lang — 27 tokens

Shape = Circle Float | Rect Float Float | Empty

area(s Shape) Float =
  | Circle r  -> 3.14159 * r * r
  | Rect w h  -> w * h
  | Empty     -> 0.0

TypeScript — 89 tokens

type Shape =
  | { tag: "Circle"; r: number }
  | { tag: "Rect"; w: number; h: number }
  | { tag: "Empty" };

function area(s: Shape): number {
  switch (s.tag) {
    case "Circle": return Math.PI * s.r * s.r;
    case "Rect":   return s.w * s.h;
    case "Empty":  return 0;
  }
}

Python — 72 tokens

from dataclasses import dataclass
from typing import Union

@dataclass
class Circle: r: float
@dataclass
class Rect: w: float; h: float
@dataclass
class Empty: pass

Shape = Union[Circle, Rect, Empty]

def area(s: Shape) -> float:
    match s:
        case Circle(r):    return 3.14159 * r * r
        case Rect(w, h):   return w * h
        case Empty():      return 0.0

ll-lang: 3.3× fewer tokens than TypeScript, 2.7× fewer than Python for this pattern. Across the benchmark suite, type definitions average 3–5× fewer tokens vs TS/Python (see benchmarks/results/token-benchmark.md).

Nested patterns

Patterns compose. Matching inside constructors:

Tree A = Leaf | Node (Tree A) A (Tree A)

depth(t Tree[A]) =
  | Leaf        -> 0
  | Node l _ r  -> 1 + max (depth l) (depth r)

-- Matching nested constructors in one arm
isLeftLeaf(t Tree[A]) =
  match t
    | Node Leaf _ _ -> true
    | _             -> false

Next steps