Skip to main content

Types

Mochi is statically typed with type inference. The compiler tracks the type of every expression at compile time and rarely requires explicit annotations outside a public surface.

Primitive types

TypeExamplesNotes
int0, 1, -1, 0xff, 1_000_00064-bit signed by default.
float3.14, 0.5, 1e964-bit IEEE 754 double.
booltrue, falseNo implicit conversion to int.
string"abc", "héllo"UTF-8, immutable.
nilnilThe only value of type nil.
void(no values)The return type of functions that do not return a value.

All numeric literals can use underscores as digit separators (1_000_000, 0xff_ff). Hex literals (0x...) and binary literals (0b...) produce int.

The any type

any accepts any value but loses type information. Use any only at boundaries with untyped data such as loose JSON or dynamic dispatch. Idiomatic Mochi code rarely uses it.

let things: list<any> = [1, "two", 3.0]

To use a value of type any, narrow it with is or match:

fun describe(value: any): string {
match value {
n: int => return "int " + str(n),
s: string => return "str " + s,
_ => return "other"
}
}

Type inference

The compiler infers the type from the right-hand side of an initializer:

let count = 0 // int
let pi = 3.14 // float
let name = "Mochi" // string
let xs = [1, 2, 3] // list<int>
let user = {"name": "Ada"} // map<string, string>

Inference also flows through function calls: the compiler picks the most specific type that satisfies every constraint.

Add annotations when:

  • Inference is too narrow (e.g. an empty list literal: let xs: list<int> = []).
  • The binding should accept multiple variants (a union, see below).
  • The binding is part of a public API and a stable shape is desirable.

Struct types

type declares a struct with named, typed fields:

type Point {
x: float
y: float
}

let origin = Point { x: 0.0, y: 0.0 }
let p = Point { x: 3.0, y: 4.0 }

print(p.x, p.y) // 3 4

Field access uses the dot operator. Field assignment is allowed only on a mutable binding:

var pos = Point { x: 0.0, y: 0.0 }
pos.x = 3.0
pos.y = 4.0

Constructors require every field. Field order in the literal does not matter as long as names are present.

Inline methods

Methods are declared inside the type body. They access fields directly, without an explicit self parameter.

type Rect {
width: float
height: float

fun area(): float {
return width * height
}

fun scale(factor: float): Rect {
return Rect { width: width * factor, height: height * factor }
}
}

let r = Rect { width: 2.0, height: 3.0 }
print(r.area()) // 6
print(r.scale(2.0).area()) // 24

Methods can call each other and refer to other fields like any other expression.

Union types

A union represents a value that can take one of several shapes. Variants are separated by |:

type Shape =
Circle(radius: float)
| Square(side: float)
| Triangle(base: float, height: float)

Each variant is a constructor. The parameter list defines the data the variant carries. Construct values with the constructor name:

let s1 = Circle(2.0)
let s2 = Square(3.0)

Use match to deconstruct:

fun area(s: Shape): float {
return match s {
Circle(r) => 3.14159 * r * r,
Square(side) => side * side,
Triangle(b, h) => 0.5 * b * h
}
}

Variants without parameters are written without parentheses:

type Tree =
Leaf
| Node(value: int, left: Tree, right: Tree)

The exhaustiveness checker warns if you forget a variant in a match.

Optional and nullable values

The optional pattern is a union with nil:

fun find(xs: list<User>, id: int): User | nil {
for u in xs {
if u.id == id { return u }
}
return nil
}

To use a T | nil value, narrow it with match or with the ?. operator that short-circuits to nil:

let u = find(users, 7)
match u {
nil => print("not found"),
user => print(user.name)
}

let name = u?.name // string | nil

Mochi has no separate Option<T> wrapper. T | nil is the idiomatic form.

Type aliases

Use type with = to give a name to an existing type:

type UserId = int
type Name = string
type IntFn = fun(int): int

Aliases are interchangeable with the underlying type. The compiler does not enforce a distinction; aliases are for readability. For nominal types, wrap the value in a one-field struct:

type UserId { value: int }

Generic types

Functions and structs can take type parameters. Generics use angle brackets.

fun first<T>(xs: list<T>): T | nil {
if len(xs) == 0 { return nil }
return xs[0]
}

print(first([1, 2, 3])) // 1
print(first(["a", "b"])) // a

Structs can also be generic:

type Pair<A, B> {
left: A
right: B
}

let p = Pair<int, string> { left: 1, right: "one" }

The compiler infers type arguments at the call site when it can. Provide them explicitly with <...> when inference is ambiguous.

Collection types

TypeLiteralNotes
list<T>[1, 2, 3]Ordered, indexed, mutable in place if var.
map<K, V>{"a": 1, "b": 2}Hashed by key. Keys must be hashable.
set<T>{"a", "b"}Unordered, no duplicates.

These come from the prelude and have type-parameterized methods such as xs.push(value), m.keys(), and s.contains(value). See the built-ins reference for the full list.

Function types

Function types are written fun(<params>): <return>.

type Predicate = fun(int): bool

fun any(xs: list<int>, p: Predicate): bool {
for x in xs {
if p(x) { return true }
}
return false
}

print(any([1, 2, 3], fun(n: int): bool => n > 2)) // true

A function with no parameters and no return value is fun(): void.

Type narrowing

An is test or a match arm narrows the binding's type inside that branch:

fun length_of(value: int | string): int {
if value is string {
return len(value) // value is string here
}
return value // value is int here
}

is returns a bool. The compiler tracks the narrowing only inside the if body. After the if, the original type is restored.

The void type

void is the return type of functions that produce no value. A void value cannot be constructed or stored. Calling a void function in an expression position is a type error.

fun log(msg: string): void {
print(msg)
}

let x = log("hi") // error: cannot use a void value

Equality and ordering

Primitive types support ==, !=, and the comparison operators. Structs compare equal field by field. Lists, maps, and sets compare element by element.

let p1 = Point { x: 1.0, y: 2.0 }
let p2 = Point { x: 1.0, y: 2.0 }
print(p1 == p2) // true

let xs = [1, 2, 3]
let ys = [1, 2, 3]
print(xs == ys) // true

Function values compare by reference identity.

Reading a complex type

Type expressions are read inside-out:

ExpressionReads as
list<int>A list whose elements are int.
map<string, list<User>>A map from string to a list of User.
fun(int, int): intA function from two int to int.
User | nilEither a User or nil.
list<User | nil>A list whose elements are each either User or nil.

Common errors

MessageCauseFix
cannot infer type of empty list literallet xs = []Annotate: let xs: list<int> = [].
missing field <name>Struct literal incompleteProvide every required field.
unknown variant <Name>Constructor not part of the unionCheck spelling; add the variant to the type.
match is not exhaustivematch on a union missing a variantAdd a branch or _ => ... catch-all.

Next