fnkit 0.5.0__py3-none-any.whl
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.
- fnkit/__init__.py +0 -0
- fnkit/option.py +54 -0
- fnkit/py.typed +0 -0
- fnkit/result.py +57 -0
- fnkit/state.py +46 -0
- fnkit/transformers.py +47 -0
- fnkit-0.5.0.dist-info/METADATA +154 -0
- fnkit-0.5.0.dist-info/RECORD +9 -0
- fnkit-0.5.0.dist-info/WHEEL +4 -0
fnkit/__init__.py
ADDED
|
File without changes
|
fnkit/option.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Option[T](Protocol):
|
|
7
|
+
def map[U](self, fn: Callable[[T], U]) -> Option[U]: ...
|
|
8
|
+
def bind[U](self, fn: Callable[[T], Option[U]]) -> Option[U]: ...
|
|
9
|
+
def just_or(self, fn: Callable[[], T]) -> T: ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def pure[T](v: T) -> Option[T]:
|
|
13
|
+
return Just(v)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def lift[A, B, C](fn: Callable[[A, B], C]) -> Callable[[Option[A], Option[B]], Option[C]]:
|
|
17
|
+
def _inner(a: Option[A], b: Option[B]) -> Option[C]:
|
|
18
|
+
return a.bind(lambda w: b.map(lambda v: fn(w, v)))
|
|
19
|
+
|
|
20
|
+
return _inner
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class Just[T](Option[T]):
|
|
25
|
+
v: T
|
|
26
|
+
|
|
27
|
+
def map[U](self, fn: Callable[[T], U]) -> Just[U]:
|
|
28
|
+
return Just(fn(self.v))
|
|
29
|
+
|
|
30
|
+
def bind[U](self, fn: Callable[[T], Option[U]]) -> Option[U]:
|
|
31
|
+
return fn(self.v)
|
|
32
|
+
|
|
33
|
+
def just_or(self, fn: Callable[[], T]) -> T:
|
|
34
|
+
return self.v
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Nothing[T](Option[T]):
|
|
38
|
+
def map[U](self, fn: Callable[[T], U]) -> Option[U]:
|
|
39
|
+
return self # type: ignore
|
|
40
|
+
|
|
41
|
+
def bind[U](self, fn: Callable[[T], Option[U]]) -> Option[U]:
|
|
42
|
+
return self # type: ignore
|
|
43
|
+
|
|
44
|
+
def just_or(self, fn: Callable[[], T]) -> T:
|
|
45
|
+
return fn()
|
|
46
|
+
|
|
47
|
+
def __eq__(self, other: Any) -> bool:
|
|
48
|
+
return isinstance(other, Nothing)
|
|
49
|
+
|
|
50
|
+
def __hash__(self) -> int:
|
|
51
|
+
return hash(Nothing)
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return "Nothing()"
|
fnkit/py.typed
ADDED
|
File without changes
|
fnkit/result.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Result[T, E](Protocol):
|
|
7
|
+
def map[U](self, fn: Callable[[T], U]) -> Result[U, E]: ...
|
|
8
|
+
def map_err[F](self, fn: Callable[[E], F]) -> Result[T, F]: ...
|
|
9
|
+
def bind[U](self, fn: Callable[[T], Result[U, E]]) -> Result[U, E]: ...
|
|
10
|
+
def ok_or(self, fn: Callable[[E], T]) -> T: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def pure[T, E = Exception](v: T) -> Result[T, E]:
|
|
14
|
+
return Ok(v)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def lift[A, B, C, E](
|
|
18
|
+
fn: Callable[[A, B], C],
|
|
19
|
+
) -> Callable[[Result[A, E], Result[B, E]], Result[C, E]]:
|
|
20
|
+
def _inner(a: Result[A, E], b: Result[B, E]) -> Result[C, E]:
|
|
21
|
+
return a.bind(lambda w: b.map(lambda v: fn(w, v)))
|
|
22
|
+
|
|
23
|
+
return _inner
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class Ok[T, E](Result[T, E]):
|
|
28
|
+
v: T
|
|
29
|
+
|
|
30
|
+
def map[U](self, fn: Callable[[T], U]) -> Result[U, E]:
|
|
31
|
+
return Ok(fn(self.v))
|
|
32
|
+
|
|
33
|
+
def map_err[F](self, fn: Callable[[E], F]) -> Result[T, F]:
|
|
34
|
+
return self # type: ignore
|
|
35
|
+
|
|
36
|
+
def bind[U](self, fn: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
37
|
+
return fn(self.v)
|
|
38
|
+
|
|
39
|
+
def ok_or(self, fn: Callable[[E], T]) -> T:
|
|
40
|
+
return self.v
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class Err[T, E](Result[T, E]):
|
|
45
|
+
v: E
|
|
46
|
+
|
|
47
|
+
def map[U](self, fn: Callable[[T], U]) -> Result[U, E]:
|
|
48
|
+
return self # type: ignore
|
|
49
|
+
|
|
50
|
+
def bind[U](self, fn: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
51
|
+
return self # type: ignore
|
|
52
|
+
|
|
53
|
+
def ok_or(self, fn: Callable[[E], T]) -> T:
|
|
54
|
+
return fn(self.v)
|
|
55
|
+
|
|
56
|
+
def map_err[F](self, fn: Callable[[E], F]) -> Result[T, F]:
|
|
57
|
+
return Err(fn(self.v))
|
fnkit/state.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from types import NoneType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class State[S, A]:
|
|
8
|
+
run_state: Callable[[S], tuple[A, S]]
|
|
9
|
+
|
|
10
|
+
def map[B](self, fn: Callable[[A], B]) -> State[S, B]:
|
|
11
|
+
def _composed(s: S) -> tuple[B, S]:
|
|
12
|
+
v = self.run_state(s)
|
|
13
|
+
return fn(v[0]), v[1]
|
|
14
|
+
|
|
15
|
+
return State(_composed)
|
|
16
|
+
|
|
17
|
+
def map_state[Y](self, sy: Callable[[S], Y], ys: Callable[[Y], S]) -> State[Y, A]:
|
|
18
|
+
def _inner(y: Y) -> tuple[A, Y]:
|
|
19
|
+
s = ys(y)
|
|
20
|
+
a, s1 = self.run_state(s)
|
|
21
|
+
return a, sy(s1)
|
|
22
|
+
|
|
23
|
+
return State(_inner)
|
|
24
|
+
|
|
25
|
+
def bind[B](self, fn: Callable[[A], State[S, B]]) -> State[S, B]:
|
|
26
|
+
def _inner(s: S) -> tuple[B, S]:
|
|
27
|
+
a, s1 = self.run_state(s)
|
|
28
|
+
return fn(a).run_state(s1)
|
|
29
|
+
|
|
30
|
+
return State(_inner)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def pure[A, S = NoneType](v: A) -> State[S, A]:
|
|
34
|
+
return State(lambda s: (v, s))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def lift[A, B, C, S](fn: Callable[[A, B], C]) -> Callable[[State[S, A], State[S, B]], State[S, C]]:
|
|
38
|
+
def _inner(a: State[S, A], b: State[S, B]) -> State[S, C]:
|
|
39
|
+
def _run_state(s: S) -> tuple[C, S]:
|
|
40
|
+
v1, s1 = a.run_state(s)
|
|
41
|
+
v2, s2 = b.run_state(s1)
|
|
42
|
+
return fn(v1, v2), s2
|
|
43
|
+
|
|
44
|
+
return State(_run_state)
|
|
45
|
+
|
|
46
|
+
return _inner
|
fnkit/transformers.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from fnkit.option import Just, Option
|
|
4
|
+
from fnkit.result import Ok, Result
|
|
5
|
+
from fnkit.state import State
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def r_bindt[S, A, B, E](
|
|
9
|
+
state: State[S, Result[A, E]],
|
|
10
|
+
fn: Callable[[A], State[S, Result[B, E]]],
|
|
11
|
+
) -> State[S, Result[B, E]]:
|
|
12
|
+
"""
|
|
13
|
+
r_bindt :: State s (Result a e) ->
|
|
14
|
+
(a -> State s (Result b e)) ->
|
|
15
|
+
State s (Result b e)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def _inner(s: S) -> tuple[Result[B, E], S]:
|
|
19
|
+
result, s1 = state.run_state(s)
|
|
20
|
+
match result:
|
|
21
|
+
case Ok(value):
|
|
22
|
+
return fn(value).run_state(s1)
|
|
23
|
+
case _:
|
|
24
|
+
return result, s1 # type: ignore
|
|
25
|
+
|
|
26
|
+
return State(_inner)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def o_bindt[S, A, B](
|
|
30
|
+
state: State[S, Option[A]],
|
|
31
|
+
fn: Callable[[A], State[S, Option[B]]],
|
|
32
|
+
) -> State[S, Option[B]]:
|
|
33
|
+
"""
|
|
34
|
+
o_bindt :: State s (Option a) ->
|
|
35
|
+
(a -> State s (Option b)) ->
|
|
36
|
+
State s (Option b)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def _inner(s: S) -> tuple[Option[B], S]:
|
|
40
|
+
result, s1 = state.run_state(s)
|
|
41
|
+
match result:
|
|
42
|
+
case Just(value):
|
|
43
|
+
return fn(value).run_state(s1)
|
|
44
|
+
case _:
|
|
45
|
+
return result, s1 # type: ignore
|
|
46
|
+
|
|
47
|
+
return State(_inner)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fnkit
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Slightly alternative way to write the code
|
|
5
|
+
Keywords: functional,monad,option,result,state,types
|
|
6
|
+
Author: Vitaliy Muminov
|
|
7
|
+
Author-email: Vitaliy Muminov <vitaliy.muminov+github@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Project-URL: Homepage, https://github.com/vmuminov/fnkit
|
|
16
|
+
Project-URL: Repository, https://github.com/vmuminov/fnkit
|
|
17
|
+
Project-URL: Issues, https://github.com/vmuminov/fnkit/issues
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# fnkit
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/fnkit/)
|
|
23
|
+
[](https://pypi.org/project/fnkit/)
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
[](https://github.com/vmuminov/fnkit/actions)
|
|
26
|
+
|
|
27
|
+
Typed functional programming primitives for Python 3.14, built on native PEP 695 generics.
|
|
28
|
+
|
|
29
|
+
`Option`, `Result`, and `State` give you composable, type-checked alternatives to `None`
|
|
30
|
+
checks, exception-driven error handling, and hand-rolled state threading, without a
|
|
31
|
+
class hierarchy to inherit from and without losing type information along the way.
|
|
32
|
+
|
|
33
|
+
## Why this exists
|
|
34
|
+
|
|
35
|
+
Most Python functional-programming libraries predate PEP 695 and still lean on
|
|
36
|
+
`TypeVar` and `Generic[T]`. This library is built from the ground up on Python 3.14's
|
|
37
|
+
native generic syntax (`class Foo[T]`, `def bar[T, E = Default]`), structural
|
|
38
|
+
`Protocol`s instead of inheritance, and `match` statements for unwrapping values.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **`Option[T]`**: a `Just` / `Nothing` pair for values that may or may not exist,
|
|
43
|
+
replacing `None`-based optionality with a type-checked container.
|
|
44
|
+
- **`Result[T, E]`**: an `Ok` / `Err` pair for computations that may fail, replacing
|
|
45
|
+
exception-driven control flow with explicit, inspectable failure values.
|
|
46
|
+
- **`State[S, A]`**: a composable wrapper around `S -> (A, S)` functions, for
|
|
47
|
+
threading state through a pipeline without mutation or global variables.
|
|
48
|
+
- **Monad transformers** (`o_bindt`, `r_bindt`): compose `State` with `Option` or
|
|
49
|
+
`Result` directly, so a stateful computation can short-circuit on `Nothing` or
|
|
50
|
+
`Err` without manual unwrapping at every step.
|
|
51
|
+
- **`map` / `bind` / `lift`** across all three types, for a consistent functional
|
|
52
|
+
interface regardless of which container you are working with.
|
|
53
|
+
- Fully immutable: every type is a frozen, slotted dataclass. No shared mutable
|
|
54
|
+
state, no reference cycles.
|
|
55
|
+
- Structural typing throughout: `Option`, `Result`, and `State` are `Protocol`s, so
|
|
56
|
+
you can write your own conforming implementations without subclassing.
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
Python 3.14 or later.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install fnkit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick examples
|
|
69
|
+
|
|
70
|
+
### Option
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from fnkit.option import Just, Nothing, Option, pure
|
|
74
|
+
|
|
75
|
+
def find_user(name: str) -> Option[str]:
|
|
76
|
+
return Just(name) if name == "ada" else Nothing()
|
|
77
|
+
|
|
78
|
+
result = (
|
|
79
|
+
find_user("ada")
|
|
80
|
+
.map(str.upper)
|
|
81
|
+
.bind(lambda name: Just(f"hello, {name}"))
|
|
82
|
+
.just_or(lambda: "user not found")
|
|
83
|
+
)
|
|
84
|
+
print(result) # hello, ADA
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Result
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from fnkit.result import Err, Ok, Result
|
|
91
|
+
|
|
92
|
+
def parse_positive(raw: str) -> Result[int, str]:
|
|
93
|
+
value = int(raw)
|
|
94
|
+
return Ok(value) if value > 0 else Err(f"{value} is not positive")
|
|
95
|
+
|
|
96
|
+
result = (
|
|
97
|
+
parse_positive("42")
|
|
98
|
+
.map(lambda n: n * 2)
|
|
99
|
+
.map_err(lambda e: f"parse failed: {e}")
|
|
100
|
+
.ok_or(lambda e: -1)
|
|
101
|
+
)
|
|
102
|
+
print(result) # 84
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### State
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from fnkit.state import State, pure
|
|
109
|
+
|
|
110
|
+
def push[T](item: T) -> State[list[T], None]:
|
|
111
|
+
def run(stack: list[T]) -> tuple[None, list[T]]:
|
|
112
|
+
return None, [*stack, item]
|
|
113
|
+
return State(run)
|
|
114
|
+
|
|
115
|
+
def pop[T]() -> State[list[T], T]:
|
|
116
|
+
def run(stack: list[T]) -> tuple[T, list[T]]:
|
|
117
|
+
return stack[-1], stack[:-1]
|
|
118
|
+
return State(run)
|
|
119
|
+
|
|
120
|
+
program = push(1).bind(lambda _: push(2)).bind(lambda _: pop())
|
|
121
|
+
value, final_state = program.run_state([])
|
|
122
|
+
print(value, final_state) # 2 [1]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Combining State with Result or Option
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from fnkit.result import Err, Ok, Result
|
|
129
|
+
from fnkit.state import State
|
|
130
|
+
from fnkit.transformers import r_bindt
|
|
131
|
+
|
|
132
|
+
def read_and_validate(state: State[int, Result[int, str]]) -> State[int, Result[int, str]]:
|
|
133
|
+
def validate(value: int) -> State[int, Result[int, str]]:
|
|
134
|
+
if value < 0:
|
|
135
|
+
return State(lambda s: (Err("negative value"), s))
|
|
136
|
+
return State(lambda s: (Ok(value * 2), s))
|
|
137
|
+
|
|
138
|
+
return r_bindt(state, validate)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Design notes
|
|
142
|
+
|
|
143
|
+
- `Nothing`, `Ok`, and `Err` compare and hash by value, not by identity, since all
|
|
144
|
+
three are plain frozen dataclasses.
|
|
145
|
+
- `State` compares and hashes by function identity, since two closures cannot be
|
|
146
|
+
compared for behavioral equality in general. Do not rely on `State() == State()`
|
|
147
|
+
for anything other than the same object.
|
|
148
|
+
- `lift` for `Option` and `Result` evaluates its first argument before its second,
|
|
149
|
+
so when both sides are `Nothing`/`Err`, the first argument's failure is the one
|
|
150
|
+
that propagates. This matches left-to-right short-circuiting.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT. See `LICENSE` for the full text.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
fnkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
fnkit/option.py,sha256=MnUl3vikjfCy1xlocvuc7f5SzlDcCZP63Pda9UB9aG0,1426
|
|
3
|
+
fnkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
fnkit/result.py,sha256=Oz5wfICCjIKGiY_fnL0zcJ9RAnlTLoOzlpSmhwnu4R8,1610
|
|
5
|
+
fnkit/state.py,sha256=5ABdWNCJZPMEUPNHcS8H2BziFfbPTIMLRdktBsurzbM,1312
|
|
6
|
+
fnkit/transformers.py,sha256=ssQ4LowNLNo7AqI-Pcis7g-mOhByw0wq47SanjTss6k,1237
|
|
7
|
+
fnkit-0.5.0.dist-info/WHEEL,sha256=YR4QUxGCbsyoOnWBVTv-p1I4yc3seKGPev46mvmc8Cg,81
|
|
8
|
+
fnkit-0.5.0.dist-info/METADATA,sha256=TECsT2QHgl5Rl0-OutNiWvoUAF2r3re9IoL--Zkwz0c,5434
|
|
9
|
+
fnkit-0.5.0.dist-info/RECORD,,
|