π Type System
Hindley-Milner type inference with structured values
Overview
AetherShell uses Hindley-Milner type inference, the same powerful type system found in Haskell, ML, and other functional languages. This means:
- β No type annotations required - types are automatically inferred
- β Type safety - catches type errors before runtime
- β Parametric polymorphism - functions work with any compatible type
- β Structured data - values flow as structured types, not raw text
Value types (Int, Float, String, Array,
Record, Lambda), not bash-style text streams.
Core Types
Primitive Types
| Type | Description | Examples | Operations |
|---|---|---|---|
Int |
Integer numbers | 42, -10, 0 |
Arithmetic, comparison |
Float |
Floating-point numbers | 3.14, -2.5, 0.1 |
Arithmetic, comparison |
Str |
UTF-8 strings | "hello", "world" |
Concatenation, interpolation |
Bool |
Boolean values | true, false |
Logical operations |
Null |
Absence of value | null |
Equality checks |
Compound Types
| Type | Description | Example |
|---|---|---|
Array |
Ordered collection of values | [1, 2, 3], ["a", "b"] |
Record |
Key-value mappings (objects/structs) | {name: "Alice", age: 30} |
Lambda |
First-class functions | fn(x) => x * 2 |
Type Checking
// Check types at runtime
type_of(42) // "Int"
type_of(3.14) // "Float"
type_of("hello") // "Str"
type_of(true) // "Bool"
type_of(null) // "Null"
type_of([1,2,3]) // "Array"
type_of({x: 1}) // "Record"
type_of(fn(x) => x) // "Lambda"
Type Inference
Automatic Inference
Types are inferred from context - no declarations needed:
// Integer inferred
x = 42
// Float inferred
y = 3.14
// String inferred
name = "Alice"
// Array inferred
numbers = [1, 2, 3]
// Record inferred
person = {name: "Bob", age: 25}
// Lambda inferred
double = fn(x) => x * 2
Polymorphic Functions
Functions can work with multiple types:
// Works with any array type
first = fn(arr) => arr[0]
first([1, 2, 3]) // 1 (Int)
first(["a", "b", "c"]) // "a" (Str)
first([[1,2], [3,4]]) // [1,2] (Array)
// Type signature (conceptual): βa. [a] β a
Type Constraints
Operations enforce type compatibility:
// Valid: same types
10 + 5 // 15 (Int + Int = Int)
3.14 + 2.86 // 6.0 (Float + Float = Float)
"Hello " + "World" // "Hello World" (Str + Str = Str)
// Invalid: incompatible types
// 10 + "hello" // Error: Cannot add Int and Str
// true * 5 // Error: Cannot multiply Bool and Int
Mutability: = vs :=
Immutable Bindings (=)
Default behavior - creates a binding that cannot be reassigned:
x = 10
print(x) // 10
// x = 20 // ERROR: Cannot reassign immutable binding
// But you can shadow in a new scope
{
x = 20 // Creates NEW binding in this scope
print(x) // 20
}
print(x) // Still 10 (outer scope unchanged)
Mutable Bindings (:=)
Allows reassignment of the same variable:
y := 10
print(y) // 10
y = 20 // OK - reassignment allowed
print(y) // 20
y = y + 5 // OK
print(y) // 25
When to Use Each
Use = (Immutable)
- Default choice
- Functional programming patterns
- Values that don't change
- Safer, easier to reason about
Use := (Mutable)
- Counters and accumulators
- Loop variables
- State that must change
- Imperative algorithms
Examples
// Functional style (immutable)
sum = [1, 2, 3, 4, 5]
| reduce(fn(a, b) => a + b, 0)
print(sum) // 15
// Imperative style (mutable)
total := 0
i := 1
while (i <= 5) {
total = total + i
i = i + 1
}
print(total) // 15
Arrays
Array Types
// Homogeneous arrays (same type)
ints = [1, 2, 3, 4] // [Int]
strs = ["a", "b", "c"] // [Str]
bools = [true, false, true] // [Bool]
// Heterogeneous arrays (mixed types)
mixed = [1, "two", 3.0, true] // [Value]
// Nested arrays
matrix = [[1,2], [3,4], [5,6]] // [[Int]]
Array Operations
arr = [1, 2, 3, 4, 5]
// Indexing
arr[0] // 1
arr[4] // 5
arr[-1] // 5 (last element)
// Length
len(arr) // 5
// Transformation
arr | map(fn(x) => x * 2) // [2, 4, 6, 8, 10]
arr | where(fn(x) => x > 2) // [3, 4, 5]
arr | reduce(fn(a,b) => a+b, 0) // 15
// Slicing
slice(arr, 1, 4) // [2, 3, 4]
take(arr, 3) // [1, 2, 3]
Records (Objects)
Record Structure
// Simple record
point = {x: 10, y: 20}
// Nested records
person = {
name: "Alice",
age: 30,
address: {
street: "123 Main St",
city: "Springfield"
},
hobbies: ["reading", "coding", "music"]
}
Record Access
person = {name: "Alice", age: 30}
// Field access
person.name // "Alice"
person.age // 30
// Nested access
person.address.city // "Springfield"
// Get all keys
keys(person) // ["name", "age"]
// Check field count
len(person) // 2
Records in Pipelines
// Table of records (database-like)
users = [
{name: "Alice", age: 30, active: true},
{name: "Bob", age: 25, active: false},
{name: "Charlie", age: 35, active: true}
]
// Filter active users
active = users | where(fn(u) => u.active)
// Get names of active users
names = active | map(fn(u) => u.name)
// Find average age
avg = users
| map(fn(u) => u.age)
| reduce(fn(a,b) => a+b, 0)
| fn(total) => total / len(users)
Functions (Lambdas)
Lambda Types
// Type: () β Int
get_answer = fn() => 42
// Type: Int β Int
double = fn(x) => x * 2
// Type: (Int, Int) β Int
add = fn(a, b) => a + b
// Type: [Int] β Int
sum = fn(arr) => arr | reduce(fn(a,b) => a+b, 0)
// Type: (Int β Int) β (Int β Int) (Higher-order)
twice = fn(f) => fn(x) => f(f(x))
Function Composition
// Compose functions
compose = fn(f, g) => fn(x) => f(g(x))
times2 = fn(x) => x * 2
plus3 = fn(x) => x + 3
// times2_then_plus3 : Int β Int
times2_then_plus3 = compose(plus3, times2)
times2_then_plus3(5) // 13 (5*2 = 10, 10+3 = 13)
Closures
// Functions capture their environment
make_counter = fn(start) => {
count := start
fn() => {
old = count
count = count + 1
old
}
}
counter = make_counter(10)
counter() // 10
counter() // 11
counter() // 12
Type Coercion & Conversion
Automatic Coercion
Limited automatic coercion for convenience:
// Int promotes to Float in mixed operations
10 + 3.14 // 13.14 (IntβFloat, then Float+Float)
// String interpolation converts values
x = 42
"Answer: ${x}" // "Answer: 42" (IntβStr)
Explicit Conversion (Future)
Explicit conversion functions may be added:
// Future possibilities
// to_int("42") // 42
// to_float("3.14") // 3.14
// to_string(42) // "42"
Type Safety
Static Guarantees
Type inference catches errors before runtime:
// Type mismatch caught early
// f = fn(x) => x + "hello"
// f(10) // Error: Cannot add Int and Str
// Undefined field access caught
// person = {name: "Alice"}
// person.age // Error: Field 'age' does not exist
Runtime Type Checks
// Check types dynamically
value = 42
if type_of(value) == "Int" {
print("It's an integer!")
}
// Pattern matching (conceptual)
// match value {
// Int => "number",
// Str => "text",
// _ => "other"
// }
Advanced Type Patterns
Generic Data Structures
// Stack implementation
make_stack = fn() => {
items := []
{
push: fn(x) => items = push(items, x),
pop: fn() => {
top = last(items)
items = slice(items, 0, len(items) - 1)
top
},
peek: fn() => last(items),
is_empty: fn() => len(items) == 0
}
}
stack = make_stack()
stack.push(1)
stack.push(2)
stack.pop() // 2
stack.peek() // 1
Type-Driven Development
// Design by types
// map : ([a], (a β b)) β [b]
// filter : ([a], (a β Bool)) β [a]
// reduce : ([a], (b, a β b), b) β b
// Compose higher-order operations
process = fn(data) =>
data
| where(fn(x) => x > 0) // [a] β [a]
| map(fn(x) => x * 2) // [a] β [b]
| reduce(fn(acc, x) => acc + x, 0) // [a] β b
Type System Benefits
π Safety
Catch type errors before runtime, preventing crashes and unexpected behavior.
π Clarity
Types document code intent, making it easier to understand and maintain.
π Performance
Type information enables optimizations and efficient execution.
π Refactoring
Safe refactoring - type checker ensures changes don't break code.