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. It also works with lists:
let data = { scores: [85, 92, 78] } in
fetch(data, [.scores.0, .scores.1, .scores.2])
|> reduce(0, fn(sum, x ~> sum + x))
Extract multiple paths in one call, then pipe the result to further processing.
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.