Skip to content

expression oriented programming

ghūl is expression-oriented: most control-flow constructs produce a value, so they can be assigned to a local variable, returned, or passed as an argument. With expression bodies on functions and methods, a computation can read as one value-producing expression rather than a sequence of assignments.

The constructs are covered in full elsewhere: if and case as expressions on the expressions page, and the if and case statement forms under control flow. This page shows them working together.

if as an expression

An if yields the value of the chosen branch. Each branch is itself an expression, and the branches agree on a type:

ghul
sign(n: int) -> string =>
if n < 0 then "negative"
elif n == 0 then "zero"
else "positive"
fi;
entry() is
write_line(sign(-4));
write_line(sign(0));
write_line(sign(7));
si

case as an expression

A case yields the value of the matched arm. As an expression it needs an else arm, so every value is covered:

ghul
day_kind(day: int) -> string =>
case day
when 0, 6 then "weekend"
else "weekday"
esac;
entry() is
write_line(day_kind(0));
write_line(day_kind(3));
write_line(day_kind(6));
si

val blocks

A val ... lav block runs a sequence of statements and yields a value: its tail expression, or any return that targets the block. It gives an expression room for intermediate local variables, loops, and early exits:

ghul
// a val ... lav block runs its statements, then yields the tail expression:
midpoint(lo: int, hi: int) -> int => val
let span = hi - lo;
lo + span / 2
lav;
// an early return yields from the block, not the enclosing function:
first_even(xs: int[]) -> int => val
for x in xs do
if x % 2 == 0 then
return x;
fi
od
-1
lav;
entry() is
write_line("midpoint = {midpoint(10, 20)}");
write_line("first_even = {first_even([1, 3, 4, 7])}");
write_line("first_even = {first_even([1, 3, 5])}");
si

A return inside the block yields from the block, not from the enclosing function.

let in

A let ... in ... expression introduces one or more local variables scoped to a single trailing expression. It is lighter than a val ... lav block when a value needs only a local or two:

ghul
hypotenuse_squared(a: int, b: int) -> int =>
let a2 = a * a, b2 = b * b in a2 + b2;
entry() is
write_line("h2 = {hypotenuse_squared(3, 4)}");
si
h2 = 25

expression bodies

A function, method, property, or anonymous function can replace its block body with => and a single expression. That expression can be an if, a case, or a val ... lav block:

ghul
// expression-bodied free function:
square(n: int) -> int => n * n;
class COUNTER is
_count: int;
init() is si
// an expression body can be a val ... lav block:
bump() -> int => val
_count = _count + 1;
_count
lav;
si
entry() is
write_line("square(6) = {square(6)}");
let c = COUNTER();
write_line("bump = {c.bump()}");
write_line("bump = {c.bump()}");
// expression-bodied anonymous function:
let twice = (n: int) => n * 2;
write_line("twice(21) = {twice(21)}");
si

composing them

These forms nest, so one expression body can hold a case, an if, and a val block:

ghul
// one expression body holding a case expression, an if expression, and a val block:
grade(score: int) -> string => val
let band =
case score / 10
when 10, 9 then "A"
when 8 then "B"
when 7 then "C"
else "F"
esac;
if band == "F" then "fail" else "pass ({band})" fi
lav;
entry() is
write_line(grade(95));
write_line(grade(82));
write_line(grade(60));
si