Overview

AetherShell uses Hindley-Milner type inference, the same powerful type system found in Haskell, ML, and other functional languages. This means:

Key Principle: Data flows as 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.

Next: Learn about AI & Agents or explore Syntax Knowledge Base.