pythonic-fp-fptools 5.0.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.
@@ -0,0 +1,38 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Pythonic FP - Functional Programming Tools
16
+
17
+ Functions as first class objects, Lazy (non-strict) function evaluation,
18
+ and classical Functional Programming data structures.
19
+
20
+ +--------------------------+------------------------------+
21
+ | Description | Module |
22
+ +==========================+==============================+
23
+ | Function manipulation | pythonic_fp.fptools.function |
24
+ +--------------------------+------------------------------+
25
+ | Lazy function evaluation | pythonic_fp.fptools.lazy |
26
+ +--------------------------+------------------------------+
27
+ | Maybe monad | pythonic_fp.fptools.maybe |
28
+ +--------------------------+------------------------------+
29
+ | Either monad | pythonic_fp.fptools.either |
30
+ +--------------------------+------------------------------+
31
+ | State monad | pythonic_fp.fptools.state |
32
+ +--------------------------+------------------------------+
33
+
34
+ """
35
+
36
+ __author__ = 'Geoffrey R. Scheller'
37
+ __copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
38
+ __license__ = 'Apache License 2.0'
@@ -0,0 +1,274 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ __all__ = ['Either', 'LEFT', 'RIGHT']
18
+
19
+ from collections.abc import Callable, Iterator, Sequence
20
+ from typing import cast, Never, overload, TypeVar
21
+ from pythonic_fp.singletons.sbool import SBool, Truth as Left, Lie as Right
22
+ from pythonic_fp.singletons.sentinel import Sentinel as _Sentinel
23
+ from .maybe import MayBe
24
+
25
+ L = TypeVar('L', covariant=True)
26
+ R = TypeVar('R', covariant=True)
27
+
28
+ LEFT = Left('LEFT')
29
+ RIGHT = Right('RIGHT')
30
+
31
+
32
+ class Either[L, R]:
33
+ """Either monad, data structure semantically containing either a left
34
+ or a right value, but not both.
35
+
36
+ Implements a left biased Either Monad.
37
+
38
+ - `Either(value: +L, LEFT)` produces a left `Either`
39
+ - `Either(value: +L, RIGHT)` produces a right `Either`
40
+
41
+ In a Boolean context
42
+
43
+ - `True` if a left `Either`
44
+ - `False` if a right `Either`
45
+
46
+ Two `Either` objects compare as equal when
47
+
48
+ - both are left values or both are right values whose values
49
+
50
+ - are the same object
51
+ - compare as equal
52
+
53
+ Immutable, an `Either` does not change after being created. Therefore
54
+ map & bind return new instances
55
+
56
+ .. warning::
57
+
58
+ The contained value need not be immutable, therefore
59
+ not hashable if value is mutable.
60
+
61
+ .. note::
62
+
63
+ ``Either(value: +L, side: Left): Either[+L, +R] -> left: Either[+L, +R]``
64
+ ``Either(value: +R, side: Right): Either[+L, +R] -> right: Either[+L, +R]``
65
+
66
+ """
67
+ __slots__ = '_value', '_side'
68
+ __match_args__ = ('_value', '_side')
69
+
70
+ U = TypeVar('U', covariant=True)
71
+ V = TypeVar('V', covariant=True)
72
+ T = TypeVar('T')
73
+
74
+ @overload
75
+ def __init__(self, value: L, side: Left) -> None: ...
76
+ @overload
77
+ def __init__(self, value: R, side: Right) -> None: ...
78
+
79
+ def __init__(self, value: L | R, side: SBool = LEFT) -> None:
80
+ self._value = value
81
+ self._side = side
82
+
83
+ def __hash__(self) -> int:
84
+ return hash((_Sentinel('XOR'), self._value, self._side))
85
+
86
+ def __bool__(self) -> bool:
87
+ return self._side == LEFT
88
+
89
+ def __iter__(self) -> Iterator[L]:
90
+ if self:
91
+ yield cast(L, self._value)
92
+
93
+ def __repr__(self) -> str:
94
+ if self:
95
+ return 'Either(' + repr(self._value) + ', LEFT)'
96
+ return 'Either(' + repr(self._value) + ', RIGHT)'
97
+
98
+ def __str__(self) -> str:
99
+ if self:
100
+ return '< ' + str(self._value) + ' | >'
101
+ return '< | ' + str(self._value) + ' >'
102
+
103
+ def __len__(self) -> int:
104
+ """An Either always contains just one value."""
105
+ return 1
106
+
107
+ def __eq__(self, other: object) -> bool:
108
+ if not isinstance(other, type(self)):
109
+ return False
110
+
111
+ if self and other:
112
+ if (self._value is other._value) or (self._value == other._value):
113
+ return True
114
+
115
+ if not self and not other:
116
+ if (self._value is other._value) or (self._value == other._value):
117
+ return True
118
+
119
+ return False
120
+
121
+ def get(self) -> L | Never:
122
+ """Get value if a left.
123
+
124
+ .. warning::
125
+
126
+ Unsafe method ``get``. Will raise ``ValueError`` if ``Either``
127
+ is a right. Best practice is to first check the ``Either`` in
128
+ a boolean context.
129
+
130
+ :return: its value if a Left
131
+ :rtype: +L
132
+ :raises ValueError: if not a left
133
+
134
+ """
135
+ if self._side == RIGHT:
136
+ msg = 'Either: get method called on a right valued Either'
137
+ raise ValueError(msg)
138
+ return cast(L, self._value)
139
+
140
+ def get_left(self) -> MayBe[L]:
141
+ """Get value of `Either` if a left. Safer version of `get` method.
142
+
143
+ - if `Either` contains a left value, return it wrapped in a MayBe
144
+ - if `Either` contains a right value, return MayBe()
145
+
146
+ """
147
+ if self._side == LEFT:
148
+ return MayBe(cast(L, self._value))
149
+ return MayBe()
150
+
151
+ def get_right(self) -> MayBe[R]:
152
+ """Get value of `Either` if a right
153
+
154
+ - if `Either` contains a right value, return it wrapped in a MayBe
155
+ - if `Either` contains a left value, return MayBe()
156
+
157
+ """
158
+ if self._side == RIGHT:
159
+ return MayBe(cast(R, self._value))
160
+ return MayBe()
161
+
162
+ def map_right[V](self, f: Callable[[R], V]) -> Either[L, V]:
163
+ """Construct new Either with a different right."""
164
+ if self._side == LEFT:
165
+ return cast(Either[L, V], self)
166
+ return Either[L, V](f(cast(R, self._value)), RIGHT)
167
+
168
+ def map[U](self, f: Callable[[L], U]) -> Either[U, R]:
169
+ """Map over if a left value. Return new instance."""
170
+ if self._side == RIGHT:
171
+ return cast(Either[U, R], self)
172
+ return Either(f(cast(L, self._value)), LEFT)
173
+
174
+ def bind[U](self, f: Callable[[L], Either[U, R]]) -> Either[U, R]:
175
+ """Flatmap over the left value, propagate right values."""
176
+ if self:
177
+ return f(cast(L, self._value))
178
+ return cast(Either[U, R], self)
179
+
180
+ def map_except[U](self, f: Callable[[L], U], fallback_right: R) -> Either[U, R]:
181
+ """Map over if a left value - with fallback upon exception.
182
+
183
+ - if `Either` is a left then map `f` over its value
184
+
185
+ - if `f` successful return a left `Either[+U, +R]`
186
+ - if `f` unsuccessful return right `Either[+U, +R]`
187
+
188
+ - swallows many exceptions `f` may throw at run time
189
+
190
+ - if `Either` is a right
191
+
192
+ - return new `Either(right=self._right): Either[+U, +R]`
193
+
194
+ """
195
+ if self._side == RIGHT:
196
+ return cast(Either[U, R], self)
197
+
198
+ applied: MayBe[Either[U, R]] = MayBe()
199
+ fall_back: MayBe[Either[U, R]] = MayBe()
200
+ try:
201
+ applied = MayBe(Either(f(cast(L, self._value)), LEFT))
202
+ except (
203
+ LookupError,
204
+ ValueError,
205
+ TypeError,
206
+ BufferError,
207
+ ArithmeticError,
208
+ RecursionError,
209
+ ReferenceError,
210
+ RuntimeError,
211
+ ):
212
+ fall_back = MayBe(cast(Either[U, R], Either(fallback_right, RIGHT)))
213
+
214
+ if fall_back:
215
+ return fall_back.get()
216
+ return applied.get()
217
+
218
+ def bind_except[U](
219
+ self, f: Callable[[L], Either[U, R]], fallback_right: R
220
+ ) -> Either[U, R]:
221
+ """Flatmap `Either` with function `f` with fallback right
222
+
223
+ .. warning::
224
+ Swallows exceptions.
225
+
226
+ :param fallback_right: fallback value if exception thrown
227
+
228
+ """
229
+ if self._side == RIGHT:
230
+ return cast(Either[U, R], self)
231
+
232
+ applied: MayBe[Either[U, R]] = MayBe()
233
+ fall_back: MayBe[Either[U, R]] = MayBe()
234
+ try:
235
+ if self:
236
+ applied = MayBe(f(cast(L, self._value)))
237
+ except (
238
+ LookupError,
239
+ ValueError,
240
+ TypeError,
241
+ BufferError,
242
+ ArithmeticError,
243
+ RecursionError,
244
+ ReferenceError,
245
+ RuntimeError,
246
+ ):
247
+ fall_back = MayBe(cast(Either[U, R], Either(fallback_right, RIGHT)))
248
+
249
+ if fall_back:
250
+ return fall_back.get()
251
+ return applied.get()
252
+
253
+ @staticmethod
254
+ def sequence[U, V](
255
+ sequence_xor_uv: Sequence[Either[U, V]],
256
+ ) -> Either[Sequence[U], V]:
257
+ """Sequence an indexable of type `Either[~U, ~V]`
258
+
259
+ If the iterated `Either` values are all lefts, then return an `Either` of
260
+ an iterable of the left values. Otherwise return a right Either containing
261
+ the first right encountered.
262
+
263
+ """
264
+ list_items: list[U] = []
265
+
266
+ for xor_uv in sequence_xor_uv:
267
+ if xor_uv:
268
+ list_items.append(xor_uv.get())
269
+ else:
270
+ return Either(xor_uv.get_right().get(), RIGHT)
271
+
272
+ sequence_type = cast(Sequence[U], type(sequence_xor_uv))
273
+
274
+ return Either(sequence_type(list_items)) # type: ignore # subclass will be callable
@@ -0,0 +1,83 @@
1
+ # Copyright 2024-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Pythonic FP - FP tools for functions
16
+
17
+ Not a replacement for the std library's `functools` which is more about
18
+ modifying function behavior through decorators than functional composition
19
+ and application.
20
+
21
+ FP utilities to manipulate and partially apply functions
22
+
23
+ - *function* **swap** - Swap the arguments of a 2 argument function
24
+ - *function* **sequenced** - Convert function to take a sequence of its arguments
25
+ - *function* **negate** - Transforms a predicate to its negation
26
+ - *function* **partial** - Returns a partially applied function
27
+
28
+ """
29
+
30
+ from __future__ import annotations
31
+ from collections.abc import Callable
32
+ from typing import Any, ParamSpec
33
+
34
+ __all__ = ['swap', 'sequenced', 'partial', 'negate']
35
+
36
+ P = ParamSpec('P')
37
+
38
+
39
+ def swap[U, V, R](f: Callable[[U, V], R]) -> Callable[[V, U], R]:
40
+ """Swap arguments of a two argument function."""
41
+ return lambda v, u: f(u, v)
42
+
43
+
44
+ def negate[**P](f: Callable[P, bool]) -> Callable[P, bool]:
45
+ """Take a predicate and return its negation."""
46
+
47
+ def ff(*args: P.args, **kwargs: P.kwargs) -> bool:
48
+ return not f(*args, **kwargs)
49
+
50
+ return ff
51
+
52
+
53
+ def sequenced[R](f: Callable[..., R]) -> Callable[[tuple[Any]], R]:
54
+ """Convert a function with arbitrary positional arguments to one taking
55
+ a tuple of the original arguments.
56
+
57
+ - was awaiting typing and mypy "improvements" to ParamSpec
58
+
59
+ - return type: Callable[tuple[P.args], R] ???
60
+ - return type: Callable[[tuple[P.args]], R] ???
61
+
62
+ - not going to happen, `see <https://github.com/python/mypy/pull/18278>`_
63
+
64
+ TODO: Look into replacing this function with a Callable class?
65
+
66
+ """
67
+ def ff(tupled_args: tuple[Any]) -> R:
68
+ return f(*tupled_args)
69
+
70
+ return ff
71
+
72
+
73
+ def partial[**P, R](f: Callable[P, R], *args: Any) -> Callable[..., R]:
74
+ """Partially apply arguments to a function, left to right.
75
+
76
+ - type-wise the only thing guaranteed is the return type
77
+ - best practice is to cast the result immediately
78
+
79
+ """
80
+ def finish(*rest: Any) -> R:
81
+ return sequenced(f)(args + rest)
82
+
83
+ return finish
@@ -0,0 +1,177 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Pythonic FP - Lazy function evaluation
16
+
17
+ Delayed function evaluations. FP tools for "non-strict" function evaluations.
18
+ Useful to delay a function's evaluation until some inner scope.
19
+
20
+ Non-strict delayed function evaluation.
21
+
22
+ - *class* **Lazy** - Delay evaluation of functions taking & returning single values
23
+ - *function* **lazy** - Delay evaluation of functions taking any number of values
24
+ - *function* **real_lazy** - Version of ``lazy`` which caches its result
25
+
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from collections.abc import Callable
31
+ from typing import Any, Final, Never, TypeVar, ParamSpec
32
+ from .function import sequenced
33
+ from .either import Either, LEFT, RIGHT
34
+ from .maybe import MayBe
35
+
36
+ __all__ = ['Lazy', 'lazy', 'real_lazy']
37
+
38
+ D = TypeVar('D')
39
+ R = TypeVar('R', contravariant=True)
40
+ P = ParamSpec('P')
41
+
42
+
43
+ class Lazy[D, R]:
44
+ """Delayed evaluation of a singled valued function.
45
+
46
+ Class instance delays the executable of a function where ``Lazy(f, arg)``
47
+ constructs an object that can evaluate the Callable ``f`` with its argument
48
+ at a later time.
49
+
50
+ - first argument ``f`` taking values of type ``D`` to values of type ``R``
51
+ - second argument ``arg: D`` is the argument to be passed to ``f``
52
+
53
+ - where the type ``D`` is the ``tuple`` type of the argument types to ``f``
54
+
55
+ - function is evaluated when the ``eval`` method is called
56
+ - result is cached unless ``pure`` is set to ``False``
57
+ - returns True in Boolean context if evaluated
58
+
59
+ Usually use case is to make a function "non-strict" by passing some of its
60
+ arguments wrapped in Lazy instances.
61
+
62
+ """
63
+
64
+ __slots__ = ('_f', '_d', '_result', '_pure', '_evaluated', '_exceptional')
65
+
66
+ def __init__(self, f: Callable[[D], R], d: D, pure: bool = True) -> None:
67
+ self._f: Final[Callable[[D], R]] = f
68
+ self._d: Final[D] = d
69
+ self._pure: bool = pure
70
+ self._evaluated: bool = False
71
+ self._exceptional: MayBe[bool] = MayBe()
72
+ self._result: Either[R, Exception]
73
+
74
+ def __bool__(self) -> bool:
75
+ return self._evaluated
76
+
77
+ def eval(self) -> None:
78
+ """Evaluate function with its argument.
79
+
80
+ - evaluate function
81
+ - cache result or exception if ``pure == True``
82
+ - reevaluate if ``pure == False``
83
+
84
+ """
85
+ if not (self._pure and self._evaluated):
86
+ try:
87
+ result = self._f(self._d)
88
+ except Exception as exc:
89
+ self._result, self._evaluated, self._exceptional = (
90
+ Either(exc, RIGHT),
91
+ True,
92
+ MayBe(True),
93
+ )
94
+ else:
95
+ self._result, self._evaluated, self._exceptional = (
96
+ Either(result, LEFT),
97
+ True,
98
+ MayBe(False),
99
+ )
100
+
101
+ def got_result(self) -> MayBe[bool]:
102
+ """Return true if an evaluated Lazy did not raise an exception."""
103
+ return self._exceptional.bind(lambda x: MayBe(not x))
104
+
105
+ def got_exception(self) -> MayBe[bool]:
106
+ """Return true if Lazy raised exception."""
107
+ return self._exceptional
108
+
109
+ def get(self, alt: R | None = None) -> R | Never:
110
+ """Get result only if evaluated and no exceptions occurred, otherwise
111
+ return an alternate value.
112
+
113
+ A possible use case would be if the calculation is expensive, but if it
114
+ has already been done, its result is better than the alternate value.
115
+
116
+ """
117
+ if self._evaluated and self._result:
118
+ return self._result.get()
119
+ if alt is not None:
120
+ return alt
121
+ msg = 'Lazy: method get needed an alternate value but none given.'
122
+ raise ValueError(msg)
123
+
124
+ def get_result(self) -> MayBe[R]:
125
+ """Get result only if evaluated and not exceptional."""
126
+ if self._evaluated and self._result:
127
+ return self._result.get_left()
128
+ return MayBe()
129
+
130
+ def get_exception(self) -> MayBe[Exception]:
131
+ """Get result only if evaluate and exceptional."""
132
+ if self._evaluated and not self._result:
133
+ return self._result.get_right()
134
+ return MayBe()
135
+
136
+
137
+ def lazy[**P, R](
138
+ f: Callable[P, R], *args: P.args, **kwargs: P.kwargs
139
+ ) -> Lazy[tuple[Any, ...], R]:
140
+ """Delayed evaluation of a function with arbitrary positional arguments.
141
+
142
+ Function returning a delayed evaluation of a function of an arbitrary number
143
+ of positional arguments.
144
+
145
+ - first positional argument ``f`` takes a function
146
+ - next positional arguments are the arguments to be applied later to ``f``
147
+
148
+ - ``f`` is reevaluated whenever ``eval`` method of the returned ``Lazy`` is called
149
+
150
+ - any kwargs passed are ignored
151
+
152
+ - if ``f`` needs them, then wrap ``f`` in another function
153
+
154
+ """
155
+ return Lazy(sequenced(f), args, pure=False)
156
+
157
+
158
+ def real_lazy[**P, R](
159
+ f: Callable[P, R], *args: P.args, **kwargs: P.kwargs
160
+ ) -> Lazy[tuple[Any, ...], R]:
161
+ """Cached delayed evaluation of a function with arbitrary positional arguments.
162
+
163
+ Function returning a delayed evaluation of a function of an arbitrary number
164
+ of positional arguments.
165
+
166
+ - first positional argument ``f`` takes a function
167
+ - next positional arguments are the arguments to be applied later to ``f``
168
+
169
+ - ``f`` is evaluated when ``eval`` method of the returned ``Lazy`` is called
170
+ - ``f`` is evaluated only once with results cached
171
+
172
+ - any kwargs passed are ignored
173
+
174
+ - if ``f`` needs them then wrap ``f`` in another function
175
+
176
+ """
177
+ return Lazy(sequenced(f), args)
@@ -0,0 +1,141 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Pythonic FP - Maybe Monad"""
16
+
17
+ from __future__ import annotations
18
+
19
+ __all__ = ['MayBe']
20
+
21
+ from collections.abc import Callable, Iterator, Sequence
22
+ from typing import cast, Final, Never, overload, TypeVar
23
+ from pythonic_fp.singletons.sentinel import Sentinel
24
+
25
+ D = TypeVar('D', covariant=True)
26
+
27
+
28
+ class MayBe[D]:
29
+ """Maybe monad, data structure wrapping a potentially missing value.
30
+
31
+ Immutable semantics
32
+
33
+ - can store any item of any type, including ``None``
34
+ - can store any value of any type with one exception
35
+ - immutable semantics, therefore made covariant
36
+
37
+ .. warning::
38
+
39
+ Hashability invalidated if contained value is not hashable.
40
+
41
+ """
42
+
43
+ U = TypeVar('U', covariant=True)
44
+ V = TypeVar('V', covariant=True)
45
+ T = TypeVar('T')
46
+
47
+ __slots__ = ('_value',)
48
+ __match_args__ = ('_value',)
49
+
50
+ @overload
51
+ def __init__(self) -> None: ...
52
+ @overload
53
+ def __init__(self, value: D) -> None: ...
54
+
55
+ def __init__(self, value: D | Sentinel = Sentinel('MayBe')) -> None:
56
+ self._value: D | Sentinel = value
57
+
58
+ def __hash__(self) -> int:
59
+ return hash((Sentinel('MayBe'), self._value))
60
+
61
+ def __bool__(self) -> bool:
62
+ return self._value is not Sentinel('MayBe')
63
+
64
+ def __iter__(self) -> Iterator[D]:
65
+ if self:
66
+ yield cast(D, self._value)
67
+
68
+ def __repr__(self) -> str:
69
+ if self:
70
+ return 'MayBe(' + repr(self._value) + ')'
71
+ return 'MayBe()'
72
+
73
+ def __len__(self) -> int:
74
+ return 1 if self else 0
75
+
76
+ def __eq__(self, other: object) -> bool:
77
+ if not isinstance(other, type(self)):
78
+ return False
79
+ if self._value is other._value:
80
+ return True
81
+ if self._value == other._value:
82
+ return True
83
+ return False
84
+
85
+ @overload
86
+ def get(self) -> D | Never: ...
87
+ @overload
88
+ def get(self, alt: D) -> D: ...
89
+
90
+ def get(self, alt: D | Sentinel = Sentinel('MayBe')) -> D | Never:
91
+ """Return the contained value if it exists, otherwise an alternate value.
92
+
93
+ .. warning::
94
+
95
+ Unsafe method ``get``. Will raise ``ValueError`` if MayBe empty
96
+ and an alt return value not given. Best practice is to first check
97
+ the MayBe in a boolean context.
98
+
99
+ :raises ValueError: when an alternate value is not provided but needed
100
+
101
+ """
102
+ _sentinel: Final[Sentinel] = Sentinel('MayBe')
103
+ if self._value is not _sentinel:
104
+ return cast(D, self._value)
105
+ if alt is _sentinel:
106
+ msg = 'MayBe: an alternate return type not provided'
107
+ raise ValueError(msg)
108
+ return cast(D, alt)
109
+
110
+ def map[U](self, f: Callable[[D], U]) -> MayBe[U]:
111
+ """Map function `f` over contents."""
112
+
113
+ if self:
114
+ return MayBe(f(cast(D, self._value)))
115
+ return cast(MayBe[U], self)
116
+
117
+ def bind[U](self, f: Callable[[D], MayBe[U]]) -> MayBe[U]:
118
+ """Flatmap ``MayBe`` with function ``f``."""
119
+ return f(cast(D, self._value)) if self else cast(MayBe[U], self)
120
+
121
+ @staticmethod
122
+ def sequence[U](sequence_mb_u: Sequence[MayBe[U]]) -> MayBe[Sequence[U]]:
123
+ """Sequence a mutable indexable of type ``MayBe[~U]``
124
+
125
+ If the iterated `MayBe` values are not all empty,
126
+
127
+ - return a MayBe of the Sequence subtype of the contained values
128
+ - otherwise return an empty MayBe
129
+
130
+ """
131
+ list_items: list[U] = list()
132
+
133
+ for mb_u in sequence_mb_u:
134
+ if mb_u:
135
+ list_items.append(mb_u.get())
136
+ else:
137
+ return MayBe()
138
+
139
+ sequence_type = cast(Sequence[U], type(sequence_mb_u))
140
+
141
+ return MayBe(sequence_type(list_items)) # type: ignore # subclass will be callable
File without changes
@@ -0,0 +1,156 @@
1
+ # Copyright 2024-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Pythonic FP - State Monad
16
+
17
+ Handling state functionally.
18
+
19
+ ##### *class* State - Classic FP State Monad
20
+
21
+ A pure FP immutable implementation for the State Monad.
22
+
23
+ - translated to Python from the book "Functional Programming in Scala"
24
+
25
+ - authors Chiusana & Bjarnason
26
+ - run "action" returns a tuple ``(a, s)`` reversed to the type ``State[S, A]``
27
+
28
+ - the standard convention seen in the FP community
29
+ - another "factoid" to remember
30
+
31
+ - choose the name ``bind`` instead of ``flatmap``
32
+
33
+ - the ``flatmap`` name is misleading for non-container-like monads
34
+ - ``flatmap`` name too long, ``bind`` shorter to type
35
+
36
+ - without "do-notation", code tends to march to the right
37
+
38
+ - typing for the ``modify`` class method may be a bit suspect
39
+
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ __all__ = ['State']
45
+
46
+ from collections.abc import Callable
47
+ from typing import TypeVar
48
+ from pythonic_fp.circulararray.auto import CA
49
+
50
+ S = TypeVar('S')
51
+ A = TypeVar('A')
52
+ B = TypeVar('B')
53
+ C = TypeVar('C')
54
+ ST = TypeVar('ST')
55
+ AA = TypeVar('AA')
56
+
57
+
58
+ class State[S, A]:
59
+ """Data structure generating values while propagating changes of state.
60
+
61
+ - class ``State`` represents neither a state nor (value, state) pair
62
+
63
+ - it wraps a transformation old_state -> (value, new_state)
64
+ - the ``run`` method is this wrapped transformation
65
+ - ``bind`` is just state propagating function composition
66
+
67
+ """
68
+
69
+ __slots__ = ('run',)
70
+
71
+ def __init__(self, run: Callable[[S], tuple[A, S]]) -> None:
72
+ self.run = run
73
+
74
+ def bind[B](self, g: Callable[[A], State[S, B]]) -> State[S, B]:
75
+ """Perform function composition while propagating state."""
76
+
77
+ def compose(s: S) -> tuple[B, S]:
78
+ a, s = self.run(s)
79
+ return g(a).run(s)
80
+
81
+ return State(compose)
82
+
83
+ def eval(self, init: S) -> A:
84
+ """Evaluate the Monad via passing an initial state."""
85
+ a, _ = self.run(init)
86
+ return a
87
+
88
+ def map[B](self, f: Callable[[A], B]) -> State[S, B]:
89
+ """Map a function over a run action."""
90
+ return self.bind(lambda a: State.unit(f(a)))
91
+
92
+ def map2[B, C](self, sb: State[S, B], f: Callable[[A, B], C]) -> State[S, C]:
93
+ """Map a function of two variables over two state actions."""
94
+ return self.bind(lambda a: sb.map(lambda b: f(a, b)))
95
+
96
+ def both[B](self, rb: State[S, B]) -> State[S, tuple[A, B]]:
97
+ """Return a tuple of two state actions."""
98
+ return self.map2(rb, lambda a, b: (a, b))
99
+
100
+ @staticmethod
101
+ def unit[ST, B](b: B) -> State[ST, B]:
102
+ """Create a State action returning the given value."""
103
+ return State(lambda s: (b, s))
104
+
105
+ @staticmethod
106
+ def get[ST]() -> State[ST, ST]:
107
+ """Set run action to return the current state
108
+
109
+ - the current state is propagated unchanged
110
+ - current value now set to current state
111
+ - will need type annotation
112
+
113
+ """
114
+ return State[ST, ST](lambda s: (s, s))
115
+
116
+ @staticmethod
117
+ def put[ST](s: ST) -> State[ST, tuple[()]]:
118
+ """Manually insert a state.
119
+
120
+ THe run action.
121
+
122
+ - ignores previous state and swaps in a new state
123
+ - assigns a canonically meaningless value to current value
124
+
125
+ """
126
+ return State(lambda _: ((), s))
127
+
128
+ @staticmethod
129
+ def modify[ST](f: Callable[[ST], ST]) -> State[ST, tuple[()]]:
130
+ """Modify previous state.
131
+
132
+ - like put, but modify previous state via ``f``
133
+ - will need type annotation
134
+
135
+ - mypy has no "a priori" way to know what ST is
136
+
137
+ """
138
+ return State.get().bind(lambda a: State.put(f(a))) # type: ignore
139
+
140
+ @staticmethod
141
+ def sequence[ST, AA](sas: list[State[ST, AA]]) -> State[ST, list[AA]]:
142
+ """Combine a list of state actions into a state action of a list.
143
+
144
+ - all state actions must be of the same type
145
+ - run method evaluates list front to back
146
+
147
+ """
148
+
149
+ def append_ret(ls: list[AA], a: AA) -> list[AA]:
150
+ ls.append(a)
151
+ return ls
152
+
153
+ return CA(sas).foldl(
154
+ lambda s1, sa: s1.map2(sa, append_ret),
155
+ State.unit(list[AA]([]))
156
+ )
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythonic-fp-fptools
3
+ Version: 5.0.0
4
+ Summary: Pythonic FP - Functional Programming Tools
5
+ Keywords: either,fp,functional,functional programming,lazy,maybe,monad,non-strict
6
+ Author-email: "Geoffrey R. Scheller" <geoffrey@scheller.com>
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/x-rst
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: Pytest
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ License-File: LICENSE
17
+ Requires-Dist: pythonic-fp-circulararray>=5.3.0
18
+ Requires-Dist: pythonic-fp-singletons>=1.0.0
19
+ Requires-Dist: pytest>=8.4.1 ; extra == "test"
20
+ Requires-Dist: pythonic-fp-containers>=3.0.0 ; extra == "test"
21
+ Project-URL: Changelog, https://github.com/grscheller/pythonic-fp-fptools/blob/main/CHANGELOG.rst
22
+ Project-URL: Documentation, https://grscheller.github.io/pythonic-fp/fptools/development/build/html/releases.html
23
+ Project-URL: Homepage, https://github.com/grscheller/pythonic-fp/blob/main/README.md
24
+ Project-URL: Source, https://github.com/grscheller/pythonic-fp-fptools
25
+ Provides-Extra: test
26
+
27
+ Pythonic FP - Functional tools
28
+ ==============================
29
+
30
+ PyPI project
31
+ `pythonic-fp.fptools
32
+ <https://pypi.org/project/pythonic-fp.fptools>`_.
33
+
34
+ Tools to aid with functional programming in Python yet still endeavoring to
35
+ remain Pythonic.
36
+
37
+ - Functions as first class objects
38
+ - Lazy (non-strict) function evaluation
39
+ - Maybe monad - representing a possible missing value
40
+ - Either monad - left bias either monad representing either a LEFT or RIGHT value
41
+ - State monad implementation
42
+
43
+ - pure FP handling of state (the state monad)
44
+ - Classic FP implementation
45
+
46
+ - the monad encapsulates a state transformation, not a "state"
47
+
48
+ This PyPI project is part of of the grscheller
49
+ `pythonic-fp namespace projects
50
+ <https://github.com/grscheller/pythonic-fp/blob/main/README.md>`_
51
+
52
+ **Warning:** The maintainer intends to break out the first, forth and
53
+ fifth modules to their own repos sometime in the near future.
54
+
55
+ Documentation
56
+ -------------
57
+
58
+ Documentation for this project is hosted on
59
+ `GitHub Pages
60
+ <https://grscheller.github.io/pythonic-fp/fptools/development/build/html>`_.
61
+
62
+ Copyright and License
63
+ ---------------------
64
+
65
+ Copyright (c) 2023-2025 Geoffrey R. Scheller. Licensed under the Apache
66
+ License, Version 2.0. See the LICENSE file for details.
67
+
@@ -0,0 +1,11 @@
1
+ pythonic_fp/fptools/__init__.py,sha256=wMmxz9DMh1r27bJxoPtP46YCMVRLLsERv2gXORAf8Uk,1685
2
+ pythonic_fp/fptools/either.py,sha256=Au32tb7NeyPsgmB17d3Ic6svVWwJ8w0cQd3k34QpQVs,8502
3
+ pythonic_fp/fptools/function.py,sha256=tqJkWNyeK-ifMA-gCa4rSmuyS5yuFBOHFB8pfL7YpCI,2652
4
+ pythonic_fp/fptools/lazy.py,sha256=Z-bUsMy0EF56cToZIOlRz4Nz-F1YoYw29dvVMIfJpcI,6152
5
+ pythonic_fp/fptools/maybe.py,sha256=C-QWzyt74ztKj0gNaipiJ7ZqRXmFiUc_8GRLmeeGj7s,4320
6
+ pythonic_fp/fptools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ pythonic_fp/fptools/state.py,sha256=nulrtZRmGcB65l7-ZfxpmRNKPflhRzrwIBujAUILRnw,4717
8
+ pythonic_fp_fptools-5.0.0.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
9
+ pythonic_fp_fptools-5.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
10
+ pythonic_fp_fptools-5.0.0.dist-info/METADATA,sha256=UGLIvIrh8IBAzTzgmrzvLpmvtbBFLK1TCBXNldkoF_I,2490
11
+ pythonic_fp_fptools-5.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,176 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS