Expressions and Pipelines
In this chapter you will learn:
- The primitive value types in eucalypt
- How function application works via catenation (pipelining)
- How partial application and currying work
- How to compose pipelines of transformations
Primitive Values
Eucalypt has the following primitive types:
| Type | Examples | Notes |
|---|---|---|
| Numbers | 42, -7, 3.14 | Integers and floats |
| Strings | "hello", "it's" | Double-quoted only |
| Symbols | :name, :active | Colon-prefixed identifiers |
| Booleans | true, false | |
| Null | null | Renders as YAML ~ or JSON null |
Lists
Lists are comma-separated values in square brackets (unlike in blocks, commas are required):
numbers: [1, 2, 3, 4, 5]
mixed: [1, "two", :three, true]
nested: [[1, 2], [3, 4]]
empty: []
Calling Functions
Functions can be called by placing arguments in parentheses directly after the function name (with no intervening space):
add(x, y): x + y
result: add(3, 4)
result: 7
Catenation: The Pipeline Style
One distinctive feature of eucalypt is catenation: applying a function by writing the argument before the function name, separated by whitespace.
result: 5 inc
This is equivalent to inc(5) and produces 6.
Catenation lets you chain operations into readable pipelines:
eu -e '[1, 2, 3, 4, 5] reverse head'
5
Each step in the pipeline passes its result to the next function. You can read it left to right: "take the list, reverse it, take the head."
Combining Catenation with Arguments
When a function takes multiple arguments, you can supply some in parentheses and the rest via catenation. The catenated value becomes the last argument:
result: [1, 2, 3] map(inc)
Here map takes two arguments: a function and a list. inc is
provided in parentheses and [1, 2, 3] is provided by catenation.
The result is [2, 3, 4].
This is the standard eucalypt pattern for data processing pipelines:
eu -e '[1, 2, 3, 4, 5] filter(> 3) map(* 10)'
- 40
- 50
Currying and Partial Application
All functions in eucalypt are curried: if you provide fewer arguments than a function expects, you get back a partially applied function.
add(x, y): x + y
add-five: add(5)
result: add-five(3)
result: 8
Curried application also works with multi-argument calls:
f(x, y, z): x + y + z
a: f(1, 2, 3) # all at once
b: f(1)(2)(3) # one at a time
c: f(1, 2)(3) # mixed
All three produce 6.
Lookup: The Dot Operator
The dot operator (.) accesses a named property within a block:
person: { name: "Alice" age: 30 }
name: person.name
person:
name: Alice
age: 30
name: Alice
Lookups can be chained:
config: { db: { host: "localhost" port: 5432 } }
host: config.db.host
config:
db:
host: localhost
port: 5432
host: localhost
Warning: The dot operator binds very tightly (precedence 90). Writing
list head.nameis parsed aslist (head.name), not(list head).name. Use explicit parentheses when combining lookup with catenation:(list head).name.The
↑(up arrow) prefix operator, which is shorthand forhead, binds even tighter (precedence 95). So↑xs.namemeans(↑xs).name.
"Juxtaposed" call syntax
When a function is passed only a single list argument or a single block argument, it is possible to omit the outer parentheses for brevity:
result: f[1, 2, 3] ∧ g{a: 1 b: 2}
The "juxtaposition" refers to the resulting feature that directly placing a function together with any form of brackets (with no intervening whitespace) is now call syntax - whereas using an intervening space is a pipeline syntax.
Juxtaposed call syntax: f(x), f[x], f{x}.
Pipeline syntax: x f, [x] f, {x} f.
Generalised Lookup (or "block-dot" notation)
Lookup can be generalised: any expression after the dot is evaluated in the context of the block to the left.
point: { x: 3 y: 4 }
sum: point.(x + y)
pair: point.[x, y]
label: point."{x},{y}"
point:
x: 3
y: 4
sum: 7
pair:
- 3
- 4
label: 3,4
This is particularly useful for creating temporary scopes:
result: { a: 10 b: 20 }.(a * b)
result: 200
"Block-dot" syntax is particularly significant in monadic blocks (see Monads and the monad() Utility).
Building Pipelines
Combining catenation, partial application, and the standard prelude creates powerful data processing pipelines:
eu -e '["alice", "bob", "charlie"] map(str.to-upper) filter(str.matches?("^[AB]"))'
- ALICE
- BOB
A more complete example:
people: [
{ name: "Alice" age: 30 }
{ name: "Bob" age: 25 }
{ name: "Charlie" age: 35 }
]
over-thirty: people filter(.age > 30) map(.name)
people:
- name: Alice
age: 30
- name: Bob
age: 25
- name: Charlie
age: 35
over-thirty:
- Charlie
The then Function
The then function provides a pipeline-friendly conditional:
eu -e '5 > 3 then("yes", "no")'
yes
It is equivalent to if with the condition as the last argument,
making it natural in pipelines:
result: [1, 2, 3] count (> 2) then("many", "few")
result: many
Key Concepts
- Catenation applies a function by writing the argument before the
function name:
5 incmeansinc(5) - Pipelines are built by chaining catenation:
data f g h - Functions are curried: partial application is automatic
- The dot operator looks up properties:
block.key - Generalised lookup evaluates expressions in a block's scope:
block.(expr) - Combine these techniques for concise data processing pipelines