SagaSaga
Guide

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 + y

Let 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 # ok

For 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:

TypeExamplesNotes
Int0, 42, -7Arbitrary precision integer
Float3.14, -0.564-bit IEEE 754
String"hello", ""UTF-8
BoolTrue, 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     # False

Logic

True && False   # False
True || False   # True

Negation

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 expression

Strings

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 holes

Any 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 newline

Blocks

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 n

For 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"