pyeffect 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyeffect-0.1.0/PKG-INFO +138 -0
- pyeffect-0.1.0/README.md +129 -0
- pyeffect-0.1.0/pyproject.toml +24 -0
- pyeffect-0.1.0/src/pyeffect/__init__.py +69 -0
- pyeffect-0.1.0/src/pyeffect/compose.py +342 -0
- pyeffect-0.1.0/src/pyeffect/effect.py +279 -0
- pyeffect-0.1.0/src/pyeffect/option.py +265 -0
- pyeffect-0.1.0/src/pyeffect/pipe.py +150 -0
- pyeffect-0.1.0/src/pyeffect/py.typed +0 -0
- pyeffect-0.1.0/src/pyeffect/result.py +481 -0
- pyeffect-0.1.0/src/pyeffect/retry.py +69 -0
pyeffect-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pyeffect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A fully typed functional core for Python: Result, Option, Effect, and composition utilities.
|
|
5
|
+
Author: Tomperez98
|
|
6
|
+
Author-email: Tomperez98 <tomasperezalvarez@gmail.com>
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# pyeffect
|
|
11
|
+
|
|
12
|
+
Handle expected failures as **values** — a fully typed `Result`, `Option`,
|
|
13
|
+
and lazy `Effect` core for Python 3.14+.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pyeffect import Err, Ok, Result, attempt
|
|
17
|
+
|
|
18
|
+
result: Result[int, str] = attempt(lambda: int("42")).map(lambda n: n * 2)
|
|
19
|
+
match result:
|
|
20
|
+
case Ok(value):
|
|
21
|
+
print(value) # 84
|
|
22
|
+
case Err(error):
|
|
23
|
+
print(f"failed: {error}")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What it is
|
|
27
|
+
|
|
28
|
+
`pyeffect` is a functional core for Python that turns expected failure
|
|
29
|
+
(network errors, bad input, absence) into values you can compose, instead of
|
|
30
|
+
exceptions you must catch. `Result` and `Option` handle the data; `Effect`
|
|
31
|
+
defers side effects until you run them.
|
|
32
|
+
|
|
33
|
+
### The one rule
|
|
34
|
+
|
|
35
|
+
> **Bugs panic, expected failures return values.**
|
|
36
|
+
|
|
37
|
+
- **Panic** when the program reaches a state that should be impossible —
|
|
38
|
+
`unwrap()` on `Err`/`Nothing`, a broken retry `Policy`, an over-arity
|
|
39
|
+
`curry`. The damage stops at the exact line.
|
|
40
|
+
- **Return a value** when failure is expected — network errors, bad input,
|
|
41
|
+
absence. The caller decides what to do with it.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
Not on PyPI yet — clone the repo and install from source:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv sync
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That installs `pyeffect` and the dev dependencies (pytest, ruff, ty) into
|
|
52
|
+
`.venv`.
|
|
53
|
+
|
|
54
|
+
## A complete example
|
|
55
|
+
|
|
56
|
+
Paste this and it runs — expected output is in the comments:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pyeffect import Effect, Err, Ok, Result, attempt, from_optional
|
|
60
|
+
|
|
61
|
+
# Expected failure is a value the caller handles.
|
|
62
|
+
result: Result[int, str] = attempt(lambda: int("42")).map(lambda n: n * 2)
|
|
63
|
+
match result:
|
|
64
|
+
case Ok(value):
|
|
65
|
+
print(value) # 84
|
|
66
|
+
case Err(error):
|
|
67
|
+
print(f"failed: {error}")
|
|
68
|
+
|
|
69
|
+
# Absence is a value too.
|
|
70
|
+
assert from_optional({"a": 1}.get("b")).unwrap_or(0) == 0
|
|
71
|
+
|
|
72
|
+
# Side effects are deferred until you run them.
|
|
73
|
+
effect = (
|
|
74
|
+
Effect.attempt(lambda: open("config.json").read())
|
|
75
|
+
.map(str.strip)
|
|
76
|
+
.context("while loading config")
|
|
77
|
+
.catch(lambda e: Effect.success("{}"))
|
|
78
|
+
)
|
|
79
|
+
print(effect.run()) # {} — the read failed, the fallback ran
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## What's inside
|
|
83
|
+
|
|
84
|
+
| Module | Types |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `pyeffect.result` | `Ok` / `Err` / `Result[T, E]`, `attempt`, `guard`, `traverse`, `flatten`, `transpose`, `ErrorContext` |
|
|
87
|
+
| `pyeffect.option` | `Some` / `Nothing` / `Option[T]`, `from_optional`, `flatten`, `transpose` |
|
|
88
|
+
| `pyeffect.effect` | `Effect[T, E]` — a lazy, re-runnable computation; `sequence`, `attempt`, `retry_result` |
|
|
89
|
+
| `pyeffect.retry` | `retry` + `Policy` — deterministic, injectable backoff |
|
|
90
|
+
| `pyeffect.pipe` | `pipe(value, f, g, ...)` — left-to-right threading |
|
|
91
|
+
| `pyeffect.compose` | `compose`, `curry`, `lift` / `lift2` / `lift3`, `identity`, `tap`, `flip`, `unpack`, `constant`, `partial` |
|
|
92
|
+
|
|
93
|
+
The combinators are methods: `Effect` carries `map`, `and_then`, `catch`,
|
|
94
|
+
`context`, `inspect`, `inspect_err`, `map_or`, `flatten`, `zip`, and `retry`;
|
|
95
|
+
every variant has the eager boolean combinators `and_` / `or_` (trailing
|
|
96
|
+
underscore because `and`/`or` are Python keywords), plus `Option.xor`.
|
|
97
|
+
|
|
98
|
+
`flatten` and `transpose` are module-level functions on `result` and
|
|
99
|
+
`option` (`from pyeffect.result import flatten, transpose`) because Python's
|
|
100
|
+
invariant generics make the equivalent methods untypeable at call sites.
|
|
101
|
+
|
|
102
|
+
## `Effect` is the impurity boundary
|
|
103
|
+
|
|
104
|
+
Python is eager: every call executes immediately, so purity is lost the
|
|
105
|
+
moment you touch IO or time. `Effect` restores it — **constructing and
|
|
106
|
+
composing run nothing; `run()` / `run_result()` are the only impure
|
|
107
|
+
moments.** Dependencies (readers, clocks, backends) are captured in the
|
|
108
|
+
thunk's closure, and `sleep` is injected, so tests run with zero delay and
|
|
109
|
+
no real I/O.
|
|
110
|
+
|
|
111
|
+
The library is **sync-first by design**: the value layer (`Result`,
|
|
112
|
+
`Option`, `pipe`, `compose`) is pure and async-agnostic, and `Effect` is a
|
|
113
|
+
sync thunk over `Result`. An `AsyncEffect` sibling (awaitable thunks,
|
|
114
|
+
`asyncio.sleep`-injected retry) is planned for when network use-cases
|
|
115
|
+
demand it — not before.
|
|
116
|
+
|
|
117
|
+
## Typing notes
|
|
118
|
+
|
|
119
|
+
Every combinator is checked with [`ty`](https://github.com/astral-sh/ty) as
|
|
120
|
+
part of the test suite (`tests/typing/` pins the contracts, including
|
|
121
|
+
intentional failures that must stay rejected). Known constraints of Python
|
|
122
|
+
generics, documented in the module docstrings:
|
|
123
|
+
|
|
124
|
+
- **No variance in PEP 695** — `Effect.success` / `Effect.failure` leave the
|
|
125
|
+
unbound slot as `Any`; anchor the chain with an annotation or an operation
|
|
126
|
+
that fixes the slot (`attempt`'s `catch`, `map_err`, `context`).
|
|
127
|
+
- **No higher-kinded types** — there are no generic `Functor` / `Monad`
|
|
128
|
+
typeclasses; each type implements `map` / `and_then` structurally.
|
|
129
|
+
- **Fixed arity ceilings** — `pipe` / `compose` type-check up to ten
|
|
130
|
+
functions, `curry` up to five; the runtimes accept more.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
uv run pytest # runtime + doctest tests
|
|
136
|
+
uv run ty check src tests # typing contract; also gates the tests/typing/ fixtures
|
|
137
|
+
uv run ruff check src tests
|
|
138
|
+
```
|
pyeffect-0.1.0/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# pyeffect
|
|
2
|
+
|
|
3
|
+
Handle expected failures as **values** — a fully typed `Result`, `Option`,
|
|
4
|
+
and lazy `Effect` core for Python 3.14+.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from pyeffect import Err, Ok, Result, attempt
|
|
8
|
+
|
|
9
|
+
result: Result[int, str] = attempt(lambda: int("42")).map(lambda n: n * 2)
|
|
10
|
+
match result:
|
|
11
|
+
case Ok(value):
|
|
12
|
+
print(value) # 84
|
|
13
|
+
case Err(error):
|
|
14
|
+
print(f"failed: {error}")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What it is
|
|
18
|
+
|
|
19
|
+
`pyeffect` is a functional core for Python that turns expected failure
|
|
20
|
+
(network errors, bad input, absence) into values you can compose, instead of
|
|
21
|
+
exceptions you must catch. `Result` and `Option` handle the data; `Effect`
|
|
22
|
+
defers side effects until you run them.
|
|
23
|
+
|
|
24
|
+
### The one rule
|
|
25
|
+
|
|
26
|
+
> **Bugs panic, expected failures return values.**
|
|
27
|
+
|
|
28
|
+
- **Panic** when the program reaches a state that should be impossible —
|
|
29
|
+
`unwrap()` on `Err`/`Nothing`, a broken retry `Policy`, an over-arity
|
|
30
|
+
`curry`. The damage stops at the exact line.
|
|
31
|
+
- **Return a value** when failure is expected — network errors, bad input,
|
|
32
|
+
absence. The caller decides what to do with it.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
Not on PyPI yet — clone the repo and install from source:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv sync
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That installs `pyeffect` and the dev dependencies (pytest, ruff, ty) into
|
|
43
|
+
`.venv`.
|
|
44
|
+
|
|
45
|
+
## A complete example
|
|
46
|
+
|
|
47
|
+
Paste this and it runs — expected output is in the comments:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from pyeffect import Effect, Err, Ok, Result, attempt, from_optional
|
|
51
|
+
|
|
52
|
+
# Expected failure is a value the caller handles.
|
|
53
|
+
result: Result[int, str] = attempt(lambda: int("42")).map(lambda n: n * 2)
|
|
54
|
+
match result:
|
|
55
|
+
case Ok(value):
|
|
56
|
+
print(value) # 84
|
|
57
|
+
case Err(error):
|
|
58
|
+
print(f"failed: {error}")
|
|
59
|
+
|
|
60
|
+
# Absence is a value too.
|
|
61
|
+
assert from_optional({"a": 1}.get("b")).unwrap_or(0) == 0
|
|
62
|
+
|
|
63
|
+
# Side effects are deferred until you run them.
|
|
64
|
+
effect = (
|
|
65
|
+
Effect.attempt(lambda: open("config.json").read())
|
|
66
|
+
.map(str.strip)
|
|
67
|
+
.context("while loading config")
|
|
68
|
+
.catch(lambda e: Effect.success("{}"))
|
|
69
|
+
)
|
|
70
|
+
print(effect.run()) # {} — the read failed, the fallback ran
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## What's inside
|
|
74
|
+
|
|
75
|
+
| Module | Types |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `pyeffect.result` | `Ok` / `Err` / `Result[T, E]`, `attempt`, `guard`, `traverse`, `flatten`, `transpose`, `ErrorContext` |
|
|
78
|
+
| `pyeffect.option` | `Some` / `Nothing` / `Option[T]`, `from_optional`, `flatten`, `transpose` |
|
|
79
|
+
| `pyeffect.effect` | `Effect[T, E]` — a lazy, re-runnable computation; `sequence`, `attempt`, `retry_result` |
|
|
80
|
+
| `pyeffect.retry` | `retry` + `Policy` — deterministic, injectable backoff |
|
|
81
|
+
| `pyeffect.pipe` | `pipe(value, f, g, ...)` — left-to-right threading |
|
|
82
|
+
| `pyeffect.compose` | `compose`, `curry`, `lift` / `lift2` / `lift3`, `identity`, `tap`, `flip`, `unpack`, `constant`, `partial` |
|
|
83
|
+
|
|
84
|
+
The combinators are methods: `Effect` carries `map`, `and_then`, `catch`,
|
|
85
|
+
`context`, `inspect`, `inspect_err`, `map_or`, `flatten`, `zip`, and `retry`;
|
|
86
|
+
every variant has the eager boolean combinators `and_` / `or_` (trailing
|
|
87
|
+
underscore because `and`/`or` are Python keywords), plus `Option.xor`.
|
|
88
|
+
|
|
89
|
+
`flatten` and `transpose` are module-level functions on `result` and
|
|
90
|
+
`option` (`from pyeffect.result import flatten, transpose`) because Python's
|
|
91
|
+
invariant generics make the equivalent methods untypeable at call sites.
|
|
92
|
+
|
|
93
|
+
## `Effect` is the impurity boundary
|
|
94
|
+
|
|
95
|
+
Python is eager: every call executes immediately, so purity is lost the
|
|
96
|
+
moment you touch IO or time. `Effect` restores it — **constructing and
|
|
97
|
+
composing run nothing; `run()` / `run_result()` are the only impure
|
|
98
|
+
moments.** Dependencies (readers, clocks, backends) are captured in the
|
|
99
|
+
thunk's closure, and `sleep` is injected, so tests run with zero delay and
|
|
100
|
+
no real I/O.
|
|
101
|
+
|
|
102
|
+
The library is **sync-first by design**: the value layer (`Result`,
|
|
103
|
+
`Option`, `pipe`, `compose`) is pure and async-agnostic, and `Effect` is a
|
|
104
|
+
sync thunk over `Result`. An `AsyncEffect` sibling (awaitable thunks,
|
|
105
|
+
`asyncio.sleep`-injected retry) is planned for when network use-cases
|
|
106
|
+
demand it — not before.
|
|
107
|
+
|
|
108
|
+
## Typing notes
|
|
109
|
+
|
|
110
|
+
Every combinator is checked with [`ty`](https://github.com/astral-sh/ty) as
|
|
111
|
+
part of the test suite (`tests/typing/` pins the contracts, including
|
|
112
|
+
intentional failures that must stay rejected). Known constraints of Python
|
|
113
|
+
generics, documented in the module docstrings:
|
|
114
|
+
|
|
115
|
+
- **No variance in PEP 695** — `Effect.success` / `Effect.failure` leave the
|
|
116
|
+
unbound slot as `Any`; anchor the chain with an annotation or an operation
|
|
117
|
+
that fixes the slot (`attempt`'s `catch`, `map_err`, `context`).
|
|
118
|
+
- **No higher-kinded types** — there are no generic `Functor` / `Monad`
|
|
119
|
+
typeclasses; each type implements `map` / `and_then` structurally.
|
|
120
|
+
- **Fixed arity ceilings** — `pipe` / `compose` type-check up to ten
|
|
121
|
+
functions, `curry` up to five; the runtimes accept more.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
uv run pytest # runtime + doctest tests
|
|
127
|
+
uv run ty check src tests # typing contract; also gates the tests/typing/ fixtures
|
|
128
|
+
uv run ruff check src tests
|
|
129
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyeffect"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A fully typed functional core for Python: Result, Option, Effect, and composition utilities."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Tomperez98", email = "tomasperezalvarez@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
14
|
+
build-backend = "uv_build"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=9.1.1",
|
|
19
|
+
"ruff>=0.16.5",
|
|
20
|
+
"ty>=0.0.77",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""pyeffect: a fully typed functional core for Python."""
|
|
2
|
+
|
|
3
|
+
from pyeffect.compose import (
|
|
4
|
+
compose,
|
|
5
|
+
constant,
|
|
6
|
+
curry,
|
|
7
|
+
flip,
|
|
8
|
+
identity,
|
|
9
|
+
lift,
|
|
10
|
+
lift2,
|
|
11
|
+
lift3,
|
|
12
|
+
partial,
|
|
13
|
+
tap,
|
|
14
|
+
unpack,
|
|
15
|
+
)
|
|
16
|
+
from pyeffect.effect import Effect, sequence
|
|
17
|
+
from pyeffect.option import (
|
|
18
|
+
Nothing,
|
|
19
|
+
Option,
|
|
20
|
+
Some,
|
|
21
|
+
UnwrapNothingError,
|
|
22
|
+
flatten,
|
|
23
|
+
from_optional,
|
|
24
|
+
)
|
|
25
|
+
from pyeffect.pipe import pipe
|
|
26
|
+
from pyeffect.result import (
|
|
27
|
+
Err,
|
|
28
|
+
ErrorContext,
|
|
29
|
+
Ok,
|
|
30
|
+
Result,
|
|
31
|
+
UnwrapError,
|
|
32
|
+
attempt,
|
|
33
|
+
guard,
|
|
34
|
+
traverse,
|
|
35
|
+
)
|
|
36
|
+
from pyeffect.retry import Policy, retry
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Effect",
|
|
40
|
+
"Err",
|
|
41
|
+
"ErrorContext",
|
|
42
|
+
"Nothing",
|
|
43
|
+
"Ok",
|
|
44
|
+
"Option",
|
|
45
|
+
"Policy",
|
|
46
|
+
"Result",
|
|
47
|
+
"Some",
|
|
48
|
+
"UnwrapError",
|
|
49
|
+
"UnwrapNothingError",
|
|
50
|
+
"attempt",
|
|
51
|
+
"compose",
|
|
52
|
+
"constant",
|
|
53
|
+
"curry",
|
|
54
|
+
"flatten",
|
|
55
|
+
"flip",
|
|
56
|
+
"from_optional",
|
|
57
|
+
"guard",
|
|
58
|
+
"identity",
|
|
59
|
+
"lift",
|
|
60
|
+
"lift2",
|
|
61
|
+
"lift3",
|
|
62
|
+
"partial",
|
|
63
|
+
"pipe",
|
|
64
|
+
"retry",
|
|
65
|
+
"sequence",
|
|
66
|
+
"tap",
|
|
67
|
+
"traverse",
|
|
68
|
+
"unpack",
|
|
69
|
+
]
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# ruff: noqa: UP047 -- PEP 695 type params on @overload are not checked by ty;
|
|
2
|
+
# classic TypeVars required. Non-overload functions below use PEP 695.
|
|
3
|
+
"""Function composition: ``compose``, ``tap``, and small combinators.
|
|
4
|
+
|
|
5
|
+
``compose`` builds a new function from existing ones, right to left::
|
|
6
|
+
|
|
7
|
+
>>> from pyeffect.compose import compose
|
|
8
|
+
>>> compose(str, lambda x: x + 1)(2)
|
|
9
|
+
'3'
|
|
10
|
+
|
|
11
|
+
``tap`` runs a side effect on a value and passes it through unchanged — a
|
|
12
|
+
convenient debug step inside a ``pipe``.
|
|
13
|
+
|
|
14
|
+
``curry`` turns a multi-argument function into nested single-argument
|
|
15
|
+
calls; ``lift``/``lift2``/``lift3`` push plain functions into the
|
|
16
|
+
``Result`` domain::
|
|
17
|
+
|
|
18
|
+
>>> from pyeffect.compose import curry, lift2
|
|
19
|
+
>>> from pyeffect.result import Ok
|
|
20
|
+
>>> curry(lambda a, b: a + b)(2)(3)
|
|
21
|
+
5
|
|
22
|
+
>>> lift2(lambda a, b: a + b)(Ok(1), Ok(2))
|
|
23
|
+
Ok(value=3)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import inspect
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
from functools import partial
|
|
31
|
+
from typing import Any, TypeVar, overload
|
|
32
|
+
|
|
33
|
+
from pyeffect.result import Err, Ok, Result
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"compose",
|
|
37
|
+
"constant",
|
|
38
|
+
"curry",
|
|
39
|
+
"flip",
|
|
40
|
+
"identity",
|
|
41
|
+
"lift",
|
|
42
|
+
"lift2",
|
|
43
|
+
"lift3",
|
|
44
|
+
"partial",
|
|
45
|
+
"tap",
|
|
46
|
+
"unpack",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
_A = TypeVar("_A")
|
|
50
|
+
_B = TypeVar("_B")
|
|
51
|
+
_C = TypeVar("_C")
|
|
52
|
+
_D = TypeVar("_D")
|
|
53
|
+
_E = TypeVar("_E")
|
|
54
|
+
_F = TypeVar("_F")
|
|
55
|
+
_G = TypeVar("_G")
|
|
56
|
+
_H = TypeVar("_H")
|
|
57
|
+
_I = TypeVar("_I")
|
|
58
|
+
_J = TypeVar("_J")
|
|
59
|
+
_K = TypeVar("_K")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@overload
|
|
63
|
+
def compose() -> Callable[[_A], _A]: ...
|
|
64
|
+
@overload
|
|
65
|
+
def compose(f: Callable[[_A], _B]) -> Callable[[_A], _B]: ...
|
|
66
|
+
@overload
|
|
67
|
+
def compose(f: Callable[[_A], _B], g: Callable[[_C], _A]) -> Callable[[_C], _B]: ...
|
|
68
|
+
@overload
|
|
69
|
+
def compose(
|
|
70
|
+
f: Callable[[_A], _B],
|
|
71
|
+
g: Callable[[_C], _A],
|
|
72
|
+
h: Callable[[_D], _C],
|
|
73
|
+
) -> Callable[[_D], _B]: ...
|
|
74
|
+
@overload
|
|
75
|
+
def compose(
|
|
76
|
+
f: Callable[[_A], _B],
|
|
77
|
+
g: Callable[[_C], _A],
|
|
78
|
+
h: Callable[[_D], _C],
|
|
79
|
+
i: Callable[[_E], _D],
|
|
80
|
+
) -> Callable[[_E], _B]: ...
|
|
81
|
+
@overload
|
|
82
|
+
def compose(
|
|
83
|
+
f: Callable[[_A], _B],
|
|
84
|
+
g: Callable[[_C], _A],
|
|
85
|
+
h: Callable[[_D], _C],
|
|
86
|
+
i: Callable[[_E], _D],
|
|
87
|
+
j: Callable[[_F], _E],
|
|
88
|
+
) -> Callable[[_F], _B]: ...
|
|
89
|
+
@overload
|
|
90
|
+
def compose(
|
|
91
|
+
f: Callable[[_A], _B],
|
|
92
|
+
g: Callable[[_C], _A],
|
|
93
|
+
h: Callable[[_D], _C],
|
|
94
|
+
i: Callable[[_E], _D],
|
|
95
|
+
j: Callable[[_F], _E],
|
|
96
|
+
k: Callable[[_G], _F],
|
|
97
|
+
) -> Callable[[_G], _B]: ...
|
|
98
|
+
@overload
|
|
99
|
+
def compose(
|
|
100
|
+
f: Callable[[_A], _B],
|
|
101
|
+
g: Callable[[_C], _A],
|
|
102
|
+
h: Callable[[_D], _C],
|
|
103
|
+
i: Callable[[_E], _D],
|
|
104
|
+
j: Callable[[_F], _E],
|
|
105
|
+
k: Callable[[_G], _F],
|
|
106
|
+
l: Callable[[_H], _G],
|
|
107
|
+
) -> Callable[[_H], _B]: ...
|
|
108
|
+
@overload
|
|
109
|
+
def compose(
|
|
110
|
+
f: Callable[[_A], _B],
|
|
111
|
+
g: Callable[[_C], _A],
|
|
112
|
+
h: Callable[[_D], _C],
|
|
113
|
+
i: Callable[[_E], _D],
|
|
114
|
+
j: Callable[[_F], _E],
|
|
115
|
+
k: Callable[[_G], _F],
|
|
116
|
+
l: Callable[[_H], _G],
|
|
117
|
+
m: Callable[[_I], _H],
|
|
118
|
+
) -> Callable[[_I], _B]: ...
|
|
119
|
+
@overload
|
|
120
|
+
def compose(
|
|
121
|
+
f: Callable[[_A], _B],
|
|
122
|
+
g: Callable[[_C], _A],
|
|
123
|
+
h: Callable[[_D], _C],
|
|
124
|
+
i: Callable[[_E], _D],
|
|
125
|
+
j: Callable[[_F], _E],
|
|
126
|
+
k: Callable[[_G], _F],
|
|
127
|
+
l: Callable[[_H], _G],
|
|
128
|
+
m: Callable[[_I], _H],
|
|
129
|
+
n: Callable[[_J], _I],
|
|
130
|
+
) -> Callable[[_J], _B]: ...
|
|
131
|
+
@overload
|
|
132
|
+
def compose(
|
|
133
|
+
f: Callable[[_A], _B],
|
|
134
|
+
g: Callable[[_C], _A],
|
|
135
|
+
h: Callable[[_D], _C],
|
|
136
|
+
i: Callable[[_E], _D],
|
|
137
|
+
j: Callable[[_F], _E],
|
|
138
|
+
k: Callable[[_G], _F],
|
|
139
|
+
l: Callable[[_H], _G],
|
|
140
|
+
m: Callable[[_I], _H],
|
|
141
|
+
n: Callable[[_J], _I],
|
|
142
|
+
o: Callable[[_K], _J],
|
|
143
|
+
) -> Callable[[_K], _B]: ...
|
|
144
|
+
def compose(*functions: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
|
145
|
+
"""Compose functions right to left: ``compose(f, g)(x) == f(g(x))``.
|
|
146
|
+
|
|
147
|
+
``compose()`` (no functions) is the identity function. Up to ten
|
|
148
|
+
functions are fully type-checked; the runtime accepts any number.
|
|
149
|
+
"""
|
|
150
|
+
if not functions:
|
|
151
|
+
return identity
|
|
152
|
+
|
|
153
|
+
def composed(value: Any) -> Any:
|
|
154
|
+
for fn in reversed(functions):
|
|
155
|
+
value = fn(value)
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
return composed
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def identity[A](value: A) -> A:
|
|
162
|
+
"""Return ``value`` unchanged."""
|
|
163
|
+
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def tap[A](fn: Callable[[A], object]) -> Callable[[A], A]:
|
|
168
|
+
"""Return a function that runs ``fn`` on the value, then returns it.
|
|
169
|
+
|
|
170
|
+
The return value of ``fn`` is discarded — ``tap`` is for side effects
|
|
171
|
+
(logging, recording) inside a pipeline.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def tapped(value: A) -> A:
|
|
175
|
+
fn(value)
|
|
176
|
+
return value
|
|
177
|
+
|
|
178
|
+
return tapped
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def constant[A](value: A) -> Callable[..., A]:
|
|
182
|
+
"""Return a function that ignores its arguments and yields ``value``."""
|
|
183
|
+
|
|
184
|
+
def const(*args: object, **kwargs: object) -> A:
|
|
185
|
+
return value
|
|
186
|
+
|
|
187
|
+
return const
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def flip[A, B, C](f: Callable[[A, B], C]) -> Callable[[B, A], C]:
|
|
191
|
+
"""Swap the first two arguments of a binary function."""
|
|
192
|
+
|
|
193
|
+
def flipped(b: B, a: A) -> C:
|
|
194
|
+
return f(a, b)
|
|
195
|
+
|
|
196
|
+
return flipped
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@overload
|
|
200
|
+
def unpack[A, R](f: Callable[[A], R]) -> Callable[[tuple[A]], R]: ...
|
|
201
|
+
@overload
|
|
202
|
+
def unpack[A, B, R](f: Callable[[A, B], R]) -> Callable[[tuple[A, B]], R]: ...
|
|
203
|
+
@overload
|
|
204
|
+
def unpack[A, B, C, R](f: Callable[[A, B, C], R]) -> Callable[[tuple[A, B, C]], R]: ...
|
|
205
|
+
def unpack(f: Callable[..., Any]) -> Callable[[tuple[Any, ...]], Any]:
|
|
206
|
+
"""Return a function that applies ``f`` to the elements of a tuple.
|
|
207
|
+
|
|
208
|
+
``unpack(f)((1, 2))`` is ``f(1, 2)``. Arities 1-3 are fully
|
|
209
|
+
type-checked; the runtime accepts any arity.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def applied(args: tuple[Any, ...]) -> Any:
|
|
213
|
+
return f(*args)
|
|
214
|
+
|
|
215
|
+
return applied
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _positional_arity(f: Callable[..., Any]) -> int:
|
|
219
|
+
"""Count the fixed positional parameters of ``f``.
|
|
220
|
+
|
|
221
|
+
Currying must know when a call has supplied every positional argument.
|
|
222
|
+
A ``*args`` parameter makes that unknowable, and a required
|
|
223
|
+
keyword-only parameter can never be supplied positionally — both are
|
|
224
|
+
defects and crash at construction instead of misbehaving at call time.
|
|
225
|
+
"""
|
|
226
|
+
parameters = inspect.signature(f).parameters.values()
|
|
227
|
+
for parameter in parameters:
|
|
228
|
+
if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
|
|
229
|
+
raise TypeError(f"curry requires a fixed arity, got variadic {f!r}")
|
|
230
|
+
if (
|
|
231
|
+
parameter.kind is inspect.Parameter.KEYWORD_ONLY
|
|
232
|
+
and parameter.default is inspect.Parameter.empty
|
|
233
|
+
):
|
|
234
|
+
raise TypeError(
|
|
235
|
+
f"curry requires positional parameters only, got required "
|
|
236
|
+
f"keyword-only {parameter.name!r} in {f!r}"
|
|
237
|
+
)
|
|
238
|
+
return sum(
|
|
239
|
+
1
|
|
240
|
+
for parameter in parameters
|
|
241
|
+
if parameter.kind
|
|
242
|
+
in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@overload
|
|
247
|
+
def curry[R](f: Callable[[], R]) -> Callable[[], R]: ...
|
|
248
|
+
@overload
|
|
249
|
+
def curry[A, R](f: Callable[[A], R]) -> Callable[[A], R]: ...
|
|
250
|
+
@overload
|
|
251
|
+
def curry[A, B, R](
|
|
252
|
+
f: Callable[[A, B], R],
|
|
253
|
+
) -> Callable[[A], Callable[[B], R]]: ...
|
|
254
|
+
@overload
|
|
255
|
+
def curry[A, B, C, R](
|
|
256
|
+
f: Callable[[A, B, C], R],
|
|
257
|
+
) -> Callable[[A], Callable[[B], Callable[[C], R]]]: ...
|
|
258
|
+
@overload
|
|
259
|
+
def curry[A, B, C, D, R](
|
|
260
|
+
f: Callable[[A, B, C, D], R],
|
|
261
|
+
) -> Callable[[A], Callable[[B], Callable[[C], Callable[[D], R]]]]: ...
|
|
262
|
+
@overload
|
|
263
|
+
def curry[A, B, C, D, E, R](
|
|
264
|
+
f: Callable[[A, B, C, D, E], R],
|
|
265
|
+
) -> Callable[[A], Callable[[B], Callable[[C], Callable[[D], Callable[[E], R]]]]]: ...
|
|
266
|
+
def curry(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
267
|
+
"""Curry ``f`` so each step supplies one positional argument.
|
|
268
|
+
|
|
269
|
+
``curry(f)(a)(b)(c)`` is ``f(a, b, c)``; a step may also supply
|
|
270
|
+
several arguments at once (``curry(f)(a, b)(c)``), and the step that
|
|
271
|
+
completes the arity runs ``f`` immediately. Arities 1-5 are fully
|
|
272
|
+
type-checked; the runtime accepts any fixed arity. Variadic ``*args``
|
|
273
|
+
callables and required keyword-only parameters are rejected at
|
|
274
|
+
construction — the arity is unknowable or unreachable positionally.
|
|
275
|
+
"""
|
|
276
|
+
arity = _positional_arity(f)
|
|
277
|
+
|
|
278
|
+
def curried(*args: Any) -> Any:
|
|
279
|
+
if len(args) >= arity:
|
|
280
|
+
return f(*args)
|
|
281
|
+
return curry(partial(f, *args))
|
|
282
|
+
|
|
283
|
+
return curried
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def lift[T, U, E](
|
|
287
|
+
f: Callable[[T], U],
|
|
288
|
+
) -> Callable[[Result[T, E]], Result[U, E]]:
|
|
289
|
+
"""Lift a unary function into the ``Result`` domain.
|
|
290
|
+
|
|
291
|
+
``lift(f)(r)`` is ``r.map(f)`` as a reusable value — useful when the
|
|
292
|
+
function must be passed somewhere instead of called on a receiver.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
def lifted(result: Result[T, E]) -> Result[U, E]:
|
|
296
|
+
if isinstance(result, Ok):
|
|
297
|
+
return Ok(f(result.value))
|
|
298
|
+
return result
|
|
299
|
+
|
|
300
|
+
return lifted
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def lift2[T, U, R, E](
|
|
304
|
+
f: Callable[[T, U], R],
|
|
305
|
+
) -> Callable[[Result[T, E], Result[U, E]], Result[R, E]]:
|
|
306
|
+
"""Lift a binary function into the ``Result`` domain (applicative style).
|
|
307
|
+
|
|
308
|
+
``lift2(f)(r1, r2)`` applies ``f`` to both values, failing fast on the
|
|
309
|
+
first ``Err``.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
def lifted(first: Result[T, E], second: Result[U, E]) -> Result[R, E]:
|
|
313
|
+
if isinstance(first, Err):
|
|
314
|
+
return first
|
|
315
|
+
if isinstance(second, Err):
|
|
316
|
+
return second
|
|
317
|
+
return Ok(f(first.value, second.value))
|
|
318
|
+
|
|
319
|
+
return lifted
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def lift3[T, U, V, R, E](
|
|
323
|
+
f: Callable[[T, U, V], R],
|
|
324
|
+
) -> Callable[[Result[T, E], Result[U, E], Result[V, E]], Result[R, E]]:
|
|
325
|
+
"""Lift a ternary function into the ``Result`` domain (applicative style).
|
|
326
|
+
|
|
327
|
+
``lift3(f)(r1, r2, r3)`` applies ``f`` to all three values, failing
|
|
328
|
+
fast on the first ``Err``.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def lifted(
|
|
332
|
+
first: Result[T, E], second: Result[U, E], third: Result[V, E]
|
|
333
|
+
) -> Result[R, E]:
|
|
334
|
+
if isinstance(first, Err):
|
|
335
|
+
return first
|
|
336
|
+
if isinstance(second, Err):
|
|
337
|
+
return second
|
|
338
|
+
if isinstance(third, Err):
|
|
339
|
+
return third
|
|
340
|
+
return Ok(f(first.value, second.value, third.value))
|
|
341
|
+
|
|
342
|
+
return lifted
|