Functions
Functions are the primary building block in Saga. Everything is a function or a value computed from one.
Defining Functions
A function definition has two parts: an optional type annotation, and one or more equations that provide the implementation.
# Annotated (required for pub)
pub fun add : Int -> Int -> Int
add x y = x + y
# Private with annotation (optional)
fun double : Int -> Int
double x = x * 2
# Private, fully inferred
triple x = x * 3The annotation and the equations are separate declarations. The annotation describes the type; the equation gives the body. For public functions the annotation is required, both to enforce a stable API and as documentation for anyone reading the code.
Calling Functions: Currying and Partial Application
If you're coming from most other languages, Saga's call syntax will look unusual at first. There are no parentheses around argument lists and no commas between arguments:
add 1 2 # not add(1, 2)
clamp n 0 100 # not clamp(n, 0, 100)This is because all functions in Saga are curried. A function that takes two
arguments is actually a function that takes one argument and returns another
function that takes the second. add 1 2 means "apply add to 1, getting
back a function, then apply that to 2."
The practical payoff is partial application: you can apply fewer arguments than a function expects, and get a new function back:
pub fun add : Int -> Int -> Int
add x y = x + y
increment = add 1 # type: Int -> Int
increment 3 # 4This composes naturally with higher-order functions. Rather than writing a lambda, partially apply:
# These are equivalent
List.map (fun x -> x + 1) [1, 2, 3]
List.map (add 1) [1, 2, 3]One thing to watch: parentheses are still used to group expressions, just not
for argument lists. So f (g x) means "apply f to the result of g x", not
a comma-separated argument list.
Multiple Equations and Guards
A function can have multiple equations, matched top to bottom:
fun fib : Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)Guards add a condition to an equation using when. The first equation whose
pattern and guard both match is used:
fizzbuzz n when n % 15 == 0 = "FizzBuzz"
fizzbuzz n when n % 3 == 0 = "Fizz"
fizzbuzz n when n % 5 == 0 = "Buzz"
fizzbuzz n = show nThe compiler checks that the equations are exhaustive. Patterns and guards are covered in full in Pattern Matching.
Zero-Arity Functions
A function can take no arguments at all. A definition with no parameters is a zero-arity function, and its body runs every time the name is referenced:
fun answer : Int
answer = 42
config_path = "/etc/app.conf" # inferred, zero-arityThis is how module-level constants are written (see Basics). For a pure value, "evaluated on every reference" is exactly what you want. But it has teeth when the body is expensive or effectful, because the work happens at each mention rather than once:
# Runs the query EVERY time `all_users` is referenced — probably not intended
fun all_users : List User needs {Database}
all_users = query! "SELECT * FROM users"When you want the work to happen once, or not until you ask for it, you have two options:
-
Bind it with
let. The right-hand side is evaluated a single time and the result is bound to a name:let users = all_users # query runs once, here process users process users # reuses the same list, no second query -
Keep it a thunk. Give the function a
Unitparameter (written()) so the name is just a value you can pass around, with the body running only when you apply():fun load_users : Unit -> List User needs {Database} load_users () = query! "SELECT * FROM users" let later = load_users # nothing has run yet let users = load_users () # query runs here
A Unit parameter is the idiomatic way to delay a computation. It's why
callbacks passed to handlers, async!, and the test framework all take
Unit -> a: the wrapper lets them hand a computation around without triggering
it until the handler decides to.
Parameter Labels
Type annotations can include parameter labels. These are documentation only and have no effect on how the function is called:
pub fun clamp : (value: Int) -> (min: Int) -> (max: Int) -> Int
clamp value min max = ...The label appears in hover tooltips and docs but the call site is still
clamp x 0 100.
Lambdas
Anonymous functions use fun:
fun x -> x + 1
fun x y -> x + yLambdas are most commonly passed to higher-order functions:
List.map (fun x -> x * 2) [1, 2, 3]
List.filter (fun x -> x % 2 == 0) [1, 2, 3, 4]Functions as Values
Functions are first-class values. You can assign them to let bindings and
pass them around like any other value:
let double = fun x -> x * 2
let apply = fun f x -> f x
apply double 5 # 10Named functions work the same way -- the name is just a reference to the function value:
let transform = double # transform is now the same function as double
transform 4 # 8This is what makes partial application and higher-order functions work: add 1
is just a value of type Int -> Int, and you can store it, pass it, or call it
whenever you like.
Pipes
The pipe operator |> threads a value through a chain of functions. The value
on the left becomes the last argument to the function on the right.
Instead of:
dbg (show (add 1 2))Write:
add 1 2 |> show |> dbgPipes read left to right and eliminate deeply nested parentheses. They work especially well with curried functions, since data can flow through a pipeline of partially-applied steps:
[1, 2, 3, 4, 5]
|> List.filter (fun x -> x % 2 == 0)
|> List.map (fun x -> x * 10)
|> List.foldl (fun acc x -> acc + x) 0
|> dbgThe backward pipe <| binds in the other direction, which can avoid
parentheses when the argument is a complex expression:
dbg <| add 1 2
# equivalent to: dbg (add 1 2)Function Composition
>> composes two functions into one. f >> g means "apply f, then apply g to
the result":
let process = parse >> validate >> save
data |> process
# same as: data |> parse |> validate |> saveComposition is useful for building reusable pipelines from smaller pieces, especially when you want to pass the pipeline itself as a value rather than applying it immediately.