Type System

March uses Hindley-Milner type inference with bidirectional checking at function boundaries. You get the convenience of inferred types with the safety of static checking.


Primitive Types

Type Description Literals
Int 64-bit signed integer 42, -7, 0
Float 64-bit IEEE 754 3.14, -0.5, 1.0e10
Bool Boolean true, false
String UTF-8 string "hello"
Char Unicode scalar value (accessed via String operations)
() Unit (no value) ()

Type Annotations

Annotations are optional everywhere except:

  • Recursive functions where inference would loop
  • When you want explicit documentation
fn add(x : Int, y : Int) : Int do
  x + y
end

-- Equally valid  fully inferred:
fn add(x, y) do x + y end

Parameter and return annotations use ::

let count : Int = 0
fn process(data : List(String)) : Option(Int) do ... end

Algebraic Data Types (ADTs)

Sum Types (Variants)

Variants declare a type with multiple possible shapes. No leading | on the first case:

type Color = Red | Green | Blue

type Shape =
  | Circle(Float)
  | Rect(Float, Float)
  | Triangle(Float, Float, Float)

Constructors are capitalized. They can carry zero or more fields:

type Expr =
  | Num(Int)
  | Add(Expr, Expr)
  | Mul(Expr, Expr)
  | Neg(Expr)

Use constructors by applying them like functions:

let c = Circle(3.14)
let r = Rect(4.0, 6.0)
let e = Add(Num(1), Mul(Num(2), Num(3)))

Record Types

Records have named fields:

type Point = { x : Float, y : Float }
type User  = { name : String, age : Int, email : String }

Create, access, and update:

let p = { x = 1.0, y = 2.0 }
let moved = { p with x = 5.0 }
let dist = p.x +. p.y

Records and variants can be combined — a variant constructor can carry a record:

type Config =
  | Default
  | Custom({ host : String, port : Int, debug : Bool })

Type Parameters (Generics)

Type parameters are lowercase:

type Option(a) = None | Some(a)
type Result(a, e) = Ok(a) | Err(e)
type Pair(a, b) = Pair(a, b)
type Tree(a) = Leaf | Node(Tree(a), a, Tree(a))

Use the same lowercase letters in function signatures to refer to type parameters:

fn identity(x : a) : a do x end

fn map_option(opt : Option(a), f : a -> b) : Option(b) do
  match opt do
    None    -> None
    Some(x) -> Some(f(x))
  end
end

The compiler infers type parameter instantiations at call sites:

map_option(Some(42), fn x -> x * 2)  -- Option(Int)
map_option(Some("hi"), String.length) -- Option(Int)

Type Aliases

Give a type a shorter name:

type Name = String
type Age  = Int
type DB   = Map(String, List(Int))

Type aliases are structural — Name and String are interchangeable.


Option(a)

Option(a) represents a value that may or may not be present:

type Option(a) = None | Some(a)

Standard pattern:

fn safe_head(xs : List(a)) : Option(a) do
  match xs do
    Nil        -> None
    Cons(h, _) -> Some(h)
  end
end

Stdlib helpers (from prelude, always in scope):

unwrap(Some(42))           -- 42 (panics if None)
unwrap_or(None, 0)         -- 0

From Option module:

Option.map(Some(5), fn x -> x + 1)  -- Some(6)
Option.and_then(opt, fn x -> ...)   -- flatMap
Option.unwrap_or_else(opt, fn () -> compute_default())
Option.is_some(opt)
Option.is_none(opt)

Result(a, e)

Result(a, e) represents either success or failure:

type Result(a, e) = Ok(a) | Err(e)

Functions that can fail return Result:

fn parse_int(s : String) : Result(Int, String) do
  -- returns Ok(n) or Err("not a valid integer")
  parse_int_builtin(s)
end

Chain with with:

with Ok(n)    <- parse_int(input),
     Ok(user) <- fetch_user(n) do
  display(user)
else
  Err(e) -> println("Error: " ++ e)
end

Stdlib helpers:

Result.map(Ok(5), fn x -> x + 1)     -- Ok(6)
Result.map_err(Err("x"), String.upcase)
Result.and_then(res, fn v -> ...)      -- flatMap
Result.unwrap(Ok(42))                  -- 42
Result.unwrap_or(Err("e"), 0)          -- 0
Result.is_ok(res)
Result.is_err(res)

Tuples

Tuples are anonymous ordered products:

let pair : (Int, String) = (1, "hello")
let triple : (Int, Float, Bool) = (1, 2.0, true)
let unit : () = ()

Destructure with let or pattern matching:

let (a, b) = pair
match triple do
  (n, f, b) -> ...
end

Lists

List(a) is a singly-linked cons list:

type List(a) = Nil | Cons(a, List(a))

List literals desugar to Cons chains:

[1, 2, 3]   -- Cons(1, Cons(2, Cons(3, Nil)))
[]          -- Nil

Function Types

Function types are written with ->, right-associative:

Int -> Bool          -- takes Int, returns Bool
Int -> Int -> Int    -- curried: takes Int, returns (Int -> Int)
(Int, Int) -> Int    -- takes a pair

Higher-order functions:

fn apply(f : Int -> Int, x : Int) : Int do f(x) end
fn compose(f : b -> c, g : a -> b) : a -> c do
  fn x -> f(g(x))
end

Qualified Types

Types from modules are accessed with .:

Http.Request
Map.Entry(String, Int)

Type-Level Naturals (Sized Arrays)

March supports Nat in type parameters for compile-time dimension checking:

type Vector(n, a) = Vector(Array(a))
type Matrix(m, n, a) = Matrix(Array(Array(a)))

Arithmetic on type-level naturals:

type Doubled(n, a) = Array(n * 2, a)

This enables functions like zip that guarantee equal-length inputs:

fn zip_vectors(v1 : Vector(n, a), v2 : Vector(n, b)) : Vector(n, (a, b)) do
  -- compiler verifies n is the same for both inputs
  ...
end

Opaque Types

Hide a type’s representation while keeping the name usable in signatures:

mod Token do
  opaque type Token = Token(String)

  fn make(s : String) : Token do Token(s) end
  fn value(t : Token) : String do
    match t do Token(s) -> s end
  end
end

Outside Token, callers can use Token as a type but cannot construct or pattern-match it directly — only through the module’s public API.

For completely hidden types, use ptype:

ptype Internal = Foo | Bar(Int)
-- Both the type name and constructors are private

The Type Hierarchy at a Glance

Types
├── Primitives: Int, Float, Bool, String, ()
├── Sum types: type Foo = A | B(T) | ...
├── Record types: type Foo = { field : T, ... }
├── Generic types: type Foo(a) = ...
├── Function types: T -> U
├── Tuple types: (T, U, V)
├── Linear/affine: linear T, affine T
└── Stdlib: List(a), Option(a), Result(a,e), Map(k,v), ...

Next Steps