dtools.fp 1.3.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.
- dtools/fp/__init__.py +34 -0
- dtools/fp/err_handling.py +436 -0
- dtools/fp/function.py +71 -0
- dtools/fp/iterables.py +413 -0
- dtools/fp/lazy.py +126 -0
- dtools/fp/py.typed +0 -0
- dtools/fp/singletons.py +238 -0
- dtools/fp/state.py +107 -0
- dtools_fp-1.3.0.dist-info/LICENSE +176 -0
- dtools_fp-1.3.0.dist-info/METADATA +58 -0
- dtools_fp-1.3.0.dist-info/RECORD +12 -0
- dtools_fp-1.3.0.dist-info/WHEEL +4 -0
dtools/fp/__init__.py
ADDED
@@ -0,0 +1,34 @@
|
|
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
|
+
"""### Package dtools.fp - Pythonic Functional Programming
|
16
|
+
|
17
|
+
Tools to aid with functional programming in Python yet still endeavoring to
|
18
|
+
remain Pythonic.
|
19
|
+
|
20
|
+
#### Modules and sub-packages
|
21
|
+
|
22
|
+
* module dtools.fp.err_handling: monadic maybe and either
|
23
|
+
* module dtools.fp.functions: tools combine and partially apply functions
|
24
|
+
* module dtools.fp.iterables: tools for iterables
|
25
|
+
* module dtools.fp.lazy: lazy (non-strict) function evaluation
|
26
|
+
* module dtools.fp.singletons: useful types with but one instance
|
27
|
+
* module dtools.fp.state: handle state monadically
|
28
|
+
|
29
|
+
"""
|
30
|
+
__version__ = "1.3.0"
|
31
|
+
__author__ = "Geoffrey R. Scheller"
|
32
|
+
__copyright__ = "Copyright (c) 2023-2025 Geoffrey R. Scheller"
|
33
|
+
__license__ = "Apache License 2.0"
|
34
|
+
|
@@ -0,0 +1,436 @@
|
|
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
|
+
"""### Module fp.err_handling - monadic error handling
|
16
|
+
|
17
|
+
Functional data types to use in lieu of exceptions.
|
18
|
+
|
19
|
+
#### Error handling types:
|
20
|
+
|
21
|
+
* class **MB**: Maybe (Optional) monad
|
22
|
+
* class **XOR**: Left biased Either monad
|
23
|
+
|
24
|
+
"""
|
25
|
+
from __future__ import annotations
|
26
|
+
|
27
|
+
__all__ = [ 'MB', 'XOR' ]
|
28
|
+
|
29
|
+
from collections.abc import Callable, Iterator, Sequence
|
30
|
+
from typing import cast, Final, Never, overload
|
31
|
+
from .singletons import Sentinel
|
32
|
+
|
33
|
+
class MB[D]():
|
34
|
+
"""Maybe monad - class wrapping a potentially missing value.
|
35
|
+
|
36
|
+
* where `MB(value)` contains a possible value of type `~D`
|
37
|
+
* `MB()` semantically represent a non-existent or missing value of type `~D`
|
38
|
+
* `MB` objects are self flattening, therefore a `MB` cannot contain a MB
|
39
|
+
* `MB(MB(d)) == MB(d)`
|
40
|
+
* `MB(MB()) == MB()`
|
41
|
+
* immutable, a `MB` does not change after being created
|
42
|
+
* immutable semantics, map & bind return new instances
|
43
|
+
* warning: contained values need not be immutable
|
44
|
+
* warning: not hashable if contained value is mutable
|
45
|
+
|
46
|
+
"""
|
47
|
+
__slots__ = '_value',
|
48
|
+
__match_args__ = '_value',
|
49
|
+
|
50
|
+
@overload
|
51
|
+
def __init__(self) -> None: ...
|
52
|
+
@overload
|
53
|
+
def __init__(self, value: MB[D]) -> None: ...
|
54
|
+
@overload
|
55
|
+
def __init__(self, value: D) -> None: ...
|
56
|
+
|
57
|
+
def __init__(self, value: D|MB[D]|Sentinel=Sentinel('MB')) -> None:
|
58
|
+
self._value: D|Sentinel
|
59
|
+
_sentinel: Final[Sentinel] = Sentinel('MB')
|
60
|
+
match value:
|
61
|
+
case MB(d) if d is not _sentinel:
|
62
|
+
self._value = d
|
63
|
+
case MB(s):
|
64
|
+
self._value = _sentinel
|
65
|
+
case d:
|
66
|
+
self._value = d
|
67
|
+
|
68
|
+
def __bool__(self) -> bool:
|
69
|
+
return self._value is not Sentinel('MB')
|
70
|
+
|
71
|
+
def __iter__(self) -> Iterator[D]:
|
72
|
+
if self:
|
73
|
+
yield cast(D, self._value)
|
74
|
+
|
75
|
+
def __repr__(self) -> str:
|
76
|
+
if self:
|
77
|
+
return 'MB(' + repr(self._value) + ')'
|
78
|
+
else:
|
79
|
+
return 'MB()'
|
80
|
+
|
81
|
+
def __len__(self) -> int:
|
82
|
+
return (1 if self else 0)
|
83
|
+
|
84
|
+
def __eq__(self, other: object) -> bool:
|
85
|
+
if not isinstance(other, type(self)):
|
86
|
+
return False
|
87
|
+
|
88
|
+
if self._value is other._value:
|
89
|
+
return True
|
90
|
+
elif self._value == other._value:
|
91
|
+
return True
|
92
|
+
else:
|
93
|
+
return False
|
94
|
+
|
95
|
+
@overload
|
96
|
+
def get(self) -> D|Never: ...
|
97
|
+
@overload
|
98
|
+
def get(self, alt: D) -> D: ...
|
99
|
+
@overload
|
100
|
+
def get(self, alt: Sentinel) -> D|Never: ...
|
101
|
+
|
102
|
+
def get(self, alt: D|Sentinel=Sentinel('MB')) -> D|Never:
|
103
|
+
"""Return the contained value if it exists, otherwise an alternate value.
|
104
|
+
|
105
|
+
* alternate value must be of type `~D`
|
106
|
+
* raises `ValueError` if an alternate value is not provided but needed
|
107
|
+
|
108
|
+
"""
|
109
|
+
_sentinel: Final[Sentinel] = Sentinel('MB')
|
110
|
+
if self._value is not _sentinel:
|
111
|
+
return cast(D, self._value)
|
112
|
+
else:
|
113
|
+
if alt is _sentinel:
|
114
|
+
msg = 'MB: an alternate return type not provided'
|
115
|
+
raise ValueError(msg)
|
116
|
+
else:
|
117
|
+
return cast(D, alt)
|
118
|
+
|
119
|
+
def map[U](self, f: Callable[[D], U]) -> MB[U]:
|
120
|
+
"""Map function `f` over the 0 or 1 elements of this data structure.
|
121
|
+
|
122
|
+
* if `f` should fail, return a MB()
|
123
|
+
|
124
|
+
"""
|
125
|
+
if self._value is Sentinel('MB'):
|
126
|
+
return cast(MB[U], self)
|
127
|
+
else:
|
128
|
+
try:
|
129
|
+
return MB(f(cast(D, self._value)))
|
130
|
+
except Exception:
|
131
|
+
return MB()
|
132
|
+
|
133
|
+
def bind[U](self, f: Callable[[D], MB[U]]) -> MB[U]:
|
134
|
+
"""Map `MB` with function `f` and flatten."""
|
135
|
+
try:
|
136
|
+
return (f(cast(D, self._value)) if self else MB())
|
137
|
+
except Exception:
|
138
|
+
return MB()
|
139
|
+
|
140
|
+
@staticmethod
|
141
|
+
def call[U, V](f: Callable[[U], V], u: U) -> MB[V]:
|
142
|
+
"""Return an function call wrapped in a MB"""
|
143
|
+
try:
|
144
|
+
return MB(f(u))
|
145
|
+
except Exception:
|
146
|
+
return MB()
|
147
|
+
|
148
|
+
@staticmethod
|
149
|
+
def lz_call[U, V](f: Callable[[U], V], u: U) -> Callable[[], MB[V]]:
|
150
|
+
def ret() -> MB[V]:
|
151
|
+
return MB.call(f, u)
|
152
|
+
return ret
|
153
|
+
|
154
|
+
@staticmethod
|
155
|
+
def idx[V](v: Sequence[V], ii: int) -> MB[V]:
|
156
|
+
"""Return an indexed value wrapped in a MB"""
|
157
|
+
try:
|
158
|
+
return MB(v[ii])
|
159
|
+
except IndexError:
|
160
|
+
return MB()
|
161
|
+
|
162
|
+
@staticmethod
|
163
|
+
def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], MB[V]]:
|
164
|
+
def ret() -> MB[V]:
|
165
|
+
return MB.idx(v, ii)
|
166
|
+
return ret
|
167
|
+
|
168
|
+
@staticmethod
|
169
|
+
def sequence[T](seq_mb_d: Sequence[MB[T]]) -> MB[Sequence[T]]:
|
170
|
+
"""Sequence an indexable container of `MB[~D]`
|
171
|
+
|
172
|
+
* if all the contained `MB` values in the container are not empty,
|
173
|
+
* return a `MB` of a container containing the values contained
|
174
|
+
* otherwise return an empty `MB`
|
175
|
+
|
176
|
+
"""
|
177
|
+
l: list[T] = []
|
178
|
+
|
179
|
+
for mb_d in seq_mb_d:
|
180
|
+
if mb_d:
|
181
|
+
l.append(mb_d.get())
|
182
|
+
else:
|
183
|
+
return MB()
|
184
|
+
|
185
|
+
ds = cast(Sequence[T], type(seq_mb_d)(l)) # type: ignore # will be a subclass at runtime
|
186
|
+
return MB(ds)
|
187
|
+
|
188
|
+
class XOR[L, R]():
|
189
|
+
"""Either monad - class semantically containing either a left or a right
|
190
|
+
value, but not both.
|
191
|
+
|
192
|
+
* implements a left biased Either Monad
|
193
|
+
* `XOR(left: ~L, right: ~R)` produces a left `XOR` which
|
194
|
+
* contains a value of type `~L`
|
195
|
+
* and a potential right value of type `~R`
|
196
|
+
* `XOR(MB(), right)` produces a right `XOR`
|
197
|
+
* in a Boolean context
|
198
|
+
* `True` if a left `XOR`
|
199
|
+
* `False` if a right `XOR`
|
200
|
+
* two `XOR` objects compare as equal when
|
201
|
+
* both are left values or both are right values whose values
|
202
|
+
* are the same object
|
203
|
+
* compare as equal
|
204
|
+
* immutable, an `XOR` does not change after being created
|
205
|
+
* immutable semantics, map & bind return new instances
|
206
|
+
* warning: contained values need not be immutable
|
207
|
+
* warning: not hashable if value or potential right value mutable
|
208
|
+
|
209
|
+
"""
|
210
|
+
__slots__ = '_left', '_right'
|
211
|
+
__match_args__ = ('_left', '_right')
|
212
|
+
|
213
|
+
@overload
|
214
|
+
def __init__(self, left: L, right: R, /) -> None: ...
|
215
|
+
@overload
|
216
|
+
def __init__(self, left: MB[L], right: R, /) -> None: ...
|
217
|
+
|
218
|
+
def __init__(self, left: L|MB[L], right: R, /) -> None:
|
219
|
+
self._left: L|MB[L]
|
220
|
+
self._right: R
|
221
|
+
match left:
|
222
|
+
case MB(l) if l is not Sentinel('MB'):
|
223
|
+
self._left, self._right = cast(L, l), right
|
224
|
+
case MB(s):
|
225
|
+
self._left, self._right = MB(), right
|
226
|
+
case l:
|
227
|
+
self._left, self._right = l, right
|
228
|
+
|
229
|
+
def __bool__(self) -> bool:
|
230
|
+
return MB() != self._left
|
231
|
+
|
232
|
+
def __iter__(self) -> Iterator[L]:
|
233
|
+
if self:
|
234
|
+
yield cast(L, self._left)
|
235
|
+
|
236
|
+
def __repr__(self) -> str:
|
237
|
+
if self:
|
238
|
+
return 'XOR(' + repr(self._left) + ', ' + repr(self._right) + ')'
|
239
|
+
else:
|
240
|
+
return 'XOR(MB(), ' + repr(self._right) + ')'
|
241
|
+
|
242
|
+
def __str__(self) -> str:
|
243
|
+
if self:
|
244
|
+
return '< ' + str(self._left) + ' | >'
|
245
|
+
else:
|
246
|
+
return '< | ' + str(self._right) + ' >'
|
247
|
+
|
248
|
+
def __len__(self) -> int:
|
249
|
+
# Semantically, an XOR always contains just one value.
|
250
|
+
return 1
|
251
|
+
|
252
|
+
def __eq__(self, other: object) -> bool:
|
253
|
+
if not isinstance(other, type(self)):
|
254
|
+
return False
|
255
|
+
|
256
|
+
if self and other:
|
257
|
+
if self._left is other._left:
|
258
|
+
return True
|
259
|
+
elif self._left == other._left:
|
260
|
+
return True
|
261
|
+
else:
|
262
|
+
return False
|
263
|
+
|
264
|
+
if not self and not other:
|
265
|
+
if self._right is other._right:
|
266
|
+
return True
|
267
|
+
elif self._right == other._right:
|
268
|
+
return True
|
269
|
+
else:
|
270
|
+
return False
|
271
|
+
|
272
|
+
return False
|
273
|
+
|
274
|
+
@overload
|
275
|
+
def getLeft(self) -> MB[L]: ...
|
276
|
+
@overload
|
277
|
+
def getLeft(self, altLeft: L) -> MB[L]: ...
|
278
|
+
@overload
|
279
|
+
def getLeft(self, altLeft: MB[L]) -> MB[L]: ...
|
280
|
+
|
281
|
+
def getLeft(self, altLeft: L|MB[L]=MB()) -> MB[L]:
|
282
|
+
"""Get value if a left.
|
283
|
+
|
284
|
+
* if the `XOR` is a left, return its value
|
285
|
+
* if a right, return an alternate value of type ~L` if it is provided
|
286
|
+
* alternate value provided directly
|
287
|
+
* or optionally provided with a MB
|
288
|
+
* returns a `MB[L]` for when an altLeft value is needed but not provided
|
289
|
+
|
290
|
+
"""
|
291
|
+
_sentinel = Sentinel('MB')
|
292
|
+
match altLeft:
|
293
|
+
case MB(l) if l is not _sentinel:
|
294
|
+
if self:
|
295
|
+
return MB(self._left)
|
296
|
+
else:
|
297
|
+
return MB(cast(L, l))
|
298
|
+
case MB(s):
|
299
|
+
if self:
|
300
|
+
return MB(self._left)
|
301
|
+
else:
|
302
|
+
return MB()
|
303
|
+
case l:
|
304
|
+
if self:
|
305
|
+
return MB(self._left)
|
306
|
+
else:
|
307
|
+
return MB(l)
|
308
|
+
|
309
|
+
def getRight(self) -> R:
|
310
|
+
"""Get value of `XOR` if a right, potential right value if a left.
|
311
|
+
|
312
|
+
* if `XOR` is a right, return its value
|
313
|
+
* if `XOR` is a left, return the potential right value
|
314
|
+
|
315
|
+
"""
|
316
|
+
return self._right
|
317
|
+
|
318
|
+
def makeRight(self) -> XOR[L, R]:
|
319
|
+
"""Make a right based on the `XOR`.
|
320
|
+
|
321
|
+
* return a right based on potential right value
|
322
|
+
* returns itself if already a right
|
323
|
+
|
324
|
+
"""
|
325
|
+
if self._left == MB():
|
326
|
+
return self
|
327
|
+
else:
|
328
|
+
return cast(XOR[L, R], XOR(MB(), self._right))
|
329
|
+
|
330
|
+
def newRight(self, right: R) -> XOR[L, R]:
|
331
|
+
"""Swap in a right value.
|
332
|
+
|
333
|
+
* returns a new instance with a new right (or potential right) value.
|
334
|
+
|
335
|
+
"""
|
336
|
+
if self._left == MB():
|
337
|
+
return cast(XOR[L, R], XOR(MB(), right))
|
338
|
+
else:
|
339
|
+
return cast(XOR[L, R], XOR(self._left, right))
|
340
|
+
|
341
|
+
def map[U](self, f: Callable[[L], U]) -> XOR[U, R]:
|
342
|
+
"""Map over if a left value.
|
343
|
+
|
344
|
+
* if `XOR` is a left then map `f` over its value
|
345
|
+
* if `f` successful return a left `XOR[S, R]`
|
346
|
+
* if `f` unsuccessful return right `XOR[S, R]`
|
347
|
+
* swallows any exceptions `f` may throw
|
348
|
+
* if `XOR` is a right
|
349
|
+
* return new `XOR(right=self._right): XOR[S, R]`
|
350
|
+
* use method `mapRight` to adjust the returned value
|
351
|
+
|
352
|
+
"""
|
353
|
+
if self._left == MB():
|
354
|
+
return cast(XOR[U, R], self)
|
355
|
+
try:
|
356
|
+
applied = f(cast(L, self._left))
|
357
|
+
except Exception:
|
358
|
+
return cast(XOR[U, R], XOR(MB(), self._right))
|
359
|
+
else:
|
360
|
+
return XOR(applied, self._right)
|
361
|
+
|
362
|
+
def mapRight(self, g: Callable[[R], R], altRight: R) -> XOR[L, R]:
|
363
|
+
"""Map over a right or potential right value."""
|
364
|
+
try:
|
365
|
+
applied = g(self._right)
|
366
|
+
right = applied
|
367
|
+
except:
|
368
|
+
right = altRight
|
369
|
+
|
370
|
+
if self:
|
371
|
+
left: L|MB[L] = cast(L, self._left)
|
372
|
+
else:
|
373
|
+
left = MB()
|
374
|
+
|
375
|
+
return XOR(left, right)
|
376
|
+
|
377
|
+
def bind[U](self, f: Callable[[L], XOR[U, R]]) -> XOR[U, R]:
|
378
|
+
"""Flatmap - bind
|
379
|
+
|
380
|
+
* map over then flatten left values
|
381
|
+
* propagate right values
|
382
|
+
|
383
|
+
"""
|
384
|
+
if self._left == MB():
|
385
|
+
return cast(XOR[U, R], self)
|
386
|
+
else:
|
387
|
+
return f(cast(L, self._left))
|
388
|
+
|
389
|
+
@staticmethod
|
390
|
+
def call[U, V](f: Callable[[U], V], left: U) -> XOR[V, MB[Exception]]:
|
391
|
+
try:
|
392
|
+
return XOR(f(left), MB())
|
393
|
+
except Exception as esc:
|
394
|
+
return XOR(MB(), MB(esc))
|
395
|
+
|
396
|
+
@staticmethod
|
397
|
+
def lz_call[U, V](f: Callable[[U], V], left: U) -> Callable[[], XOR[V, MB[Exception]]]:
|
398
|
+
def ret() -> XOR[V, MB[Exception]]:
|
399
|
+
return XOR.call(f, left)
|
400
|
+
return ret
|
401
|
+
|
402
|
+
@staticmethod
|
403
|
+
def idx[V](v: Sequence[V], ii: int) -> XOR[V, MB[Exception]]:
|
404
|
+
try:
|
405
|
+
return XOR(v[ii], MB())
|
406
|
+
except Exception as esc:
|
407
|
+
return XOR(MB(), MB(esc))
|
408
|
+
|
409
|
+
@staticmethod
|
410
|
+
def lz_idx[V](v: Sequence[V], ii: int) -> Callable[[], XOR[V, MB[Exception]]]:
|
411
|
+
def ret() -> XOR[V, MB[Exception]]:
|
412
|
+
return XOR.idx(v, ii)
|
413
|
+
return ret
|
414
|
+
|
415
|
+
@staticmethod
|
416
|
+
def sequence(seq_xor_lr: Sequence[XOR[L, R]], potential_right: R) -> XOR[Sequence[L], R]:
|
417
|
+
"""Sequence an indexable container of `XOR[L, R]`
|
418
|
+
|
419
|
+
* if all the `XOR` values contained in the container are lefts, then
|
420
|
+
* return an `XOR` of the same type container of all the left values
|
421
|
+
* setting the potential right `potential_right`
|
422
|
+
* if at least one of the `XOR` values contained in the container is a right,
|
423
|
+
* return a right XOR containing the right value of the first right
|
424
|
+
|
425
|
+
"""
|
426
|
+
l: list[L] = []
|
427
|
+
|
428
|
+
for xor_lr in seq_xor_lr:
|
429
|
+
if xor_lr:
|
430
|
+
l.append(xor_lr.getLeft().get())
|
431
|
+
else:
|
432
|
+
return XOR(MB(), xor_lr.getRight())
|
433
|
+
|
434
|
+
ds = cast(Sequence[L], type(seq_xor_lr)(l)) # type: ignore # will be a subclass at runtime
|
435
|
+
return XOR(ds, potential_right)
|
436
|
+
|
dtools/fp/function.py
ADDED
@@ -0,0 +1,71 @@
|
|
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
|
+
"""###Module fp.functional - compose and partially apply 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 function arguments return values:
|
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 **partial:** returns a partially applied function
|
26
|
+
* function **iter_args:** function returning an iterator of its arguments
|
27
|
+
|
28
|
+
"""
|
29
|
+
from __future__ import annotations
|
30
|
+
from collections.abc import Callable, Iterator, Sequence
|
31
|
+
from typing import Any
|
32
|
+
|
33
|
+
__all__ = [ 'swap', 'sequenced', 'partial', 'iter_args' ]
|
34
|
+
|
35
|
+
## Functional Utilities
|
36
|
+
|
37
|
+
def swap[U,V,R](f: Callable[[U, V], R]) -> Callable[[V, U], R]:
|
38
|
+
"""Swap arguments of a two argument function."""
|
39
|
+
return (lambda v, u: f(u,v))
|
40
|
+
|
41
|
+
def sequenced[R](f: Callable[..., R]) -> Callable[..., R]:
|
42
|
+
"""Convert a function with arbitrary positional arguments to one taking
|
43
|
+
a sequence of the original arguments.
|
44
|
+
"""
|
45
|
+
def F(arguments: Sequence[Any]) -> R:
|
46
|
+
return f(*arguments)
|
47
|
+
return F
|
48
|
+
|
49
|
+
def partial[R](f: Callable[..., R], *args: Any) -> Callable[..., R]:
|
50
|
+
"""Partially apply arguments to a function, left to right.
|
51
|
+
|
52
|
+
* type-wise the only thing guaranteed is the return value
|
53
|
+
* best practice is to either
|
54
|
+
* use `partial` and `sequenced` results immediately and locally
|
55
|
+
* otherwise cast the results when they are created
|
56
|
+
|
57
|
+
"""
|
58
|
+
def wrap(*rest: R) -> R:
|
59
|
+
return sequenced(f)(args + rest)
|
60
|
+
|
61
|
+
return wrap
|
62
|
+
|
63
|
+
def iter_args[A](*args: A) -> Iterator[A]:
|
64
|
+
"""Function returning an iterators of its arguments.
|
65
|
+
|
66
|
+
* useful for API's with single iterable constructors
|
67
|
+
|
68
|
+
"""
|
69
|
+
for arg in args:
|
70
|
+
yield arg
|
71
|
+
|