punctional 0.2.0
A functional programming framework for Python — composable filters, method chaining, and expressive data pipelines with Option and Result monads.
License: MIT
Requires Python: >=3.10
Keywords: composition, data-pipeline, filters, functional-programming, method-chaining, monads, option, pipe-operator, result

# Punctional

> Functional programming for Python — monads, composable filters, and expressive data pipelines.

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

## What is Punctional?

**Punctional** brings robust functional programming patterns to Python. It provides:

- **Monads** for safe error handling and null-safety (`Option`, `Result`)
- **Composable filters** that chain with the pipe (`|`) operator
- **Functional wrappers** for native types enabling method chaining

No dependencies. Pure Python. Type-safe.

## Installation

```bash
pip install punctional
```

Or from source:

```bash
git clone https://github.com/peghaz/punctional.git
cd punctional
pip install -e .
```

---

## Core Features

### 1. Option Monad — Safe Null Handling

The `Option` type represents a value that may or may not exist. Use `Some` to wrap a value and `Nothing` to represent absence — eliminating `None` checks and `AttributeError` exceptions.

```python
from punctional import Some, Nothing, some

# Wrap existing values
user_name = Some("Alice")
print(user_name.map(str.upper))         # Some('ALICE')
print(user_name.get_or_else("Unknown")) # 'Alice'

# Handle missing values safely
missing = Nothing()
print(missing.map(str.upper))           # Nothing
print(missing.get_or_else("Unknown"))   # 'Unknown'

# Auto-convert from potentially None values
def find_user(user_id):
    return {"alice": "Alice"}.get(user_id)

result = some(find_user("bob"))  # Nothing (because get returns None)
result = some(find_user("alice"))  # Some('Alice')
```

**Option Methods:**

| Method | Description |
|--------|-------------|
| `map(f)` | Transform the value if present |
| `flat_map(f)` / `bind(f)` | Chain operations that return `Option` |
| `filter(predicate)` | Return `Nothing` if predicate fails |
| `get_or_else(default)` | Get value or return default |
| `get_or_none()` | Get value or `None` |
| `or_else(alternative)` | Return alternative `Option` if `Nothing` |
| `is_some()` / `is_nothing()` | Check presence |

**Chaining with Option:**

```python
from punctional import Some, Nothing

def parse_int(s: str) -> Option[int]:
    try:
        return Some(int(s))
    except ValueError:
        return Nothing()

def double(x: int) -> Option[int]:
    return Some(x * 2)

# Chain operations safely
result = Some("42").flat_map(parse_int).flat_map(double)  # Some(84)
result = Some("abc").flat_map(parse_int).flat_map(double) # Nothing
```

---

### 2. Result Monad — Explicit Error Handling

The `Result` type represents either success (`Ok`) or failure (`Error`). Unlike exceptions, errors are explicit in the type signature and must be handled.

```python
from punctional import Ok, Error, try_result

# Explicit success and failure
success = Ok(42)
failure = Error("Something went wrong")

print(success.map(lambda x: x * 2))  # Ok(84)
print(failure.map(lambda x: x * 2))  # Error('Something went wrong')

print(success.get_or_else(0))  # 42
print(failure.get_or_else(0))  # 0
```

**Wrapping exceptions with `try_result`:**

```python
from punctional import try_result

def divide(a, b):
    return a / b

result = try_result(lambda: divide(10, 2))  # Ok(5.0)
result = try_result(lambda: divide(10, 0))  # Error(ZeroDivisionError(...))

# With custom error handler
result = try_result(
    lambda: divide(10, 0),
    error_handler=lambda e: f"Division failed: {e}"
)  # Error('Division failed: division by zero')
```

**Result Methods:**

| Method | Description |
|--------|-------------|
| `map(f)` | Transform success value |
| `map_error(f)` | Transform error value |
| `flat_map(f)` / `bind(f)` | Chain operations returning `Result` |
| `get_or_else(default)` | Get value or default |
| `is_ok()` / `is_error()` | Check success/failure |
| `to_option()` | Convert to `Option` (discards error info) |

**Railway-oriented programming:**

```python
from punctional import Result, Ok, Error

def validate_age(age: int) -> Result[int, str]:
    if age < 0:
        return Error("Age cannot be negative")
    if age > 150:
        return Error("Age seems unrealistic")
    return Ok(age)

def validate_name(name: str) -> Result[str, str]:
    if not name.strip():
        return Error("Name cannot be empty")
    return Ok(name.strip())

# Chain validations
age_result = validate_age(25).map(lambda a: f"Age: {a}")     # Ok('Age: 25')
age_result = validate_age(-5).map(lambda a: f"Age: {a}")     # Error('Age cannot be negative')
```

---

### 3. Pipe Operator & Filters

Chain transformations using the `|` operator for readable, left-to-right data flow.

```python
from punctional import fint, fstr, ffloat, Add, Mult, Sub, Div, ToUpper

# Arithmetic chaining
result = fint(10) | Add(5) | Mult(2) | Sub(3)  # (10 + 5) * 2 - 3 = 27

# String transformations
text = fstr("hello") | ToUpper() | Add(" WORLD!")  # 'HELLO WORLD!'

# Float operations
value = ffloat(10.0) | Div(4) | Mult(2)  # 5.0
```

**Functional Wrappers:**

| Function | Type | Description |
|----------|------|-------------|
| `fint(x)` | `FunctionalInt` | Enables piping for integers |
| `ffloat(x)` | `FunctionalFloat` | Enables piping for floats |
| `fstr(x)` | `FunctionalStr` | Enables piping for strings |

---

### 4. Built-in Filters

**Arithmetic:**
```python
from punctional import fint, Add, Sub, Mult, Div

fint(10) | Add(5)   # 15
fint(10) | Sub(3)   # 7
fint(10) | Mult(2)  # 20
fint(10) | Div(4)   # 2.5
```

**Comparison:**
```python
from punctional import fint, GreaterThan, LessThan, Equals

fint(42) | GreaterThan(10)  # True
fint(5) | LessThan(10)      # True
fint(42) | Equals(42)       # True
```

**Logical:**
```python
from punctional import fint, AndFilter, OrFilter, NotFilter, GreaterThan, LessThan

# All conditions must pass
fint(42) | AndFilter(GreaterThan(10), LessThan(100))  # True

# At least one condition must pass
fint(5) | OrFilter(LessThan(3), GreaterThan(3))  # True

# Negate a condition
fint(5) | NotFilter(Equals(0))  # True
```

**String:**
```python
from punctional import fstr, ToUpper, ToLower, Contains, Mult

fstr("hello") | ToUpper()               # 'HELLO'
fstr("WORLD") | ToLower()               # 'world'
fstr("hello world") | Contains("world") # True
fstr("ha") | Mult(3)                    # 'hahaha'
```

**List Operations:**
```python
from punctional import Map, FilterList, Mult, GreaterThan

numbers = [1, 2, 3, 4, 5]

Map(Mult(2)).apply(numbers)              # [2, 4, 6, 8, 10]
FilterList(GreaterThan(2)).apply(numbers) # [3, 4, 5]
```

---

### 5. Composition

Create reusable pipelines with `Compose`:

```python
from punctional import Compose, Mult, Add, fint

# Define a reusable transformation
double_then_add_ten = Compose(Mult(2), Add(10))

fint(5) | double_then_add_ten   # 20  (5 * 2 + 10)
fint(10) | double_then_add_ten  # 30  (10 * 2 + 10)
```

---

### 6. Custom Filters

Create your own filters by extending the `Filter` base class:

```python
from punctional import Filter, fint

class Square(Filter[int, int]):
    def apply(self, value: int) -> int:
        return value ** 2

class Power(Filter[int, int]):
    def __init__(self, exponent: int):
        self.exponent = exponent
    
    def apply(self, value: int) -> int:
        return value ** self.exponent

fint(5) | Square()    # 25
fint(2) | Power(10)   # 1024
```

---

### 7. Functional Dataclasses

Add the `Functional` mixin to any dataclass to enable piping:

```python
from dataclasses import dataclass
from punctional import Functional, Filter

@dataclass
class Point(Functional):
    x: float
    y: float

class Scale(Filter[Point, Point]):
    def __init__(self, factor: float):
        self.factor = factor
    
    def apply(self, p: Point) -> Point:
        return Point(p.x * self.factor, p.y * self.factor)

class Translate(Filter[Point, Point]):
    def __init__(self, dx: float, dy: float):
        self.dx, self.dy = dx, dy
    
    def apply(self, p: Point) -> Point:
        return Point(p.x + self.dx, p.y + self.dy)

# Chain transformations on custom types
point = Point(3, 4)
result = point | Scale(2) | Translate(10, 10)  # Point(16, 18)
```

---

## Complete API Reference

### Monads

| Type | Description |
|------|-------------|
| `Option[T]` | Abstract base for optional values |
| `Some(value)` | Contains a value |
| `Nothing()` | Represents absence |
| `some(value)` | Creates `Some` or `Nothing` based on `None` check |
| `Result[T, E]` | Abstract base for success/failure |
| `Ok(value)` | Successful result |
| `Error(error)` | Failed result |
| `try_result(fn)` | Wraps a function, catching exceptions as `Error` |

### Filters

| Filter | Input → Output | Description |
|--------|----------------|-------------|
| `Add(n)` | `number → number` | Addition |
| `Sub(n)` | `number → number` | Subtraction |
| `Mult(n)` | `number → number` | Multiplication |
| `Div(n)` | `number → number` | Division |
| `GreaterThan(n)` | `number → bool` | Greater than comparison |
| `LessThan(n)` | `number → bool` | Less than comparison |
| `Equals(n)` | `any → bool` | Equality check |
| `AndFilter(*filters)` | `T → bool` | Logical AND |
| `OrFilter(*filters)` | `T → bool` | Logical OR |
| `NotFilter(filter)` | `T → bool` | Logical NOT |
| `ToUpper()` | `str → str` | Uppercase |
| `ToLower()` | `str → str` | Lowercase |
| `Contains(s)` | `str → bool` | Substring check |
| `Map(filter)` | `list → list` | Apply filter to each element |
| `FilterList(pred)` | `list → list` | Filter elements by predicate |
| `Compose(*filters)` | `T → U` | Compose multiple filters |

### Core Classes

| Class | Description |
|-------|-------------|
| `Filter[T, U]` | Abstract base class for all filters |
| `Functional` | Mixin that enables `\|` operator on any class |
| `FunctionalInt` | Wrapper for `int` with pipe support |
| `FunctionalFloat` | Wrapper for `float` with pipe support |
| `FunctionalStr` | Wrapper for `str` with pipe support |

---

## Examples

Run the included examples:

```bash
python -m examples.basics              # Introduction to all features
python -m examples.extending           # Creating custom filters
python -m examples.data_transformation # Real-world patterns
python -m examples.quick_reference     # Quick lookup cheatsheet
```

---

## Design Principles

1. **Explicit over implicit** — Errors are values, not exceptions
2. **Composability** — Small, reusable units that combine easily
3. **Type safety** — Generics provide IDE support and catch bugs early
4. **Immutability** — Filters return new values, never mutate
5. **Zero dependencies** — Pure Python, works everywhere

---

## License

MIT License — see [LICENSE](LICENSE) for details.

---

<p align="center">
  Made with ❤️ for functional programming in Python
</p>