# IO - Explicit Side Effects

Mark and isolate side effects from pure code.

## Overview

`IO` represents a computation with side effects:
- `IO(func)` - Lazy computation that will have side effects when run

## Basic Usage

```python
from mfn import IO

# Create IO
def print_greeting():
    print("Hello, World!")

io = IO(print_greeting)

# IO doesn't execute yet
io.run()  # Executes and prints
```

## Transformation

```python
from mfn import IO

# Map: Transform result
io = IO(lambda: 42)

result = io.map(lambda x: x * 2)
result.run()  # Returns 84

# Then: Chain IO operations
def read_file():
    return "file content"

def process(content):
    return content.upper()

def write(data):
    print(f"Writing: {data}")

io = IO(read_file).then(lambda c: IO(lambda: process(c))).then(IO)
io.run()
# Prints: "Writing: FILE CONTENT"
```

## IO for Side Effects

```python
from mfn import IO

# Database operations
def save_user(user: dict) -> IO:

    def inner():
        db.insert(user)
        print(f"Saved user {user['id']}")
        return user

    return IO(inner)


# API calls
def fetch_user(id: int) -> IO:

    def inner():
        user = api.get(f"/users/{id}")
        print(f"Fetched user {id}")
        return user

    return IO(inner)


# Chain IO operations
def register_user(data: dict):
    return (
        fetch_user(1)
        .then(lambda _: IO(lambda: validate(data)))
        .then(lambda _: IO(lambda: create_user(data)))
        .then(save_user)
    )
```

## Pure vs Impure

```python
from mfn import IO

# Pure: No side effects
def calculate_discount(total: float, rate: float) -> float:
    return total * (1 - rate)


# Impure: Has side effects
def log_discount(discount: float) -> IO:

    def inner():
        print(f"Discount: {discount}")
        return discount

    return IO(inner)


def process_order(total: float, rate: float) -> IO:

    # Pure calculation
    discounted = calculate_discount(total, rate)

    # Log side effect
    return log_discount(discounted)
```

## IO Composition

```python
from mfn import IO

def sequence(*ios: IO) -> IO:

    def inner():
        results = []
        for io in ios:
            result = io.run()
            results.append(result)
        return results

    return IO(inner)


# Use
def batch_operations():

    return sequence(
        IO(lambda: print("Step 1")),
        IO(lambda: print("Step 2")),
        IO(lambda: print("Step 3"))
    )


batch_operations().run()
# Prints all steps
```

## IO with Result

```python
from mfn import IO, Result

def safe_io(func) -> IO:

    def inner():
        try:
            return Result.ok(func())
        except Exception as e:
            return Result.error(str(e))

    return IO(inner)


# Use
def read_file(path: str) -> IO:

    def inner():
        try:
            with open(path) as f:
                return Ok(f.read())
        except IOError as e:
            return Error(str(e))

    return IO(inner)


result = read_file("data.txt").run()

if result.is_ok():
    content = result.unwrap()
else:
    error = result.error
```

## IO Helpers

```python
from mfn import IO

class IOHelpers:
    """Common IO operations"""

    @staticmethod
    def put_str(s: str) -> IO:

        return IO(lambda: print(s))

    @staticmethod
    def get_str(prompt: str) -> IO:

        return IO(lambda: input(prompt))

    @staticmethod
    def read_file(path: str) -> IO:

        return IO(lambda: open(path).read())

    @staticmethod
    def write_file(path: str, content: str) -> IO:

        def inner():
            with open(path, 'w') as f:
                f.write(content)

        return IO(inner)

    @staticmethod
    def delay(seconds: float) -> IO:

        import time
        return IO(lambda: time.sleep(seconds))


# Use
def interactive():

    return (
        IOHelpers.get_str("Enter name: ")
        .then(lambda name: IOHelpers.put_str(f"Hello, {name}"))
    )


interactive().run()
```

## IO Monad Laws

```python
from mfn import IO

# Left identity: IO(x).then(f) == f(x)
x = 42
f = lambda v: IO(lambda: v * 2)

assert (
    IO(lambda: x).then(f).run()
    == f(x).run()
)

# Right identity: m.then(IO) == m
m = IO(lambda: 42)

assert (
    m.then(IO).run()
    == m.run()
)

# Associativity: (m.then(f)).then(g) == m.then(lambda x: f(x).then(g))

def f(x):
    return IO(lambda: x * 2)

def g(x):
    return IO(lambda: x + 10)

assert (
    m.then(f).then(g).run()
    == m.then(lambda x: f(x).then(g)).run()
)
```

## Refactoring with IO

```python
# Before: Impure function
def process_user(id: int):
    user = db.find(id)          # Side effect
    user["name"] = user["name"].upper()
    db.update(user)             # Side effect
    send_email(user)            # Side effect
    return user


# After: Separate pure and impure

# Pure
def transform_user(user: dict) -> dict:
    return {**user, "name": user["name"].upper()}

# Impure (marked with IO)
def process_user_pure(id: int) -> IO:

    return (
        IO(lambda: db.find(id))
        .then(lambda user: IO(lambda: transform_user(user)))
        .then(lambda user: IO(lambda: db.update(user)))
        .then(lambda user: IO(lambda: send_email(user)))
    )
```

## DX Benefits

✅ **Explicit**: Side effects are visible
✅ **Lazy**: IO doesn't execute until run()
✅ **Composable**: Chain side effects
✅ **Separation**: Pure vs impure code
✅ **Testable**: Mock IO in tests

## Best Practices

```python
# ✅ Good: Mark side effects
def save_data(data):  # Impure
    return IO(lambda: db.save(data))

# ✅ Good: Keep logic pure
def calculate(data):  # Pure
    return data * 2

# ✅ Good: Chain IO
read().then(parse).then(save)

# ✅ Good: Return IO from functions
# Let caller decide when to run

# ❌ Bad: Mixing pure and impure
# Keep separate

# ❌ Bad: Running IO too early
# Return IO, don't run it
```
