# Chapter 12: Behavioral Types

Lux's behavioral types let you make **compile-time guarantees** about function behavior. Unlike comments or documentation, these are actually verified by the compiler.

## Why Behavioral Types Matter

Consider these real-world scenarios:

1. **Payment processing**: You retry a failed charge. If the function isn't idempotent, you might charge the customer twice.

2. **Caching**: You cache a computation. If the function isn't deterministic, you'll serve stale/wrong results.

3. **Parallelization**: You run tasks in parallel. If they aren't pure, you'll have race conditions.

4. **Infinite loops**: A function never returns. If it was supposed to be total, you have a bug.

**Behavioral types catch these bugs at compile time.**

## The Five Properties

### 1. Pure (`is pure`)

A pure function has **no side effects**. It only depends on its inputs.

```lux
// GOOD: No effects, just computation
fn add(a: Int, b: Int): Int is pure = a + b

fn double(x: Int): Int is pure = x * 2

fn greet(name: String): String is pure = "Hello, " + name

// ERROR: Pure function cannot have effects
fn impure(x: Int): Int is pure with {Console} =
    Console.print("x = " + toString(x))  // Compiler error!
    x
```

**What the compiler checks:**
- Function must have an empty effect set
- No calls to effectful operations

**When to use `is pure`:**
- Mathematical functions
- Data transformations
- Any function that should be cacheable

**Compiler optimizations enabled:**
- Memoization (cache results)
- Common subexpression elimination
- Parallel execution
- Dead code elimination (if result unused)

### 2. Total (`is total`)

A total function **always terminates** and **never fails**. It produces a value for every valid input.

```lux
// GOOD: Always terminates (structural recursion)
fn factorial(n: Int): Int is total =
    if n <= 1 then 1 else n * factorial(n - 1)

// GOOD: Non-recursive is always total
fn max(a: Int, b: Int): Int is total =
    if a > b then a else b

// GOOD: List operations that terminate
fn length<T>(list: List<T>): Int is total =
    match list {
        [] => 0,
        [_, ...rest] => 1 + length(rest)  // Structurally decreasing
    }

// ERROR: Uses Fail effect
fn divide(a: Int, b: Int): Int is total with {Fail} =
    if b == 0 then Fail.fail("division by zero")  // Compiler error!
    else a / b

// ERROR: May not terminate (not structurally decreasing)
fn collatz(n: Int): Int is total =
    if n == 1 then 1
    else if n % 2 == 0 then collatz(n / 2)
    else collatz(3 * n + 1)  // Not structurally smaller!
```

**What the compiler checks:**
- No `Fail` effect used
- Recursive calls must have at least one structurally decreasing argument

**When to use `is total`:**
- Core business logic that must never crash
- Mathematical functions
- Data structure operations

**Compiler optimizations enabled:**
- No exception handling overhead
- Aggressive inlining
- Removal of termination checks

### 3. Deterministic (`is deterministic`)

A deterministic function produces the **same output for the same input**, every time.

```lux
// GOOD: Same input = same output
fn hash(s: String): Int is deterministic =
    List.fold(String.chars(s), 0, fn(acc: Int, c: String): Int => acc * 31 + charCode(c))

fn formatDate(year: Int, month: Int, day: Int): String is deterministic =
    toString(year) + "-" + padZero(month) + "-" + padZero(day)

// ERROR: Random is non-deterministic
fn generateId(): String is deterministic with {Random} =
    "id-" + toString(Random.int(0, 1000000))  // Compiler error!

// ERROR: Time is non-deterministic
fn timestamp(): Int is deterministic with {Time} =
    Time.now()  // Compiler error!
```

**What the compiler checks:**
- No `Random` effect
- No `Time` effect

**When to use `is deterministic`:**
- Hashing functions
- Serialization/formatting
- Test helpers

**Compiler optimizations enabled:**
- Result caching
- Parallel execution with consistent results
- Test reproducibility

### 4. Idempotent (`is idempotent`)

An idempotent function satisfies: `f(f(x)) == f(x)`. Applying it multiple times has the same effect as applying it once.

```lux
// GOOD: Pattern 1 - Constants
fn alwaysZero(x: Int): Int is idempotent = 0

// GOOD: Pattern 2 - Identity
fn identity<T>(x: T): T is idempotent = x

// GOOD: Pattern 3 - Projection
fn getName(person: Person): String is idempotent = person.name

// GOOD: Pattern 4 - Clamping
fn clampPositive(x: Int): Int is idempotent =
    if x < 0 then 0 else x

// GOOD: Pattern 5 - Absolute value
fn abs(x: Int): Int is idempotent =
    if x < 0 then 0 - x else x

// ERROR: Not idempotent (increment changes value each time)
fn increment(x: Int): Int is idempotent = x + 1  // f(f(1)) = 3, not 2

// If you're certain a function is idempotent but the compiler can't verify:
fn normalize(s: String): String assume is idempotent =
    String.toLower(String.trim(s))
```

**What the compiler checks:**
- Pattern recognition: constants, identity, projections, clamping, abs

**When to use `is idempotent`:**
- Setting configuration
- Database upserts
- API PUT/DELETE operations (REST semantics)
- Retry-safe operations

**Real-world example - safe retries:**

```lux
// Payment processing with safe retries
fn chargeCard(amount: Int, cardId: String): Receipt
    is idempotent
    with {Payment, Logger} = {
    Logger.log("Charging card " + cardId)
    Payment.charge(amount, cardId)
}

// Safe to retry because chargeCard is idempotent
fn processWithRetry(amount: Int, cardId: String): Receipt with {Payment, Logger, Fail} = {
    let result = retry(3, fn(): Receipt => chargeCard(amount, cardId))
    match result {
        Ok(receipt) => receipt,
        Err(e) => Fail.fail("Payment failed after 3 attempts: " + e)
    }
}
```

### 5. Commutative (`is commutative`)

A commutative function satisfies: `f(a, b) == f(b, a)`. The order of arguments doesn't matter.

```lux
// GOOD: Addition is commutative
fn add(a: Int, b: Int): Int is commutative = a + b

// GOOD: Multiplication is commutative
fn multiply(a: Int, b: Int): Int is commutative = a * b

// GOOD: Min/max are commutative
fn minimum(a: Int, b: Int): Int is commutative =
    if a < b then a else b

// ERROR: Subtraction is not commutative (3 - 2 != 2 - 3)
fn subtract(a: Int, b: Int): Int is commutative = a - b  // Compiler error!

// ERROR: Wrong number of parameters
fn triple(a: Int, b: Int, c: Int): Int is commutative = a + b + c  // Must have exactly 2
```

**What the compiler checks:**
- Must have exactly 2 parameters
- Body must be a commutative operation (+, *, min, max, ==, !=, &&, ||)

**When to use `is commutative`:**
- Mathematical operations
- Set operations (union, intersection)
- Merging/combining functions

**Compiler optimizations enabled:**
- Argument reordering for efficiency
- Parallel reduction
- Algebraic simplifications

## Combining Properties

Properties can be combined for stronger guarantees:

```lux
// Pure + deterministic + total = perfect for caching
fn computeHash(data: String): Int
    is pure
    is deterministic
    is total = {
    List.fold(String.chars(data), 0, fn(acc: Int, c: String): Int =>
        acc * 31 + charCode(c)
    )
}

// Pure + idempotent = safe transformation
fn normalizeEmail(email: String): String
    is pure
    is idempotent = {
    String.toLower(String.trim(email))
}

// Commutative + pure = parallel reduction friendly
fn merge(a: Record, b: Record): Record
    is pure
    is commutative = {
    { ...a, ...b }  // Last wins, but both contribute
}
```

## Property Constraints in Where Clauses

You can require function arguments to have certain properties:

```lux
// Higher-order function that requires a pure function
fn map<T, U>(list: List<T>, f: fn(T): U is pure): List<U> is pure =
    match list {
        [] => [],
        [x, ...rest] => [f(x), ...map(rest, f)]
    }

// Only accepts idempotent functions - safe to retry
fn retry<T>(times: Int, action: fn(): T is idempotent): Result<T, String> = {
    if times <= 0 then Err("No attempts left")
    else {
        match tryCall(action) {
            Ok(result) => Ok(result),
            Err(e) => retry(times - 1, action)  // Safe because action is idempotent
        }
    }
}

// Only accepts deterministic functions - safe to cache
fn memoize<K, V>(f: fn(K): V is deterministic): fn(K): V = {
    let cache = HashMap.new()
    fn(key: K): V => {
        match cache.get(key) {
            Some(v) => v,
            None => {
                let v = f(key)
                cache.set(key, v)
                v
            }
        }
    }
}

// Usage:
let cachedHash = memoize(computeHash)  // OK: computeHash is deterministic
let badCache = memoize(generateRandom)  // ERROR: generateRandom is not deterministic
```

## The `assume` Escape Hatch

Sometimes you know a function has a property but the compiler can't verify it. Use `assume`:

```lux
// Compiler can't verify this is idempotent, but we know it is
fn setUserStatus(userId: String, status: String): Unit
    assume is idempotent
    with {Database} = {
    Database.execute("UPDATE users SET status = ? WHERE id = ?", [status, userId])
}

// Use assume sparingly - it bypasses compiler checks!
// If you're wrong, you may have subtle bugs.
```

**Warning**: `assume` tells the compiler to trust you. If you're wrong, the optimization or guarantee may be invalid.

## Compiler Optimizations

When the compiler knows behavioral properties, it can optimize aggressively:

| Property | Optimizations |
|----------|---------------|
| `is pure` | Memoization, CSE, dead code elimination, parallelization |
| `is total` | No exception handling, aggressive inlining |
| `is deterministic` | Result caching, parallel execution |
| `is idempotent` | Retry optimization, duplicate call elimination |
| `is commutative` | Argument reordering, parallel reduction |

### Example: Automatic Memoization

```lux
fn expensiveComputation(n: Int): Int
    is pure
    is deterministic
    is total = {
    // Complex calculation...
    fib(n)
}

// The compiler may automatically cache results because:
// - pure: no side effects, so caching is safe
// - deterministic: same input = same output
// - total: will always return a value
```

### Example: Safe Parallelization

```lux
fn processItems(items: List<Item>): List<Result>
    is pure = {
    List.map(items, processItem)
}

// If processItem is pure, the compiler can parallelize this automatically
```

## Practical Examples

### Example 1: Financial Calculations

```lux
// Interest calculation - pure, deterministic, total
fn calculateInterest(principal: Int, rate: Float, years: Int): Float
    is pure
    is deterministic
    is total = {
    let r = rate / 100.0
    Float.fromInt(principal) * Math.pow(1.0 + r, Float.fromInt(years))
}

// Transaction validation - pure, total
fn validateTransaction(tx: Transaction): Result<Transaction, String>
    is pure
    is total = {
    if tx.amount <= 0 then Err("Amount must be positive")
    else if tx.from == tx.to then Err("Cannot transfer to self")
    else Ok(tx)
}
```

### Example 2: Data Processing Pipeline

```lux
// Each step is pure and deterministic
fn cleanData(raw: String): String is pure is deterministic =
    raw |> String.trim |> String.toLower

fn parseRecord(line: String): Result<Record, String> is pure is deterministic =
    match String.split(line, ",") {
        [name, age, email] => Ok({ name, age: parseInt(age), email }),
        _ => Err("Invalid format")
    }

fn validateRecord(record: Record): Bool is pure is deterministic is total =
    String.length(record.name) > 0 && record.age > 0

// Pipeline can be parallelized because all functions are pure + deterministic
fn processFile(contents: String): List<Record> is pure is deterministic = {
    contents
    |> String.lines
    |> List.map(cleanData)
    |> List.map(parseRecord)
    |> List.filterMap(fn(r: Result<Record, String>): Option<Record> =>
        match r { Ok(v) => Some(v), Err(_) => None })
    |> List.filter(validateRecord)
}
```

### Example 3: Idempotent API Handlers

```lux
// PUT /users/:id - idempotent by REST semantics
fn handlePutUser(id: String, data: UserData): Response
    is idempotent
    with {Database, Logger} = {
    Logger.log("PUT /users/" + id)
    Database.upsert("users", id, data)
    Response.ok({ id, ...data })
}

// DELETE /users/:id - idempotent by REST semantics
fn handleDeleteUser(id: String): Response
    is idempotent
    with {Database, Logger} = {
    Logger.log("DELETE /users/" + id)
    Database.delete("users", id)  // Safe to call multiple times
    Response.noContent()
}
```

## Summary

| Property | Meaning | Compiler Checks | Use Case |
|----------|---------|-----------------|----------|
| `is pure` | No effects | Empty effect set | Caching, parallelization |
| `is total` | Always terminates | No Fail, structural recursion | Core logic |
| `is deterministic` | Same in = same out | No Random/Time | Caching, testing |
| `is idempotent` | f(f(x)) = f(x) | Pattern recognition | Retries, APIs |
| `is commutative` | f(a,b) = f(b,a) | 2 params, commutative op | Math, merging |

## What's Next?

- [Chapter 13: Schema Evolution](./13-schema-evolution.md) - Version your data types
- [Tutorials](../tutorials/README.md) - Practical projects
