Metadata-Version: 2.4
Name: fnutil
Version: 1.1.0
Summary: Lightweight functional-style helpers for Python 3.13+
Author: Luke Scherrer
Author-email: Luke Scherrer <lscherrer2@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.13
Project-URL: Homepage, https://github.com/lscherrer2/fnutil
Project-URL: Repository, https://github.com/lscherrer2/fnutil
Project-URL: Issues, https://github.com/lscherrer2/fnutil/issues
Description-Content-Type: text/markdown

# fnutil

Lightweight functional-style helpers for Python 3.13+.

Four composable primitives:

- **`Expression[T]`** — wraps a single value in a chainable, Option-aware container with arithmetic and comparison operators.
- **`Iterator[T]`** — wraps any iterable with a lazy, chainable adapter API modelled on Rust's `Iterator` trait.
- **`Result[T, E]`** — represents either success (`Ok[T]`) or failure (`Err[E]`), modelled on Rust's `Result` type.
- **`Lazy[T]`** — defers evaluation of a zero-argument callable until the value is first accessed.

## Installation

```
pip install fnutil
```

```
uv add fnutil
```

## Quick start

```python
from fnutil import expr, it, ok, err, lazy

# Expression: wrap → transform → unwrap
result = (
    expr(10)
    .map(lambda x: x * 2)
    .filter(lambda x: x > 15)
    .unwrap_or(0)
)
assert result == 20

# Iterator: wrap → chain lazy adapters → collect
total = (
    it(range(10))
    .filter(lambda x: x % 2 == 0)
    .map(lambda x: x ** 2)
    .fold(0, lambda acc, x: acc + x)
)
assert total == 120

# Result: explicit success / failure without exceptions
def parse_int(s: str):
    return ok(int(s)) if s.isdigit() else err(f"not a number: {s!r}")

assert parse_int("42").map(lambda x: x * 2).unwrap_or(0) == 84
assert parse_int("abc").map(lambda x: x * 2).unwrap_or(0) == 0

# Lazy: defer expensive work until needed
import time
slow = lazy(lambda: time.sleep(0) or "done")
assert slow.is_evaluated is False
assert slow.value == "done"
assert slow.is_evaluated is True
```

---

## `Expression[T]`

```python
from fnutil import expr
```

### Construction

```python
e = expr(42)        # Expression[int]
e = expr(None)      # Expression[None]
e = expr([1, 2, 3]) # Expression[list[int]]
```

### Transform

| Method | Description |
|---|---|
| `.map(fn)` | Apply `fn` to the value; return `Expression[U]` |
| `.flat_map(fn)` | Apply `fn` which returns `Expression[U]`; unwrap one level |
| `.pipe(f, g, h)` | Thread value through functions left-to-right |
| `.then(fn)` | Apply `fn` and return the **raw** result (exits the container) |
| `.inspect(fn)` | Call `fn` for side-effects; return `self` unchanged |
| `.zip(other)` | Pair with another `Expression`; return `Expression[tuple[T, U]]` |
| `.flatten()` | Unwrap `Expression[Expression[U]]` → `Expression[U]` |
| `.try_map(fn)` | Apply `fn`; return `Ok(result)` on success or `Err(exc)` on exception |

### Option-like

| Method / Property | Description |
|---|---|
| `.is_some` | `True` when value is not `None` |
| `.is_none` | `True` when value is `None` |
| `.filter(fn)` | Return `self` if `fn(value)` is truthy, else `Expression(None)` |
| `.or_else(default)` | Return `self` when `is_some`, else `Expression(default)` |
| `.or_else_with(fn)` | Return `self` when `is_some`, else `Expression(fn())` |
| `.unwrap_or(default)` | Return inner value when `is_some`, else `default` (raw) |

### Iterator interop

```python
expr([1, 2, 3]).iter()   # → Iterator[int]
expr(range(5)).iter()    # → Iterator[int]
```

### Operators

Arithmetic and comparison operators delegate to the inner value. Both operands must be `Expression` instances.

```python
expr(3) + expr(4)   # Expression(7)
expr(10) / expr(4)  # Expression(2.5)
expr(2) ** expr(8)  # Expression(256)
-expr(5)            # Expression(-5)
abs(expr(-7))       # Expression(7)

expr(1) < expr(2)   # True
expr(3) >= expr(3)  # True
sorted([expr(3), expr(1), expr(2)])  # [Expression(1), Expression(2), Expression(3)]
```

### Equality

`Expression` equality compares inner values. Comparing with a raw value returns `False`.

```python
expr(1) == expr(1)  # True
expr(1) == 1        # False
```

---

## `Iterator[T]`

```python
from fnutil import it
```

### Construction

```python
it([1, 2, 3])
it(range(10))
it(x for x in some_generator())

# Factory methods on `it`
it.repeat(0, times=3)      # → Iterator yielding 0, 0, 0
it.repeat(1)               # → infinite Iterator of 1s (use .take(n))
it.count(start=0, step=1)  # → infinite 0, 1, 2, …
it.empty()                 # → empty Iterator[T]
```

### Lazy adapters (return `Iterator`)

| Method | Description |
|---|---|
| `.filter(fn)` | Keep elements where `fn` is truthy |
| `.map(fn)` | Transform each element |
| `.enumerate()` | Yield `(index, element)` tuples |
| `.zip(other)` | Zip with another iterable (truncates to shortest) |
| `.zip_longest(other, fillvalue=None)` | Zip, padding the shorter side |
| `.chain(other)` | Concatenate another iterable |
| `.take(n)` | First `n` elements |
| `.skip(n)` | Skip first `n` elements |
| `.take_while(fn)` | Take elements while `fn` is truthy |
| `.skip_while(fn)` | Skip elements while `fn` is truthy |
| `.flat_map(fn)` | Map `fn` then flatten one level |
| `.flatten()` | Flatten one level of nesting |
| `.inspect(fn)` | Side-effect on each element; pass through unchanged |
| `.filter_false(fn)` | Keep elements where `fn` is falsy |
| `.accumulate(fn=None, *, initial=None)` | Running accumulated values |
| `.cycle()` | Repeat endlessly |
| `.batched(n)` | Non-overlapping tuples of length `n` |
| `.pairwise()` | Successive overlapping pairs |
| `.compress(selectors)` | Keep elements whose selector is truthy |
| `.starmap(fn)` | Unpack each element as `fn(*item)` |
| `.group_by(fn=None)` | Group consecutive elements by key |
| `[i]` / `[start:stop:step]` | Integer index (take) or slice (islice) |

### Consuming terminators

| Method | Returns | Description |
|---|---|---|
| `.collect(factory)` | `U` | e.g. `.collect(list)`, `.collect(set)` |
| `.collect_expr(factory)` | `Expression[U]` | Same, wrapped in `Expression` |
| `.fold(init, fn)` | `U` | Left-fold with initial value |
| `.reduce(fn)` | `T \| None` | Left-fold without init; `None` on empty |
| `.for_each(fn)` | `None` | Consume, calling `fn` on each element |
| `.count()` | `int` | Number of elements |
| `.sum()` | `T` | Sum of all elements |
| `.product()` | `T` | Product of all elements |
| `.unzip()` | `(list, list)` | Split an iterator of pairs into two lists |
| `.find(fn)` | `T \| None` | First element where `fn` is truthy |
| `.any(fn)` | `bool` | True if any element satisfies `fn` |
| `.all(fn)` | `bool` | True if all elements satisfy `fn` |
| `.min()` | `T \| None` | Minimum element; `None` if empty |
| `.max()` | `T \| None` | Maximum element; `None` if empty |
| `.nth(n)` | `T \| None` | Element at zero-based index `n`; `None` if out of range |
| `.last()` | `T \| None` | Last element; `None` if empty |
| `.partition(fn)` | `(list[T], list[T])` | `(truthy, falsy)` split |
| `.value` | `Iterable[T]` | Raw underlying iterable |

### Examples

```python
from fnutil import it

it([1, 2, 3]).map(lambda x: x * 2).collect(list)   # [2, 4, 6]
it(range(5)).sum()                                   # 10
it(range(1, 6)).product()                            # 120
it([(1, "a"), (2, "b")]).unzip()                     # ([1, 2], ['a', 'b'])
it.count(step=2).take(4).collect(list)               # [0, 2, 4, 6]

# Group consecutive words by first letter
words = ["ant", "bee", "bat", "cat"]
for letter, group in it(sorted(words)).group_by(lambda w: w[0]):
    print(letter, group.collect(list))
# a ['ant']
# b ['bat', 'bee']
# c ['cat']

# Batch into chunks
it(range(7)).batched(3).collect(list)
# [(0, 1, 2), (3, 4, 5), (6,)]
```

---

## `Result[T, E]`

```python
from fnutil import ok, err, Ok, Err, Result
```

Represents either a successful value (`Ok[T]`) or a failure (`Err[E]`). Modelled on Rust's `Result` type.

### Construction

```python
ok(42)       # Ok[int]
err("oops")  # Err[str]
Ok(42)       # same as ok(42)
Err("oops")  # same as err("oops")
```

### Properties

| Property | Description |
|---|---|
| `.is_ok` | `True` for `Ok` values |
| `.is_err` | `True` for `Err` values |

For `Ok` instances: `.value` returns the inner value.
For `Err` instances: `.error` returns the inner error.

### Methods

| Method | Description |
|---|---|
| `.map(fn)` | Apply `fn` to the value if `Ok`; pass `Err` through unchanged |
| `.map_err(fn)` | Apply `fn` to the error if `Err`; pass `Ok` through unchanged |
| `.flat_map(fn)` | Apply `fn` (which returns a `Result`) if `Ok`; pass `Err` through |
| `.inspect(fn)` | Call `fn` for side-effects if `Ok`; return `self` |
| `.inspect_err(fn)` | Call `fn` for side-effects if `Err`; return `self` |
| `.unwrap_or(default)` | Return value if `Ok`, else `default` |
| `.unwrap_or_else(fn)` | Return value if `Ok`, else `fn(error)` |
| `.unwrap()` | Return value if `Ok`, else raise `ValueError` |
| `.expect(msg)` | Return value if `Ok`, else raise `ValueError(msg)` |
| `.ok()` | Return `Expression[T \| None]` — value if `Ok`, else `None` |
| `.err_value()` | Return `Expression[E \| None]` — error if `Err`, else `None` |

### Examples

```python
from fnutil import ok, err

# Pipeline — errors short-circuit
result = (
    ok("42")
    .map(int)
    .map(lambda x: x * 2)
    .unwrap_or(0)
)
assert result == 84

result = (
    err("bad input")
    .map(int)               # skipped
    .map(lambda x: x * 2)  # skipped
    .unwrap_or(0)
)
assert result == 0

# isinstance checks
assert isinstance(ok(1), Ok)
assert isinstance(err("x"), Err)
assert isinstance(ok(1), Result)

# Convert to Expression
ok(10).ok()           # Expression(10)
err("x").ok()         # Expression(None)
err("x").err_value()  # Expression('x')
```

---

## `Lazy[T]`

```python
from fnutil import lazy, Lazy
```

Defers evaluation of a zero-argument callable until `.value` or `.force()` is first accessed. The result is cached — the callable is invoked at most once.

### Construction

```python
lz = lazy(lambda: expensive_computation())
lz = Lazy(lambda: expensive_computation())
```

### Properties and methods

| Method / Property | Description |
|---|---|
| `.is_evaluated` | `True` after the value has been computed |
| `.value` | Compute (if needed) and return the value |
| `.force()` | Alias for `.value` |
| `.map(fn)` | Return a new `Lazy` that applies `fn` to the value when forced |
| `.flat_map(fn)` | Return a new `Lazy` that applies `fn` (→ `Lazy[U]`) and forces it |
| `.pipe(f, g, h)` | Return a new `Lazy` that threads the value through functions |
| `.expr()` | Force the value and wrap it in an `Expression` |

### Examples

```python
from fnutil import lazy

calls = []
lz = lazy(lambda: calls.append(1) or 42)
assert lz.is_evaluated is False

assert lz.value == 42   # evaluated here
assert lz.value == 42   # cached — callable not called again
assert len(calls) == 1

# Lazy transformation pipeline (nothing runs until .force())
result = (
    lazy(lambda: 10)
    .map(lambda x: x * 2)
    .map(lambda x: x + 5)
    .force()
)
assert result == 25
```

---

## Type checking

`fnutil` ships a `py.typed` marker and `.pyi` stub files for all modules. It is fully compatible with Pyright and mypy.

## Requirements

- Python 3.13+
- No runtime dependencies

## License

MIT
