# Option and Try Monads for Python  
Functional programming utilities inspired by Scala.

---

# Option Monad

`Option` represents a value that may or may not exist.

- `Some(value)` — a present value  
- `Nothing` — absence of a value  

This eliminates manual `None` checks and enables functional pipelines.

## Creating Options

```python
Option.of(10)      # Some(10)
Option.of(None)    # Nothing
```

## Properties

| Property | Some | Nothing |
|----------|------|---------|
| `is_defined` | True | False |
| `is_empty` | False | True |

## Methods

### `map(fn)`
Transforms the wrapped value if present.

```python
Option.of(10).map(lambda x: x * 2)   # Some(20)
Option.of(None).map(lambda x: x * 2) # Nothing
```

### `flat_map(fn)`
Chaining operations that return `Option`.

```python
def safe_div(a, b):
    return Option.of(a / b if b != 0 else None)

Option.of(10).flat_map(lambda x: safe_div(x, 2)) # Some(5)
```

### `filter(predicate)`
Keeps the value only if it satisfies the predicate.

```python
Option.of(10).filter(lambda x: x > 5)  # Some(10)
Option.of(3).filter(lambda x: x > 5)   # Nothing
```

### `get_or_else(default)`
```python
Option.of(None).get_or_else(99)  # 99
```

### `fold(default, mapping_fn)`
Useful for extraction.

```python
Option.of(20).fold(0, lambda x: x * 2)  # 40
```

### `or_else(other_option)`
```python
Option.of(None).or_else(Option.of(5))  # Some(5)
```

### `flatten()`
```python
Option.of(Option.of(10)).flatten()  # Some(10)
```

### Iteration
```python
for value in Option.of(10):
    print(value)   # 10
```

## Decorator: `@to_option`

Converts function output into `Option`.

```python
@to_option
def compute(x):
    return x * 2

compute(10)  # Some(20)
```

---

# Try Monad

`Try` models computations that may throw exceptions.

- `Success(value)`
- `Failure(exception)`

It removes the need for `try/except` in functional code.

## Creating Try Values

```python
Try.of(lambda: 10 / 2)  # Success(5.0)
Try.of(lambda: 10 / 0)  # Failure(ZeroDivisionError)
```

## Properties

| Property | Success | Failure |
|----------|---------|---------|
| `is_success` | True | False |
| `is_failure` | False | True |

## Methods

### `map(fn)`
Only applied if successful.

```python
Try.of(lambda: 5).map(lambda x: x + 1)   # Success(6)
Try.of(lambda: 1/0).map(lambda x: x + 1) # Failure(...)
```

### `flat_map(fn)`
Chaining computations that return `Try`.

```python
def parse_int(s: str):
    return Try.of(lambda: int(s))

Try.of(lambda: "123").flat_map(parse_int)  # Success(123)
```

### `filter(predicate)`
```python
Try.of(lambda: 10).filter(lambda x: x > 5)  # Success(10)
Try.of(lambda: 3).filter(lambda x: x > 5)   # Failure(ValueError)
```

### `get_or_else(default)`
```python
Try.of(lambda: 10/0).get_or_else(99)  # 99
```

### `or_else(default_try)`
```python
Try.of(lambda: 10/0).or_else(Success(5))  # Success(5)
```

### `fold(default_fn, mapping_fn)`
```python
Try.of(lambda: 10 / 0).fold(
    default=lambda ex: f"Error: {ex}",
    mapping_fn=lambda x: x * 2
)
# "Error: division by zero"
```

### `flatten()`
```python
Try.of(lambda: Success(10)).flatten()  # Success(10)
```

### Converting to Option
```python
Try.of(lambda: 10).to_option()      # Some(10)
Try.of(lambda: 1/0).to_option()     # Nothing
```

### Iteration
```python
for v in Try.of(lambda: 10):
    print(v)
```

## Decorator: `@to_try`

Automatically wraps exceptions into a `Try`.

```python
@to_try
def divide(a, b):
    return a / b

divide(10, 2)  # Success(5.0)
divide(10, 0)  # Failure(ZeroDivisionError)
```
