Skip to content

unions and pattern matching ​

editable examples

Every example on this page can be edited and run here: click the pencil to open it in an editor, change it, and run it in your browser. Errors, hovers and completions come from the ghūl compiler as you type.

The unions and pattern-matching examples are whole programs you can run here, or build from the ghul-examples repository.

A union is a type whose values each hold one of a fixed set of variants. Each variant has a name and its own fields, or no fields at all. A Shape below is always either a CIRCLE or a SQUARE:

ghul
union Shape is
CIRCLE(radius: double)
SQUARE(side: double)
si
union Option[T] is
SOME(value: T)
NONE
si
union Result[T, E] is
OK(value: T)
ERROR(error: E)
si

A variant's own fields can be read only from a value the compiler knows holds that variant. if let, case and isa each test which variant a value holds and narrow it to the variant they find. A union with one field-carrying variant, or with one variant marked default, can also use ? and !. The definitions page covers declaring unions: unit variants, the default variant, primary-constructor headers, and traits.

matching with if let ​

if let c: CIRCLE = s tests whether s holds a CIRCLE. If it does, the branch runs with a new local variable c holding the same value at type CIRCLE, so c.radius can be read directly. If it doesn't, elif let makes the next test:

ghul
union Shape is
CIRCLE(radius: double)
SQUARE(side: double)
si
area(s: Shape) -> double is
if let c: CIRCLE = ►s then
return 3.14159d * c.radius * c.radius
elif let q: SQUARE = ►s then
return q.side * q.side
fi
return 0.0d
si

The compiler doesn't check an if let chain for exhaustiveness, so area needs the final return. A case expression is checked.

matching with case ​

A case expression tests one value, the scrutinee, against a series of when arms and evaluates the first arm that matches. Where the scrutinee's type is a closed domain, such as a union, the compiler checks that the arms cover every value. The arms below cover both variants of Shape, so area doesn't need a fallback return, and leaving an arm out is a compile error:

ghul
area(s: Shape) -> double =>
// case over a union is checked for exhaustiveness: every variant
// is covered here, so no else arm is needed
case ►s
when c: CIRCLE then 3.14159d * c.radius * c.radius
when q: SQUARE then q.side * q.side
esac
write_line("{area(CIRCLE(2.0d))}")
write_line("{area(SQUARE(3.0d))}")
12.56636
9

A when arm takes the same patterns as if let: a type test with a new local variable (c: CIRCLE), or a destructure, whose literal leaves and ~-marked leaves test values rather than define variables. An arm can end in a /\ guard. When an arm's pattern or guard fails, the next arm is tried.

A when arm can instead list values. The compiler compares the scrutinee with each value the way =~ does, so a string matches by its characters, and when null matches an absent value.

A unit variant has a single shared instance, so a when arm that names one covers that variant, the same as a type test. Arms that name every unit variant of a union cover it without an else:

ghul
union Light is
RED
AMBER
GREEN
si
next(light: Light) -> Light =>
// naming each unit variant by value covers the union, so no else is needed
case light
when Light.RED then Light.GREEN
when Light.GREEN then Light.AMBER
when Light.AMBER then Light.RED
esac
true

A value in a when arm takes its type from the scrutinee, so a unit variant of a generic union doesn't need type arguments there: when Option.NONE then works over an Option[int].

See the case statement for more details.

option-shaped unions ​

In a union with exactly one variant that has fields of its own, or with one variant marked default, the postfix ? tests whether a value holds that variant, and ! reads the variant's value, throwing if the value holds another variant. A variant with one field reads as that field; a variant with several fields reads as the variant:

ghul
if ►an_option? then
let value = an_option!
write_line("the option holds {value}")
fi
the option holds 42

The Option union here is declared only to show the shape. For a value that could be present or absent, use ghūl's optional types, T?, which work over reference types, value types and unconstrained type parameters. See optional types for more details, including how a union like this one compares with T?.

testing a variant with isa ​

isa Option.SOME(an_option) tests whether an_option holds a SOME, and if it does, narrows an_option itself to SOME in the then-branch, without defining a new variable:

ghul
if isa Option.SOME(►an_option) then
let value = an_option.value
write_line("the option holds {value}")
fi
the option holds 42

On a union with two variants, the else branch is narrowed to the other variant, so each branch can read its own variant's fields:

ghul
use IO.Std.write_line
union Option[T] is
SOME(value: T)
NONE
si
union List[T] is
NIL
CONS(head: T, tail: List[T])
si
union Tree[T] is
LEAF(value: T)
NODE(left: Tree[T], right: Tree[T])
si
use Option.SOME
use Option.NONE
use List.NIL
use List.CONS
use Tree.LEAF
use Tree.NODE
test_option()
test_list()
test_tree()
test_option() is
let some_int = SOME(42)
let none_int = NONE()
let stringify_option = o rec =>
if isa SOME(►o) then
"{o.value}"
else
"none"
fi
write_line(stringify_option(some_int))
write_line(stringify_option(none_int))
si
test_list() is
let list = CONS(1, CONS(2, CONS(3, NIL())))
let stringify_list = l rec =>
if isa CONS(►l) then
let (head, tail) = l in
"{head}, {rec(tail)}"
else
"nil"
fi
write_line(stringify_list(list))
si
test_tree() is
let tree = NODE(
NODE(
LEAF(1),
LEAF(2)
),
NODE(
LEAF(3),
LEAF(4)
)
)
let stringify_tree = t rec =>
if isa NODE(►t) then
let (left, right) = t in
"({rec(left)}, {rec(right)})"
else
"{t.value}"
fi
write_line(stringify_tree(tree))
si

isa can test a member path such as shape.outline, or self, as well as a local variable. See type narrowing for more details.