Skip to content

Expressions

This page describes the expression forms used in Lutra.

Overview

Expressions compute values. Examples include literals, tuple construction, function calls, pipelines, conditionals, and matches.

Literals and names

A literal or a name is an expression.

false
42: int32
"hello"
a
std::cmp

See Literals for the scalar literal forms.

Tuple expressions

Tuple expressions use {...}. Fields can be positional or named.

{true, 10}
{a = true, 10, b = 5.4}

You can mix named and positional fields in the same tuple.

Tuple field access

Use . with a field name or zero-based position.

{a = 10, false}.a
{a = 10, false}.1

Field lookup also works after tuple spread and on framed values such as std::Date.

{a = @2025-11-15}.a.days_epoch
@2025-11-14.0

Tuple spread

Use ..expr inside a tuple expression to insert all fields from another tuple.

{a = 4: int32, ..{x1 = 3: int32, x2 = "hello"}, b = false}
{false, ..x, false}

Tuple spread preserves field order. Spread works with named and positional fields.

Array expressions

Array expressions use [...]. All items must have the same type.

[true, false, true]
[10, 33, -3, 2, 40]: [int64]
[]: [int64]

Enum construction

Enum variants are constructed with a leading .. Variants without payloads do not use parentheses. Variants with payloads do.

.done
.pending(513)
.cancelled("I don't like it")
.dog(.collie("Belie"))

A type annotation is sometimes needed so the compiler knows which enum type you mean:

.cat("Whiskers"): Animal

Function calls

A function call uses name(arg1, arg2, ...).

my_func(4, false)
std::add(6, 2)

Arguments can be positional or labeled. See Definitions for labeled parameter syntax.

calculate(1, add = 2, multiply = 3)

Anonymous functions

Lutra supports two anonymous function forms.

The short form uses name -> expr:

x -> x + 1
n -> match n {
  "world" => "Hello world!",
  n => f"Hello, {n}!"
}

The long form uses func (...) -> expr and can include parameter and return annotations:

func (x: int64) -> x + 1
func (animal: Animal): text -> match animal {
  .cat(name) => name,
  _ => "<unnamed>",
}

Pipelines

The pipe operator | passes the value on the left as the first argument to the expression on the right. Pipelines are left-associative.

These forms are equivalent:

my_func(5)
5 | my_func()
5 | my_func

These forms are also equivalent:

another_func(my_func(5), false)
5 | my_func | another_func(false)

The right side of a pipeline is usually one of these:

  • a function name,
  • a function call with the remaining arguments,
  • an anonymous function.

Conditional expressions

An if expression chooses between two expressions.

if x then "yes" else "no"

Branches can use parentheses for multiline formatting:

if x then (
  "yes"
) else (
  "no"
)

Match expressions

A match expression compares a subject value against patterns. The first matching branch is selected.

match name {
  "world" => "Hello world!",
  n => f"Hello, {n}!"
}

You can also match enum variants and nested enum values:

match animal {
  .cat(name) => f"Hello {name}",
  .dog(.generic) => "Who's a good boy?",
  .dog(.collie(name)) => f"Come here {name}",
}

See Patterns for the available pattern forms.

Let blocks

A parenthesized block can contain one or more let bindings, separated by semicolons, followed by a final expression. The value of the block is the value of the final expression.

(
  let a = false;
  {a, a, !a}
)

You can use multiple bindings:

(
  let a = x * 2;
  let b = a * 2;
  let c = -b;
  c
)

Type ascription

Use expr: Type to fix the type of an expression.

4: int32
[]: [bool]
.none: enum {none, some: text}

See also