dtools.fp 1.3.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|