The day after the 0.9.x release, we couldn’t stop. Here’s what landed in Elo today.

Guards and Checks

While working on Elo, one thing kept bugging me: we had assert() for tests, but no way to express preconditions and postconditions in real programs. Assertions live at the statement level, but Elo is purely expression-based.

Enter guard and check:

let user = { age: 25 } in
  guard
    positive: user.age > 0,
    adult: user.age >= 18
  in
    'Welcome!'

Guards are preconditions—they validate input before computation. If any condition fails, you get a clear error message with the constraint label.

Checks work the same way but express postconditions:

let result = [1, 2, 3] in
  check non_empty: length(result) > 0 in
    result

Pipe-Style Guards

Since Elo loves pipes, guards fit naturally into data pipelines:

[1, 2, 3]
  |> guard(d | 'must not be empty': length(d) > 0)
  |> map(x ~> x * 2)
  |> guard(r | has_items: length(r) > 0)

The guard(x | condition) form binds the piped value to x for the condition check, then passes it through unchanged. It’s validation as a pipeline step.

Combining Guards, Lets, and Checks

You can nest guards and checks within let expressions:

let data = [1, 2, 3] in
  guard 'valid input': length(data) > 0 in
    let result = map(data, x ~> x * 2) in
      check non_empty: length(result) > 0 in
        result

This reads almost like a specification: validate input, compute, validate output, return.

Labeled Constraints

Guards share syntax with an enhancement we made to subtype constraints. You can now label individual constraints for better error messages:

let
  Positive = Int(i | positive: i > 0),
  Even = Int(i | even: i % 2 == 0),
  PosEven = Int(i | positive: i > 0, even: i % 2 == 0)
in
  6 |> PosEven  # passes both constraints

When a constraint fails, the error message includes the label:

constraint 'positive' failed

Or use string messages for even clearer errors:

let Adult = Int(a | 'must be 18 or older': a >= 18)
in 25 |> Adult

Passing 15 instead would produce: Error: must be 18 or older.

Polymorphic Fetch

Data extraction got more powerful. Instead of writing:

let data = { user: { name: 'Alice', email: 'alice@example.com' } } in {
  x: fetch(data, .user.name),
  y: fetch(data, .user.email)
}

You can now write:

let data = { user: { name: 'Alice', email: 'alice@example.com' } } in
  fetch(data, {
    x: .user.name,
    y: .user.email
  })

Same result, less noise. You can also use dynamic keys:

let data = { name: 'Alice' } in
let key = 'name' in
  fetch(data, key)

String keys and array paths work the same as path literals.

What’s Next

These features make Elo more practical for data validation and transformation pipelines. Combined with the Finitio-style type system, you can now express quite sophisticated data contracts:

let
  Email = String(s | 'invalid email': contains(s, '@')),
  Age = Int(a | positive: a > 0, reasonable: a < 150),
  Person = { email: Email, age: Age },
  input = { email: 'alice@example.com', age: '30' },
  person = Person(input)
in
  'Hello, ' + person.email

The vision is coming together: a simple, safe, portable expression language for data manipulation. We’re getting there.

Try the new features or check the Reference for details.