1. Introduction

Here's a taste of what Elo can do. Don't worry if you don't understand everything yet— that's what the next chapters are for!

let
  signup = D2024-01-15,
  trial = P30D
in
  TODAY > signup + trial

This checks if a 30-day trial has expired. Notice how naturally Elo handles dates and durations.

[10, 25, 5, 30] |> filter(x ~> x > 15) |> map(x ~> x * 1.21)

This filters a list to keep values above 15, then applies VAT to each. Data flows left to right.

_.email |> lower |> endsWith('@company.com')

This checks if an input email belongs to a company domain. The _ is input data.

Write once, run anywhere: These expressions compile to JavaScript, Ruby, and SQL. The same logic works in your browser, on your server, or in your database.

2. Extended Arithmetics

You know how calculators work with numbers. Elo extends this idea to other types. The same operators (+, -, *) work on different kinds of data.

Numbers

Just like a calculator:

2 + 3 * 4
2 ^ 10

Booleans

True/false values with and, or, not:

5 > 3 and 10 <= 10
not (1 == 2) or false

Strings

Text with + for concatenation, * for repetition:

'Hello, ' + 'World!'
'ho! ' * 3

Dates & Durations

Dates start with D, durations with P (ISO 8601):

D2024-12-25
TODAY + P30D
NOW + PT2H30M

You can subtract dates to get durations, and scale durations:

D2024-12-31 - D2024-01-01
P1D * 7

The pattern: Operators like +, -, * are not just for numbers. Each type defines what these operations mean. See the stdlib for all type-specific operations.

Practice: Build a Greeting

3. Data Structures

Real data comes in structures: records with fields, collections of items. Elo has two main structures.

Tuples

Named fields, like a record or JSON object:

{ name: 'Alice', age: 30, city: 'Brussels' }

Access fields with a dot:

{ name: 'Alice', age: 30 }.name

Lists

Ordered collections in square brackets:

[1, 2, 3, 4, 5]
['apple', 'banana'] + ['cherry']

Get elements by position:

first([10, 20, 30])
at([10, 20, 30], 1)

Data Paths & Fetch

Navigate nested data safely with paths:

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

Paths start with a dot. fetch returns null if any part is missing:

let data = { user: null } in fetch(data, .user.name) | 'Unknown'

Think JSON: Tuples and lists work just like JSON objects and arrays. Elo makes them first-class citizens with operators and safe navigation.

Practice: Product Total

4. Functions

Operators are great, but sometimes you need more. Elo has a standard library of functions, and you can define your own.

Standard Library

Built-in functions for common operations:

upper('hello')
abs(-42)
year(TODAY)
length([1, 2, 3])

Lambdas

Define your own functions with fn:

let double = fn(x ~> x * 2) in double(21)
let greet = fn(name ~> 'Hello, ' + name + '!') in greet('Elo')

Functions can take multiple parameters:

let add = fn(a, b ~> a + b) in add(3, 4)

Sugar Syntax

For single-parameter lambdas, you can skip fn():

map([1, 2, 3], x ~> x * 2)

Functions are values: You can pass functions to other functions, store them in variables, and return them. This is key for list processing in Chapter 6. Explore all stdlib functions.

Practice: Text Transform

5. Program Structure

Complex expressions need structure. Elo provides three key constructs.

Let Bindings

Name intermediate values to avoid repetition:

let price = 100 in price * 1.21
let width = 10, height = 5 in width * height

Names don't change—once bound, a value stays the same. No surprises!

Conditionals

Choose between values with if/then/else:

if 5 > 3 then 'yes' else 'no'
let age = 25 in if age >= 18 then 'adult' else 'minor'

In Elo, if is an expression—it always produces a value.

Pipe Operator

Chain operations left-to-right with |>:

'  hello  ' |> trim |> upper

Compare with nested calls:

upper(trim('  hello  '))

The pipe version reads naturally: take this, then do that, then that.

let double = fn(n ~> n * 2) in '42' |> Int |> double

Assembly line: Data flows through the pipe, getting transformed at each step. Much cleaner than deeply nested parentheses!

Practice: Rectangle Area

6. Advanced Processing

Now we combine functions and lists for powerful data processing.

Map, Filter, Reduce

map transforms each element:

map([1, 2, 3], x ~> x * 2)

filter keeps elements that match:

filter([1, 2, 3, 4, 5], x ~> x > 2)

reduce combines all elements into one value:

reduce([1, 2, 3, 4], 0, fn(sum, x ~> sum + x))

Chain them together:

[1, 2, 3, 4, 5] |> filter(x ~> x > 2) |> map(x ~> x * 10)

Null Handling

Missing data is represented by null. The | operator provides fallbacks:

null | 'default'
indexOf('hello', 'x') | -1

Use isNull to check:

isNull(null)

Type Selectors

Parse and validate strings into typed values:

Int('42')
Date('2024-12-25')
Duration('P1D')

Strict validation: Type selectors throw an error if parsing fails. This ensures data integrity - invalid input like Int('abc') won't silently become null.

Practice: Filter and Double

7. Input Data

So far we've worked with literal values. Real programs process external data. In Elo, input is accessed through the special _ variable.

The Input Variable

When you use _, your expression becomes a function:

_ * 2

Access fields of input data:

_.price * _.quantity
_.name |> upper

Data Transformation

Elo expressions are data transformations: input flows in, result flows out.

_.items |> map(x ~> x.price) |> reduce(0, fn(sum, p ~> sum + p))
if _.status == 'active' then _.budget * 1.1 else _.budget

Try It

In the playground, enter input data in the Input Data panel. You can switch between JSON and CSV formats using the dropdown. For example, with input {"price": 100, "quantity": 3}:

_.price * _.quantity

Think transformations: Every Elo expression with _ is a reusable transformation. The same expression works whether it runs in your browser (JS), server (Ruby), or database (SQL).

Learn more: See Data Formats in the Reference for details on JSON/CSV conversion and CLI usage.

Practice: Order Total

8. Data Validation

External data (JSON from APIs, user input) needs validation. Elo's type definitions let you define schemas that validate structure and coerce values to the right types.

Note: Type definitions compile to JavaScript and Ruby only. SQL is not supported.

Type Coercion

Type selectors like Int() parse strings. With type definitions, you define reusable types:

let T = Int in '42' |> T

Uppercase names create type definitions. Apply them with the pipe operator.

Struct Schemas

Define the expected shape of objects:

let Person = { name: String, age: Int } in
{ name: 'Alice', age: '30' } |> Person

This validates the structure and coerces age from string '30' to integer 30. Missing or extra fields cause an error.

Optional Fields

Mark fields as optional with :?:

let T = { name: String, age :? Int } in
{ name: 'Bob' } |> T

Missing optional fields are omitted from the result.

Arrays

Validate arrays where each element matches a type:

let Numbers = [Int] in ['1', '2', '3'] |> Numbers

Each string is coerced to an integer: [1, 2, 3].

Union Types

Accept multiple types with |:

let T = Int|String in 'hello' |> T

Tries each alternative left-to-right, returns the first match.

let T = [Int|String] in ['42', 'hello'] |> T

Returns [42, 'hello']—numeric strings become integers.

Constraints

Add validation predicates to types:

let Positive = Int(n | n > 0) in '42' |> Positive

The value is first coerced to Int, then the constraint n > 0 is checked.

Composing Types

Build complex schemas from simpler ones:

let
  Age = Int(a | a >= 0),
  Person = { name: String, age: Age }
in { name: 'Alice', age: '30' } |> Person

Named types can reference other named types for reusable schemas.

Think Finitio: Elo's type definitions are inspired by Finitio. They validate and transform external data in one step—perfect for API responses and user input.

Practice: Validate Data

9. Guards

Type definitions validate data structure. Guards go further—they validate conditions and make your assumptions explicit. Use guards to reason confidently about your code.

Note: Guards compile to JavaScript and Ruby only. SQL is not supported.

Simple Guards

A guard checks a condition before evaluating its body:

guard 10 > 0 in 10 * 2

If the condition is false, an error is thrown. If true, the body is evaluated.

Labeled Guards

Add labels to make error messages meaningful:

guard positive: 5 > 0 in 5 * 2

Labels can be identifiers or string messages:

guard 'value must be positive': 5 > 0 in 5 * 2

Multiple Guards

Separate multiple conditions with commas:

guard
  positive: _.age > 0,
  adult: _.age >= 18
in
  'Welcome!'

Each condition is checked in order. The first failure stops execution.

Guard with Let

Guards combine naturally with let bindings:

let x = 10 guard x > 0 in x * 2

This is sugar for: let x = 10 in guard x > 0 in x * 2

Check (Postconditions)

check is a synonym for guard, used idiomatically for postconditions:

let result = 5 * 4 check result > 0 in result

Pipe-Style Guards

Use guards in pipelines with the predicate binding syntax:

10 |> guard(x | x > 0)

The value is bound to x, checked, and returned if valid. Chain with other operations:

let double = fn(n ~> n * 2) in
  10 |> guard(x | x > 0) |> double

Why guards matter: Guards make your assumptions visible. Instead of hoping data is valid, you state exactly what you expect. When something fails, the labeled error tells you exactly which assumption was violated.

Practice: Guard Input

Exercises

Practice what you've learned! Each exercise uses assert(condition) to check your answer. If the condition is true, the assertion passes. Replace the ??? placeholders with your code, then click "Check" to verify.

Build a Greeting

Chapter 2

Use string concatenation to build the greeting "Hello, World!".

Product Total

Chapter 3

Access the price and quantity fields from the tuple to compute the total (should be 100).

Text Transform

Chapter 4

Use stdlib functions to transform " hello " into "HELLO" (trim whitespace, then uppercase).

Rectangle Area

Chapter 5

Use let bindings to store width (8) and height (5), then compute the area.

Filter and Double

Chapter 6

Keep only numbers greater than 10, then double each remaining value.

Order Total

Chapter 7

Given input data with price and quantity, compute the total.

Validate Data

Chapter 8

Define a type that validates a product with a name (String) and price (Int), then apply it to coerce the price from a string.

Guard Input

Chapter 9

Write a guard that checks the value is positive (greater than 0), then doubles it. The assertion should pass.

Real-World Use Cases

Now that you've learned the fundamentals, let's see how Elo handles real-world scenarios. These examples demonstrate practical applications that combine multiple features you've learned.

Form Validation

APIs receive data that needs validation and normalization. This example validates a user registration payload, ensuring fields have the right types and constraints.

let
  Email = String(e | contains(e, '@') and length(e) >= 5),
  Age = Int(a | a >= 13 and a <= 120),
  Username = String(u | length(u) >= 3 and length(u) <= 20),
  Registration = {
    username: Username,
    email: Email,
    age: Age,
    newsletter :? Bool
  }
in
{
  username: 'alice',
  email: 'alice@example.com',
  age: '25',
  newsletter: 'true'
} |> Registration

The type definition validates structure, coerces age from string to integer, parses newsletter as boolean, and enforces constraints on each field. If any validation fails, an error is thrown immediately.

Note: Type definitions compile to JavaScript and Ruby only.

Order Processing

E-commerce systems process orders to calculate totals with discounts. This example shows how to transform order data using pipes and list operations.

let
  order = {
    items: [
      { name: 'Widget', price: 25, quantity: 2 },
      { name: 'Gadget', price: 50, quantity: 1 },
      { name: 'Thing', price: 10, quantity: 5 }
    ],
    discountPercent: 10
  },
  subtotal = (order.items
    |> map(i ~> i.price * i.quantity)
    |> reduce(0, fn(sum, x ~> sum + x))),
  discount = subtotal * order.discountPercent / 100,
  total = subtotal - discount
in
{ subtotal: subtotal, discount: discount, total: total }

Data flows naturally through the pipeline: compute line totals, sum them, apply discount, return result. The same expression runs in the browser (JS), server (Ruby), or as part of a larger SQL query.

Subscription Logic

SaaS applications need to check subscription status, trial periods, and renewal dates. Elo's date arithmetic makes this business logic clear and portable.

let
  subscription = {
    plan: 'pro',
    startDate: D2024-01-15,
    trialDays: 30,
    billingCycle: P30D
  },
  trialEnd = subscription.startDate + P1D * subscription.trialDays,
  isTrialActive = TODAY <= trialEnd,
  nextBilling = if isTrialActive
    then trialEnd
    else subscription.startDate + subscription.billingCycle,
  daysUntilBilling = if nextBilling > TODAY
    then (nextBilling - TODAY) / P1D
    else 0
in
{
  isTrialActive: isTrialActive,
  trialEnd: trialEnd,
  nextBilling: nextBilling,
  daysUntilBilling: daysUntilBilling
}

Date operations like +, -, and comparisons work naturally. Durations can be scaled and divided. The logic remains readable regardless of which runtime executes it.

Write once, run anywhere: This same subscription logic works in browser validation (JS), server-side processing (Ruby), or database queries (SQL).

What's Next?

You've learned the core of Elo and practiced with exercises. Now explore further:

Remember: Elo compiles to JavaScript, Ruby, and SQL. Write once, run anywhere.
Happy coding!