This chapter covers how to define and use custom data types in NanoLang: structs for product types, enums for named constants, and unions for sum types.
7.1 Structs (Product Types)
Structs group multiple fields together into a single type. They're similar to records or objects in other languages.
Struct Definitions
struct Point2D {
x: int,
y: int
}
**Syntax:**
structkeyword- Type name (PascalCase)
- Fields with types
- Comma-separated (trailing comma optional)
Creating Struct Instances
fn create_point() -> Point2D {
return Point2D { x: 10, y: 20 }
}
shadow create_point {
let p: Point2D = (create_point)
assert (== p.x 10)
assert (== p.y 20)
}
**Syntax:** TypeName { field1: value1, field2: value2 }
Accessing Fields
Use dot notation to access struct fields:
struct Rectangle {
width: int,
height: int
}
fn area(r: Rectangle) -> int {
return (* r.width r.height)
}
shadow area {
let rect: Rectangle = Rectangle { width: 5, height: 10 }
assert (== (area rect) 50)
}
Examples
**Person struct:**
struct Person {
name: string,
age: int
}
fn greet_person(p: Person) -> string {
return (+ "Hello, " p.name)
}
shadow greet_person {
let alice: Person = Person { name: "Alice", age: 30 }
assert (== (greet_person alice) "Hello, Alice")
}
**Nested structs:**
struct Address {
street: string,
city: string
}
struct Contact {
name: string,
address: Address
}
fn get_city(c: Contact) -> string {
return c.address.city
}
shadow get_city {
let addr: Address = Address {
street: "123 Main St",
city: "Springfield"
}
let contact: Contact = Contact {
name: "Bob",
address: addr
}
assert (== (get_city contact) "Springfield")
}
**Struct with different types:**
struct Temperature {
value: float,
unit: string
}
fn to_celsius(t: Temperature) -> float {
if (== t.unit "F") {
return (* (- t.value 32.0) (/ 5.0 9.0))
}
return t.value
}
shadow to_celsius {
let temp_f: Temperature = Temperature { value: 212.0, unit: "F" }
let celsius: float = (to_celsius temp_f)
assert (and (> celsius 99.9) (< celsius 100.1))
}
7.2 Enums (Sum Types)
Enums define a type with a fixed set of named constants. Each constant has an associated integer value.
Enum Definitions
enum Status {
IDLE = 0,
RUNNING = 1,
DONE = 2
}
**Syntax:**
enumkeyword- Type name (PascalCase)
- Named constants (UPPER_CASE)
- Integer values (explicit)
Enum Values
Access enum values using dot notation:
fn get_initial_status() -> Status {
return Status.IDLE
}
shadow get_initial_status {
let s: Status = (get_initial_status)
assert (== s Status.IDLE)
}
Using Enums
Enums are great for representing states or categories:
enum Color {
RED = 0,
GREEN = 1,
BLUE = 2
}
fn color_name(c: Color) -> string {
if (== c Color.RED) {
return "Red"
}
if (== c Color.GREEN) {
return "Green"
}
return "Blue"
}
shadow color_name {
assert (== (color_name Color.RED) "Red")
assert (== (color_name Color.GREEN) "Green")
assert (== (color_name Color.BLUE) "Blue")
}
Examples
**HTTP status codes:**
enum HttpStatus {
OK = 200,
NOT_FOUND = 404,
SERVER_ERROR = 500
}
fn is_error(status: HttpStatus) -> bool {
return (>= status HttpStatus.NOT_FOUND)
}
shadow is_error {
assert (not (is_error HttpStatus.OK))
assert (is_error HttpStatus.NOT_FOUND)
assert (is_error HttpStatus.SERVER_ERROR)
}
**Direction enum:**
enum Direction {
NORTH = 0,
EAST = 1,
SOUTH = 2,
WEST = 3
}
fn opposite(d: Direction) -> Direction {
if (== d Direction.NORTH) { return Direction.SOUTH }
if (== d Direction.SOUTH) { return Direction.NORTH }
if (== d Direction.EAST) { return Direction.WEST }
return Direction.EAST
}
shadow opposite {
assert (== (opposite Direction.NORTH) Direction.SOUTH)
assert (== (opposite Direction.EAST) Direction.WEST)
}
7.3 Unions (Tagged Unions)
Unions represent a value that could be one of several variants. Each variant can have associated data.
Union Definitions
union Result {
Ok { value: int },
Err { error: string }
}
**Syntax:**
unionkeyword- Type name (PascalCase)
- Variants (PascalCase)
- Each variant can have fields
Union Constructors
Create union values using variant constructors:
fn success() -> Result {
return Result.Ok { value: 42 }
}
fn failure() -> Result {
return Result.Err { error: "Failed" }
}
shadow success {
let r: Result = (success)
# Use match to check variant
}
shadow failure {
let r: Result = (failure)
# Use match to check variant
}
Pattern Matching with Unions
Use match to handle different union variants:
union Option {
Some { value: int },
None {}
}
fn unwrap_or(opt: Option, default: int) -> int {
let result: int = (match opt {
Some(s) => { return s.value }
None(n) => { return default }
})
return result
}
shadow unwrap_or {
let some: Option = Option.Some { value: 42 }
let none: Option = Option.None {}
assert (== (unwrap_or some 0) 42)
assert (== (unwrap_or none 0) 0)
}
Examples
**Result type for errors:**
union ParseResult {
Success { number: int },
Error { message: string }
}
fn parse_number(s: string) -> ParseResult {
if (== s "42") {
return ParseResult.Success { number: 42 }
}
return ParseResult.Error { message: "Invalid number" }
}
fn get_or_default(r: ParseResult) -> int {
let value: int = (match r {
Success(s) => { return s.number }
Error(e) => { return -1 }
})
return value
}
shadow get_or_default {
let ok: ParseResult = (parse_number "42")
let err: ParseResult = (parse_number "bad")
assert (== (get_or_default ok) 42)
assert (== (get_or_default err) -1)
}
**Shape union:**
union Shape {
Circle { radius: float },
Rectangle { width: float, height: float }
}
fn area_shape(s: Shape) -> float {
let result: float = (match s {
Circle(c) => { return (* 3.14159 (* c.radius c.radius)) }
Rectangle(r) => { return (* r.width r.height) }
})
return result
}
shadow area_shape {
let circle: Shape = Shape.Circle { radius: 5.0 }
let rect: Shape = Shape.Rectangle { width: 4.0, height: 6.0 }
let area_c: float = (area_shape circle)
assert (and (> area_c 78.0) (< area_c 79.0))
assert (== (area_shape rect) 24.0)
}
7.4 Opaque Types
Opaque types hide their internal structure. They're defined in module interfaces but implemented elsewhere.
What Are Opaque Types?
Opaque types are useful for:
- Hiding implementation details
- Enforcing encapsulation
- FFI with C libraries
# Declare opaque type
opaque type Handle
# Use in function signatures
fn create_handle() -> Handle
fn use_handle(h: Handle) -> int
When to Use Opaque Types
**Use opaque types when:**
- Wrapping C libraries (file handles, database connections)
- Implementing abstract data types
- Enforcing invariants
Examples
**File handle (conceptual):**
# In a file I/O module:
opaque type FileHandle
extern fn open_file(path: string) -> FileHandle
extern fn read_file(handle: FileHandle) -> string
extern fn close_file(handle: FileHandle) -> void
💡 **Pro Tip:** Opaque types are mostly used for FFI. For regular NanoLang code, use structs and unions.
7.5 Pattern Matching
Pattern matching deconstructs unions and checks variants.
Match Expressions
union Status {
Ready {},
Running { progress: int },
Done { result: int }
}
fn status_message(s: Status) -> string {
let msg: string = (match s {
Ready(r) => { return "Ready to start" }
Running(r) => { return "In progress" }
Done(d) => { return "Complete" }
})
return msg
}
shadow status_message {
assert (== (status_message Status.Ready {}) "Ready to start")
assert (== (status_message Status.Running { progress: 50 }) "In progress")
assert (== (status_message Status.Done { result: 100 }) "Complete")
}
Match Syntax
**Components:**
1. match keyword
2. Value to match
3. Branches with variant patterns
4. Arrow => pointing to result
(match value {
Variant1(v1) => { /* handle v1 */ }
Variant2(v2) => { /* handle v2 */ }
})
Exhaustive Matching
Match expressions must handle all variants:
union Binary {
Zero {},
One {}
}
fn binary_to_int(b: Binary) -> int {
let val: int = (match b {
Zero(z) => { return 0 }
One(o) => { return 1 }
# Must handle all variants!
})
return val
}
shadow binary_to_int {
assert (== (binary_to_int Binary.Zero {}) 0)
assert (== (binary_to_int Binary.One {}) 1)
}
Examples
**Optional values:**
union Maybe {
Just { value: int },
Nothing {}
}
fn is_nothing(m: Maybe) -> bool {
let result: bool = (match m {
Just(j) => { return false }
Nothing(n) => { return true }
})
return result
}
shadow is_nothing {
assert (not (is_nothing Maybe.Just { value: 42 }))
assert (is_nothing Maybe.Nothing {})
}
**Error handling:**
union Validation {
Valid { data: int },
Invalid { reason: string }
}
fn is_valid(v: Validation) -> bool {
let result: bool = (match v {
Valid(val) => { return true }
Invalid(inv) => { return false }
})
return result
}
shadow is_valid {
let good: Validation = Validation.Valid { data: 100 }
let bad: Validation = Validation.Invalid { reason: "Too large" }
assert (is_valid good)
assert (not (is_valid bad))
}
**Accessing matched data:**
union Container {
Empty {},
Full { count: int, item: string }
}
fn describe(c: Container) -> string {
let desc: string = (match c {
Empty(e) => { return "Container is empty" }
Full(f) => {
return (+ "Contains " (int_to_string f.count))
}
})
return desc
}
shadow describe {
assert (== (describe Container.Empty {}) "Container is empty")
assert (== (describe Container.Full { count: 5, item: "apple" }) "Contains 5")
}
Complete Example: Binary Tree
union Tree {
Leaf { value: int },
Node { value: int, left: Tree, right: Tree }
}
fn sum_tree(t: Tree) -> int {
let total: int = (match t {
Leaf(l) => { return l.value }
Node(n) => {
let left_sum: int = (sum_tree n.left)
let right_sum: int = (sum_tree n.right)
return (+ n.value (+ left_sum right_sum))
}
})
return total
}
shadow sum_tree {
let leaf1: Tree = Tree.Leaf { value: 1 }
let leaf2: Tree = Tree.Leaf { value: 2 }
let leaf3: Tree = Tree.Leaf { value: 3 }
let node1: Tree = Tree.Node {
value: 4,
left: leaf1,
right: leaf2
}
let root: Tree = Tree.Node {
value: 5,
left: node1,
right: leaf3
}
assert (== (sum_tree leaf1) 1)
assert (== (sum_tree node1) 7) # 4 + 1 + 2
assert (== (sum_tree root) 15) # 5 + (4+1+2) + 3
}
Summary
In this chapter, you learned:
- ✅ Structs group fields into product types
- ✅ Enums define named integer constants
- ✅ Unions represent sum types with variants
- ✅ Pattern matching handles union variants
- ✅ Match expressions must be exhaustive
- ✅ Opaque types hide implementation details
Practice Exercises
# 1. Create a struct for 3D points
struct Point3D {
x: float,
y: float,
z: float
}
fn distance_from_origin(p: Point3D) -> float {
let x2: float = (* p.x p.x)
let y2: float = (* p.y p.y)
let z2: float = (* p.z p.z)
return (sqrt (+ x2 (+ y2 z2)))
}
shadow distance_from_origin {
let p: Point3D = Point3D { x: 3.0, y: 4.0, z: 0.0 }
let dist: float = (distance_from_origin p)
assert (and (> dist 4.9) (< dist 5.1))
}
# 2. Create an enum for card suits
enum Suit {
HEARTS = 0,
DIAMONDS = 1,
CLUBS = 2,
SPADES = 3
}
fn is_red(s: Suit) -> bool {
return (or (== s Suit.HEARTS) (== s Suit.DIAMONDS))
}
shadow is_red {
assert (is_red Suit.HEARTS)
assert (is_red Suit.DIAMONDS)
assert (not (is_red Suit.CLUBS))
assert (not (is_red Suit.SPADES))
}
# 3. Create a union for calculator operations
union CalcOp {
Add { a: int, b: int },
Subtract { a: int, b: int },
Multiply { a: int, b: int }
}
fn evaluate(op: CalcOp) -> int {
let result: int = (match op {
Add(add_op) => { return (+ add_op.a add_op.b) }
Subtract(sub_op) => { return (- sub_op.a sub_op.b) }
Multiply(mul_op) => { return (* mul_op.a mul_op.b) }
})
return result
}
shadow evaluate {
assert (== (evaluate CalcOp.Add { a: 5, b: 3 }) 8)
assert (== (evaluate CalcOp.Subtract { a: 5, b: 3 }) 2)
assert (== (evaluate CalcOp.Multiply { a: 5, b: 3 }) 15)
}
---
**Previous:** Chapter 6: Collections
**Next:** Chapter 8: Modules & Imports