Coming from Haskell / Elixir / OCaml
March sits in the ML/Elixir family. Most concepts map directly — the main differences are syntax, the actor model, and the absence of laziness and typeclasses.
Syntax cheatsheet
| Concept | Haskell | Elixir | OCaml | March |
|---|---|---|---|---|
| Module | module Foo where |
defmodule Foo do |
module Foo = struct |
mod Foo do |
| Function | f x = x + 1 |
def f(x), do: x + 1 |
let f x = x + 1 |
fn f(x) do x + 1 end |
| Lambda | \x -> x + 1 |
fn x -> x + 1 end |
fun x -> x + 1 |
fn x -> x + 1 |
| Pattern match | case x of |
case x do |
match x with |
match x do |
| ADT | data Shape = Circle Float \| Rect Float Float |
{:circle, r} \| {:rect, w, h} |
type shape = Circle of float \| Rect of float * float |
type Shape = Circle(Float) \| Rect(Float, Float) |
| Type params | data Maybe a = Nothing \| Just a |
— | type 'a option = None \| Some of 'a |
type Option(a) = None \| Some(a) |
| Result | Either e a |
{:ok, v} \| {:error, e} |
('a, 'e) result |
Result(a, e) |
| Let binding | let x = 42 in ... |
x = 42 |
let x = 42 in ... |
let x = 42 (no in) |
| Pipe | f $ g x |
x \|> g \|> f |
— | g(x) \|> f -- or: f(g(x)) |
| Record update | r { field = v } |
%{r \| field: v} |
{ r with field = v } |
{ r with field: v } |
| Private fn | module boundary | defp |
let (vs let ... in sig) |
pfn |
Key differences
No typeclasses / no protocols by name — use interface/impl
interface Eq(a) do
fn eq(x : a, y : a) : Bool
end
impl Eq(Int) do
fn eq(x, y) do x == y end
end
The coherence rules are the same as Haskell’s — one impl per type per interface per program.
let? instead of do-notation / >>=
Haskell’s do-notation for Maybe/Either:
run s = do
n <- parseInt s
user <- fetchUser n
return (score user)
Becomes let? in March:
fn run(s : String) : Result(Int, String) do
let? n = parse_int(s)
let? user = fetch_user(n)
Ok(user.score)
end
let? is not a monad — it’s a first-class desugaring specific to Result. Option and custom types don’t get it (yet).
Actors instead of OTP GenServers
March actors are structurally similar to Elixir GenServers but without the boilerplate:
actor Counter do
state { value : Int }
init { value: 0 }
on Inc(n : Int) do
{ value: state.value + n }
end
on Get() do
println(int_to_string(state.value))
state
end
end
spawn(Counter) returns a pid. send(pid, Inc(5)) sends a message. No handle_cast/handle_call distinction — all messages are async by default.
No laziness
March is strict. List is a linked list, not a lazy sequence. For streaming/lazy evaluation use Seq (the lazy sequence module) or Flow (concurrent lazy streams).
Perceus RC instead of GC
No garbage collector — reference counting with in-place reuse (FBIP). Deterministic latency, no stop-the-world pauses. The linear type and always_linear type features give you Rust-style ownership at compile time when you need it.
Concurrency & shared state
If you’re coming from Haskell, mutable references (IORef, MVar, TVar) and
async/Async map onto March’s two concurrency tools. If you’re coming from
Elixir, you’ll find the model familiar — actors are GenServers.
| Haskell / Elixir / OCaml | March |
|---|---|
IORef / MVar / TVar (mutable cell) |
an actor’s state { … } |
Control.Concurrent.Async / async |
Task.async(fn () -> …) + Task.await |
mapConcurrently / Async.Parallel |
List.pmap or Task.await_many |
Elixir GenServer (handle_call/cast) |
actor with on Msg do … end handlers |
| Elixir supervision tree | Supervision (one_for_one, …) |
bracket / with / withFile |
a linear resource — the compiler checks it’s released, no runtime wrapper |
The last row is the interesting one: where Haskell’s bracket guarantees cleanup
at runtime via an exception handler, March’s linear types make
“this handle must be consumed exactly once” a compile-time property — forget to
close it and the program doesn’t build. For streaming with backpressure (GenStage
territory), see Flow.
What maps cleanly
| Haskell / OCaml | March |
|---|---|
Data.Map.lookup |
Map.get |
Data.List.filter |
List.filter |
foldl |
List.fold_left |
Maybe.fromMaybe |
Option.unwrap_or |
Data.Either.fromRight |
Result.unwrap |
Data.List.sortBy |
List.sort_by |
concatMap |
List.flat_map |
Data.Map.insertWith |
Map.merge_with |
IORef |
Vault (shared store) or actor state |
STM |
actor + Channel |