Basics
March is expression-oriented: almost everything returns a value. This chapter covers the building blocks you’ll use in every program.
Functions
fn name(params) do ... end defines a named function. The last expression in the body is the return value — no return keyword.
fn add(x : Int, y : Int) : Int do
x + y
end
Type annotations are optional everywhere — the compiler infers them:
fn add(x, y) do x + y end
Private functions
pfn makes a function private to its module. The compiler rejects calls from outside:
mod Auth do
pfn hash(s : String) : String do
Crypto.sha256(s)
end
fn verify(plain, stored) do
hash(plain) == stored
end
end
Let bindings
let binds a name. No in keyword — subsequent expressions in the block see the binding automatically:
fn run() do
let x = 10
let y = x * 2
y + 1
end
Type annotation is optional:
let count : Int = 0
Algebraic data types
type defines a sum type. Constructors are capitalized; no leading | on the first case:
type Shape =
Circle(Float)
| Rect(Float, Float)
| Point
Create values by applying constructors:
let c = Circle(3.14)
let r = Rect(4.0, 6.0)
Record types use named fields:
type User = { name : String, age : Int }
let u = { name: "Alice", age: 30 }
let name = u.name
let older = { u with age: 31 }
Pattern matching
match inspects a value and dispatches on its shape. The compiler rejects non-exhaustive patterns:
fn area(s : Shape) : Float do
match s do
Circle(r) -> 3.14159 *. r *. r
Rect(w, h) -> w *. h
Point -> 0.0
end
end
Guards narrow a pattern with a when condition:
match n do
x when x > 100 -> "big"
x when x > 0 -> "small"
_ -> "non-positive"
end
match do without a subject is a cond — works like chained if/else:
match do
score >= 90 -> "A"
score >= 80 -> "B"
_ -> "C"
end
Result propagation with let?
let? p = e binds the Ok payload and short-circuits on Err, returning the error from the enclosing function immediately:
fn load_config(path : String) : Result(Config, String) do
let? src = File.read(path)
let? toml = Toml.parse(src)
Ok(toml)
end
Without let? this is a nested match pyramid. With it, the happy path reads top-to-bottom. The RHS must be a Result; all let? in one block must share the same error type; let? cannot be the last expression in a block.
Modules
Every file has exactly one top-level mod. Nest modules with mod Inner do ... end:
mod MyApp do
mod Utils do
fn clamp(n, lo, hi) do
match do
n < lo -> lo
n > hi -> hi
_ -> n
end
end
end
fn main() do
println(int_to_string(Utils.clamp(42, 0, 10)))
end
end
Complete example: a small calculator
mod Calc do
type Expr =
Num(Float)
| Plus(Expr, Expr)
| Times(Expr, Expr)
| Slash(Expr, Expr)
fn eval(e : Expr) : Result(Float, String) do
match e do
Num(n) -> Ok(n)
Plus(a, b) ->
let? x = eval(a)
let? y = eval(b)
Ok(x +. y)
Times(a, b) ->
let? x = eval(a)
let? y = eval(b)
Ok(x *. y)
Slash(a, b) ->
let? x = eval(a)
let? y = eval(b)
if y == 0.0 do
Err("division by zero")
else
Ok(x /. y)
end
end
end
fn main() do
let expr = Slash(Plus(Num(10.0), Num(2.0)), Num(3.0))
match eval(expr) do
Ok(v) -> println("result: " ++ float_to_string(v))
Err(e) -> println("error: " ++ e)
end
end
end
HashMap — key-value store without a comparator
HashMap(k, v) is like Map but uses structural == and the built-in hash
function instead of an explicit comparator. This means you never pass a cmp
argument anywhere.
Building and querying
let m = HashMap.from_list([("a", 1), ("b", 2), ("c", 3)])
HashMap.get(m, "a") -- Some(1)
HashMap.get_or(m, "z", 0) -- 0
HashMap.has(m, "b") -- true
HashMap.size(m) -- 3
Counting word frequencies (O(n))
The update function eliminates the get-then-put pattern that Map requires:
mod WordCount do
fn count_words(words : List(String)) : HashMap(String, Int) do
List.fold_left(words, HashMap.new(), fn (acc, w) ->
HashMap.update(acc, w, fn opt ->
match opt do
None -> 1
Some(n) -> n + 1
end
)
)
end
fn main() do
let words = ["the", "cat", "sat", "on", "the", "mat", "the"]
let freq = count_words(words)
println(int_to_string(HashMap.get_or(freq, "the", 0))) -- 3
end
end
The same pattern is why Enum.frequencies and Enum.uniq are now O(n)
instead of O(n²) — they use HashMap internally.
Merging two maps
let a = HashMap.from_list([("x", 1), ("y", 2)])
let b = HashMap.from_list([("y", 99), ("z", 3)])
-- b wins on conflict:
let merged = HashMap.merge(a, b)
HashMap.get(merged, "y") -- Some(99)
-- custom conflict resolution:
let summed = HashMap.merge_with(a, b, fn va -> fn vb -> va + vb)
HashMap.get(summed, "y") -- Some(101)
When to use HashMap vs Map
| Situation | Use |
|---|---|
| String/int keys, no ordering needed | HashMap |
Need sorted iteration (to_list in key order) |
Map with str_cmp/int_cmp |
Custom equality beyond == |
Map with a comparator |
| Enum.frequencies / Enum.uniq patterns | HashMap (done internally) |