# Tutorial: Dependency Injection with Effects

Learn how effects provide natural dependency injection for testing and flexibility.

## What You'll Learn

- Using effects for dependencies
- Swapping handlers for testing
- The "ports and adapters" pattern

## The Problem

Imagine you're building a user registration system:

```javascript
// Traditional approach (pseudo-code)
function registerUser(userData) {
    const validated = validate(userData);
    database.insert(validated);       // Hard-coded dependency
    emailService.send(validated.email, "Welcome!");  // Another one
    logger.info("User registered");   // And another
    return validated.id;
}
```

Testing this requires mocking `database`, `emailService`, and `logger`. In many languages, this means:
- Dependency injection frameworks
- Mock libraries
- Complex test setup

## The Lux Way

In Lux, dependencies are effects:

```lux
// Define what we need (not how it works)
effect Database {
    fn insert(table: String, data: String): Int
    fn query(sql: String): List<String>
}

effect Email {
    fn send(to: String, subject: String, body: String): Unit
}

effect Logger {
    fn info(message: String): Unit
    fn error(message: String): Unit
}
```

Now our business logic declares its dependencies:

```lux
type User = { name: String, email: String }

fn registerUser(user: User): Int with {Database, Email, Logger} = {
    Logger.info("Registering user: " + user.name)

    // Validate
    if user.name == "" then {
        Logger.error("Empty name")
        Fail.fail("Name required")
    } else ()

    if user.email == "" then {
        Logger.error("Empty email")
        Fail.fail("Email required")
    } else ()

    // Insert into database
    let userId = Database.insert("users", userToJson(user))
    Logger.info("Created user with ID: " + toString(userId))

    // Send welcome email
    Email.send(user.email, "Welcome!", "Thanks for registering, " + user.name)
    Logger.info("Sent welcome email")

    userId
}

fn userToJson(user: User): String =
    "{\"name\":\"" + user.name + "\",\"email\":\"" + user.email + "\"}"
```

## Production Handlers

For production, we implement real handlers:

```lux
handler postgresDb: Database {
    fn insert(table, data) = {
        let result = Http.post("http://db-service/insert", data)
        // Parse ID from response
        resume(parseId(result))
    }
    fn query(sql) = {
        let result = Http.post("http://db-service/query", sql)
        resume(parseRows(result))
    }
}

handler smtpEmail: Email {
    fn send(to, subject, body) = {
        Http.post("http://email-service/send", formatEmail(to, subject, body))
        resume(())
    }
}

handler consoleLogger: Logger {
    fn info(msg) = {
        Console.print("[INFO] " + msg)
        resume(())
    }
    fn error(msg) = {
        Console.print("[ERROR] " + msg)
        resume(())
    }
}
```

Production code:

```lux
fn main(): Unit with {Console, Http} = {
    let user = User { name: "Alice", email: "alice@example.com" }

    let result = run registerUser(user) with {
        Database = postgresDb,
        Email = smtpEmail,
        Logger = consoleLogger
    }

    Console.print("Registered user ID: " + toString(result))
}
```

## Test Handlers

For testing, we swap in mock handlers:

```lux
// Mock database that stores in memory
handler mockDb: Database {
    fn insert(table, data) = {
        let id = State.get()
        State.put(id + 1)
        Console.print("[MOCK DB] Inserted into " + table + ": " + data)
        resume(id)
    }
    fn query(sql) = {
        Console.print("[MOCK DB] Query: " + sql)
        resume(["mock", "data"])
    }
}

// Mock email that just logs
handler mockEmail: Email {
    fn send(to, subject, body) = {
        Console.print("[MOCK EMAIL] To: " + to + ", Subject: " + subject)
        resume(())
    }
}

// Silent logger for tests
handler silentLogger: Logger {
    fn info(msg) = resume(())
    fn error(msg) = resume(())
}

// Collecting logger for assertions
handler collectingLogger: Logger {
    fn info(msg) = {
        State.put(State.get() + "[INFO] " + msg + "\n")
        resume(())
    }
    fn error(msg) = {
        State.put(State.get() + "[ERROR] " + msg + "\n")
        resume(())
    }
}
```

Test code:

```lux
fn testRegisterUser(): Unit with {Console} = {
    let user = User { name: "Test", email: "test@test.com" }

    // Run with mocks
    let (userId, logs) = run {
        let id = run {
            run registerUser(user) with {
                Database = mockDb,
                Email = mockEmail,
                Logger = collectingLogger
            }
        } with { State = 1 }  // Database ID counter

        let logs = State.get()
        (id, logs)
    } with { State = "" }  // Log accumulator

    // Assertions
    Console.print("User ID: " + toString(userId))
    Console.print("Logs:\n" + logs)

    if userId == 1 then
        Console.print("✓ User ID is correct")
    else
        Console.print("✗ User ID is wrong")

    if String.contains(logs, "Registering user") then
        Console.print("✓ Registration was logged")
    else
        Console.print("✗ Registration was not logged")
}
```

## Testing Failure Cases

Test error handling by using handlers that fail:

```lux
handler failingDb: Database {
    fn insert(table, data) = {
        Fail.fail("Database connection failed")
    }
    fn query(sql) = resume([])
}

fn testDatabaseFailure(): Unit with {Console} = {
    let user = User { name: "Test", email: "test@test.com" }

    let result = run {
        run registerUser(user) with {
            Database = failingDb,
            Email = mockEmail,
            Logger = silentLogger
        }
    } with {}

    Console.print("Test: Database failure is handled")
    // The Fail effect should have triggered
}
```

## The "Ports and Adapters" Pattern

This is also known as hexagonal architecture:

```
                    ┌─────────────────────┐
                    │   Business Logic    │
                    │   (Pure + Effects)  │
                    └─────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              │               │               │
        ┌─────┴─────┐   ┌─────┴─────┐   ┌─────┴─────┐
        │ Database  │   │   Email   │   │  Logger   │
        │  Effect   │   │  Effect   │   │  Effect   │
        └─────┬─────┘   └─────┬─────┘   └─────┬─────┘
              │               │               │
    ┌─────────┼─────────┐     │         ┌─────┼─────┐
    │         │         │     │         │           │
┌───┴───┐ ┌───┴───┐     │ ┌───┴───┐ ┌───┴───┐ ┌─────┴─────┐
│Postgres│ │MockDB │     │ │ SMTP  │ │Console│ │ Collector │
└───────┘ └───────┘     │ └───────┘ └───────┘ └───────────┘
                        │
                  ┌─────┴─────┐
                  │ MockEmail │
                  └───────────┘
```

- **Business logic** is pure (plus declared effects)
- **Effects** are the ports (interfaces)
- **Handlers** are the adapters (implementations)

## Benefits

1. **No mocking libraries needed** - Just write a different handler
2. **Type-safe** - The compiler ensures all effects are handled
3. **Explicit dependencies** - You can see what a function needs
4. **Easy to test** - Swap handlers, no reflection or magic
5. **Flexible** - Same code, different environments

## Complete Example

```lux
// main.lux

// === Effects (Ports) ===

effect Database {
    fn insert(table: String, data: String): Int
}

effect Email {
    fn send(to: String, body: String): Unit
}

// === Business Logic ===

type User = { name: String, email: String }

fn registerUser(user: User): Int with {Database, Email, Fail} = {
    if user.name == "" then Fail.fail("Name required") else ()
    let id = Database.insert("users", user.name)
    Email.send(user.email, "Welcome!")
    id
}

// === Production Handlers (Adapters) ===

handler prodDb: Database {
    fn insert(table, data) = {
        Console.print("[DB] INSERT INTO " + table)
        resume(42)  // Would be real ID
    }
}

handler prodEmail: Email {
    fn send(to, body) = {
        Console.print("[EMAIL] Sending to " + to)
        resume(())
    }
}

// === Test Handlers ===

handler testDb: Database {
    fn insert(table, data) = resume(1)
}

handler testEmail: Email {
    fn send(to, body) = resume(())
}

// === Running ===

fn production(): Unit with {Console} = {
    let user = User { name: "Alice", email: "alice@example.com" }
    let id = run registerUser(user) with {
        Database = prodDb,
        Email = prodEmail
    }
    Console.print("Created user: " + toString(id))
}

fn test(): Unit with {Console} = {
    let user = User { name: "Test", email: "test@test.com" }
    let id = run registerUser(user) with {
        Database = testDb,
        Email = testEmail
    }
    if id == 1 then Console.print("✓ Test passed")
    else Console.print("✗ Test failed")
}

fn main(): Unit with {Console} = {
    Console.print("=== Production ===")
    production()
    Console.print("\n=== Test ===")
    test()
}

let output = run main() with {}
```

## What You Learned

- Effects define **what** you need, handlers define **how**
- Swap handlers for testing without changing business logic
- No dependency injection framework needed
- Type system ensures all dependencies are satisfied

## Next Tutorial

[State Machines](state-machines.md) - Model state transitions with custom effects.
