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 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
+ [![PyPI version](https://img.shields.io/pypi/v/fnkit)](https://pypi.org/project/fnkit/)
23
+ [![Python versions](https://img.shields.io/pypi/pyversions/fnkit)](https://pypi.org/project/fnkit/)
24
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
25
+ [![Tests](https://img.shields.io/github/actions/workflow/status/vmuminov/fnkit/tests.yml?label=tests)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.29
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any