Data Types

data declares a new type. Building a value and calling a function are syntactically indistinguishable — a type name is just a callable value.

Defining a type

data Point {
  x = 0
  y = 0
}

Fields are listed inside the braces. A field may have a default value; fields without a default are None unless given an argument.

Default values must be literals

Field defaults are evaluated at construction, not definition, and must be literal values — Int, Float, String, Bool, None, or a list of those. An identifier or call is rejected.

data P { xs = [1, "a", 2.5] }   // ok
data Q { x = base }              // error: default must be a literal

Construction

Call the type name with arguments to build an instance. Arguments fill fields positionally; the remaining fields use their defaults.

data Point {
  x = 0
  y = 0
}

const A = Point(1, 2)   // x=1, y=2
const B = Point(1)      // x=1, y=0
const C = Point()       // x=0, y=0

Passing more arguments than there are fields is a runtime error.

Field access

Read and write fields with the dot operator. Writing an unknown field is an error.

const P = Point(1, 2)
P.x        // 1
P.x = 5    // field x is now 5

Methods

A receiver method is declared with a typed receiver. It is dispatched on the instance, not bound as a global name, and the receiver is mutable inside the body.

data Point {
  x = 0
}

fun (p Point) add(v) { p.x + v }

const P = Point(1)
P.add(4)   // 5

Two types may define a method of the same name; each is dispatched by the receiver’s type.

A method name is not a global function: add(4) would fail with an undefined variable.

Names must be unique

A type name lives in the same namespace as functions and constants. Redefining, or naming a type after a function, is an error.

data P { x }
fun P() { 1 }   // error: already defined variable: P