Basics
Saga's syntax draws from ML-family languages like Haskell, Elm, and OCaml, with curly braces for blocks. If you've used any of those, or even TypeScript, much of this will feel familiar.
Comments
Line comments start with #. Doc comments use #@ and attach to the next
definition.
# This is a comment
#@ This documents the function below
pub fun add : Int -> Int -> Int
add x y = x + yLet Bindings
Inside a function or block, let binds a name to a value:
main () = {
let x = 42
let name = "Saga"
dbg name
}Bindings are immutable and cannot be modified once defined, however it is possible to shadow and reuse the binding:
let x = 5
x = 6 # error!
let x = 6 # okFor situations where mutability is truly needed (e.g. for performance), it is possible to use the Ref Effect with a BEAM handler to access the mutable process dictionary or ETS table.
let can also destructure patterns inline:
let (x, y) = compute_pair ()
let Point { x, y } = get_point ()Pattern destructuring is covered in depth in Pattern Matching.
Primitive Types
Saga has five primitive types:
| Type | Examples | Notes |
|---|---|---|
Int | 0, 42, -7 | Arbitrary precision integer |
Float | 3.14, -0.5 | 64-bit IEEE 754 |
String | "hello", "" | UTF-8 |
Bool | True, False | |
Unit | () | The "nothing useful" type |
Unit is a special "nothing" type, used with the constructor (). For functions with no inputs or that return no data, the Unit type can be used.
Other types, such as lists, mutable vectors, and bitstrings can be found in the standard library.
Operators
Arithmetic
1 + 2 # 3
10 - 3 # 7
4 * 5 # 20
7 / 2 # 3 (integer division truncates)
7 % 2 # 1 (modulo, Int only)For floats, division works as expected: 7.0 / 2.0 is 3.5.
Comparison
1 == 1 # True
1 != 2 # True
3 < 5 # True
3 > 5 # False
4 <= 4 # True
4 >= 5 # FalseLogic
True && False # False
True || False # TrueNegation
Negative literals require parentheses when passed as function arguments:
abs (-5) # fine
abs -5 # parse error: looks like binary minus
-x # fine as a standalone expressionStrings
Concatenate strings with <>:
"Hello, " <> "world" # "Hello, world"Interpolation
Prefix a string with $ to enable interpolation. Use {expr} for holes:
let name = "Dylan"
$"Hello, {name}!" # "Hello, Dylan!"
$"Items: {xs |> List.length}" # pipes work inside holesAny expression works inside a hole. Non-string values are converted
automatically via the show function:
let count = 42
let ratio = 0.75
$"count: {count}, ratio: {ratio}" # "count: 42, ratio: 0.75"To include a literal brace, escape it:
$"Show \{ literal brace"Multiline strings
Triple-quoted strings allow literal newlines. Indentation is stripped based on
the column of the closing """:
let sql = """
SELECT *
FROM users
WHERE age > 30
"""
# "SELECT *\nFROM users\nWHERE age > 30"Interpolation works in multiline strings too with $"""...""".
Raw strings
Prefix with @ to disable escape processing:
@"hello\nworld" # literal backslash-n, not a newlineBlocks
A block is a sequence of expressions inside { }. The value of the block is
its last expression:
compute x = {
let a = x * 2
let b = a + 1
b # this is the return value
}Blocks appear in function bodies, if branches, case arms, and handler
bodies. Anywhere you need to sequence multiple steps before producing a value,
use a block.
If / Then / Else
if is an expression, not a statement. Both branches must return the same type:
abs n = if n < 0 then -n else nFor multi-step branches, use a block:
describe n = if n > 0 then {
let label = "positive"
label <> ": " <> show n
} else {
"non-positive"
}Top-Level Definitions
Module-level named values are just functions with no arguments. Unlike let,
which lives inside a function body, these are declared at the top level of a
module:
pi = 3.14159
app_name = "my-app"
max_retries = 5
origins = ["localhost", "example.com"]A name like pi is a zero-arity function: referencing it evaluates its body.
For a plain constant that's exactly what you want. But because every reference
re-runs the body, a zero-arity definition whose body is expensive or performs
effects runs that work on every reference — see
Functions for when to reach for a let
binding or a thunk instead.
To export a top-level definition, mark it pub. As with any public function, a
pub definition needs a type annotation:
pub fun version : String
version = "1.0.0"