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/iterables.py
ADDED
@@ -0,0 +1,413 @@
|
|
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.iterables - Iterator related tools
|
16
|
+
|
17
|
+
Library of iterator related functions and enumerations.
|
18
|
+
|
19
|
+
* iterables are not necessarily iterators
|
20
|
+
* at all times iterator protocol is assumed to be followed, that is
|
21
|
+
* all iterators are assumed to be iterable
|
22
|
+
* for all iterators `foo` we assume `iter(foo) is foo`
|
23
|
+
|
24
|
+
#### Concatenating and merging iterables:
|
25
|
+
|
26
|
+
* function **concat**: sequentially chain iterables
|
27
|
+
* function **exhaust**: shuffle together iterables until all are exhausted
|
28
|
+
* function **merge**: shuffle together iterables until one is exhausted
|
29
|
+
|
30
|
+
---
|
31
|
+
|
32
|
+
#### Dropping and taking values from an iterable:
|
33
|
+
|
34
|
+
* function **drop**: drop first `n` values from iterable
|
35
|
+
* function **drop_while**: drop values from iterable while predicate holds
|
36
|
+
* function **take**: take up to `n` initial values from iterable
|
37
|
+
* function **take_split**: splitting out initial `n` initial values of iterable * function **take_while**: take values from iterable while predicate holds
|
38
|
+
* function **take_while_split**: splitting an iterable while predicate holds
|
39
|
+
|
40
|
+
---
|
41
|
+
|
42
|
+
#### Reducing and accumulating an iterable:
|
43
|
+
|
44
|
+
* function **accumulate**: take iterable & function, return iterator of accumulated values
|
45
|
+
* function **foldL0**: fold iterable left with a function
|
46
|
+
* raises `StopIteration` exception if iterable is empty
|
47
|
+
* function **foldL1**: fold iterable left with a function and initial value
|
48
|
+
* function **mbFoldL**: fold iterable left with an optional initial value
|
49
|
+
* wraps result in a `MB` monad
|
50
|
+
|
51
|
+
"""
|
52
|
+
from __future__ import annotations
|
53
|
+
from collections.abc import Callable, Iterator, Iterable, Reversible
|
54
|
+
from enum import auto, Enum
|
55
|
+
from typing import cast, Never, Protocol
|
56
|
+
from .err_handling import MB
|
57
|
+
from .function import swap
|
58
|
+
from .singletons import NoValue
|
59
|
+
|
60
|
+
__all__ = [ 'FM', 'concat', 'merge', 'exhaust',
|
61
|
+
'drop', 'drop_while',
|
62
|
+
'take', 'take_while',
|
63
|
+
'take_split', 'take_while_split',
|
64
|
+
'accumulate', 'foldL0', 'foldL1', 'mbFoldL' ] #,
|
65
|
+
# 'scFoldL', 'scFoldR' ]
|
66
|
+
|
67
|
+
## Iterate over multiple Iterables
|
68
|
+
|
69
|
+
class FM(Enum):
|
70
|
+
CONCAT = auto()
|
71
|
+
MERGE = auto()
|
72
|
+
EXHAUST = auto()
|
73
|
+
|
74
|
+
def concat[D](*iterables: Iterable[D]) -> Iterator[D]:
|
75
|
+
"""Sequentially concatenate multiple iterables together.
|
76
|
+
|
77
|
+
* pure Python version of standard library's `itertools.chain`
|
78
|
+
* iterator sequentially yields each iterable until all are exhausted
|
79
|
+
* an infinite iterable will prevent subsequent iterables from yielding any values
|
80
|
+
* performant to `itertools.chain`
|
81
|
+
|
82
|
+
"""
|
83
|
+
for iterator in map(lambda x: iter(x), iterables):
|
84
|
+
while True:
|
85
|
+
try:
|
86
|
+
value = next(iterator)
|
87
|
+
yield value
|
88
|
+
except StopIteration:
|
89
|
+
break
|
90
|
+
|
91
|
+
def exhaust[D](*iterables: Iterable[D]) -> Iterator[D]:
|
92
|
+
"""Shuffle together multiple iterables until all are exhausted.
|
93
|
+
|
94
|
+
* iterator yields until all iterables are exhausted
|
95
|
+
|
96
|
+
"""
|
97
|
+
iterList = list(map(lambda x: iter(x), iterables))
|
98
|
+
if (numIters := len(iterList)) > 0:
|
99
|
+
ii = 0
|
100
|
+
values = []
|
101
|
+
while True:
|
102
|
+
try:
|
103
|
+
while ii < numIters:
|
104
|
+
values.append(next(iterList[ii]))
|
105
|
+
ii += 1
|
106
|
+
for value in values:
|
107
|
+
yield value
|
108
|
+
ii = 0
|
109
|
+
values.clear()
|
110
|
+
except StopIteration:
|
111
|
+
numIters -= 1
|
112
|
+
if numIters < 1:
|
113
|
+
break
|
114
|
+
del iterList[ii]
|
115
|
+
for value in values:
|
116
|
+
yield value
|
117
|
+
|
118
|
+
def merge[D](*iterables: Iterable[D], yield_partials: bool=False) -> Iterator[D]:
|
119
|
+
"""Shuffle together the `iterables` until one is exhausted.
|
120
|
+
|
121
|
+
* iterator yields until one of the iterables is exhausted
|
122
|
+
* if `yield_partials` is true,
|
123
|
+
* yield any unmatched yielded values from other iterables
|
124
|
+
* prevents data lose
|
125
|
+
* if any of the iterables are iterators with external references
|
126
|
+
|
127
|
+
"""
|
128
|
+
iterList = list(map(lambda x: iter(x), iterables))
|
129
|
+
values = []
|
130
|
+
if (numIters := len(iterList)) > 0:
|
131
|
+
while True:
|
132
|
+
try:
|
133
|
+
for ii in range(numIters):
|
134
|
+
values.append(next(iterList[ii]))
|
135
|
+
for value in values:
|
136
|
+
yield value
|
137
|
+
values.clear()
|
138
|
+
except StopIteration:
|
139
|
+
break
|
140
|
+
if yield_partials:
|
141
|
+
for value in values:
|
142
|
+
yield value
|
143
|
+
|
144
|
+
## dropping and taking
|
145
|
+
|
146
|
+
def drop[D](
|
147
|
+
iterable: Iterable[D],
|
148
|
+
n: int, /
|
149
|
+
) -> Iterator[D]:
|
150
|
+
"""Drop the next `n` values from `iterable`."""
|
151
|
+
it = iter(iterable)
|
152
|
+
for _ in range(n):
|
153
|
+
try:
|
154
|
+
next(it)
|
155
|
+
except StopIteration:
|
156
|
+
break
|
157
|
+
return it
|
158
|
+
|
159
|
+
def drop_while[D](
|
160
|
+
iterable: Iterable[D],
|
161
|
+
predicate: Callable[[D], bool], /
|
162
|
+
) -> Iterator[D]:
|
163
|
+
"""Drop initial values from `iterable` while predicate is true."""
|
164
|
+
it = iter(iterable)
|
165
|
+
while True:
|
166
|
+
try:
|
167
|
+
value = next(it)
|
168
|
+
if not predicate(value):
|
169
|
+
it = concat((value,), it)
|
170
|
+
break
|
171
|
+
except StopIteration:
|
172
|
+
break
|
173
|
+
return it
|
174
|
+
|
175
|
+
def take[D](
|
176
|
+
iterable: Iterable[D],
|
177
|
+
n: int, /
|
178
|
+
) -> Iterator[D]:
|
179
|
+
"""Return an iterator of up to `n` initial values of an iterable"""
|
180
|
+
it = iter(iterable)
|
181
|
+
for _ in range(n):
|
182
|
+
try:
|
183
|
+
value = next(it)
|
184
|
+
yield value
|
185
|
+
except StopIteration:
|
186
|
+
break
|
187
|
+
|
188
|
+
def take_split[D](
|
189
|
+
iterable: Iterable[D],
|
190
|
+
n: int, /
|
191
|
+
) -> tuple[Iterator[D], Iterator[D]]:
|
192
|
+
"""Same as take except also return an iterator of the remaining values.
|
193
|
+
|
194
|
+
* return a tuple of
|
195
|
+
* an iterator of up to `n` initial values
|
196
|
+
* an iterator of the remaining vales of the `iterable`
|
197
|
+
* best practice is not to access second iterator until first is exhausted
|
198
|
+
|
199
|
+
"""
|
200
|
+
it = iter(iterable)
|
201
|
+
itn = take(it, n)
|
202
|
+
|
203
|
+
return itn, it
|
204
|
+
|
205
|
+
def take_while[D](
|
206
|
+
iterable: Iterable[D],
|
207
|
+
pred: Callable[[D], bool], /
|
208
|
+
) -> Iterator[D]:
|
209
|
+
"""Yield values from `iterable` while predicate is true.
|
210
|
+
|
211
|
+
**Warning:** risk of potential value loss if iterable is iterator with
|
212
|
+
multiple references.
|
213
|
+
"""
|
214
|
+
it = iter(iterable)
|
215
|
+
while True:
|
216
|
+
try:
|
217
|
+
value = next(it)
|
218
|
+
if pred(value):
|
219
|
+
yield value
|
220
|
+
else:
|
221
|
+
break
|
222
|
+
except StopIteration:
|
223
|
+
break
|
224
|
+
|
225
|
+
def take_while_split[D](
|
226
|
+
iterable: Iterable[D],
|
227
|
+
predicate: Callable[[D], bool], /
|
228
|
+
) -> tuple[Iterator[D], Iterator[D]]:
|
229
|
+
"""Yield values from `iterable` while `predicate` is true.
|
230
|
+
|
231
|
+
* return a tuple of two iterators
|
232
|
+
* first of initial values where predicate is true, followed by first to fail
|
233
|
+
* second of the remaining values of the iterable after first failed value
|
234
|
+
* best practice is not to access second iterator until first is exhausted
|
235
|
+
|
236
|
+
"""
|
237
|
+
def _take_while(it: Iterator[D], pred: Callable[[D], bool], val: list[D]) -> Iterator[D]:
|
238
|
+
while True:
|
239
|
+
try:
|
240
|
+
if val:
|
241
|
+
val[0] = next(it)
|
242
|
+
else:
|
243
|
+
val.append(next(it))
|
244
|
+
if pred(val[0]):
|
245
|
+
yield val[0]
|
246
|
+
val.pop()
|
247
|
+
else:
|
248
|
+
break
|
249
|
+
except StopIteration:
|
250
|
+
break
|
251
|
+
|
252
|
+
it = iter(iterable)
|
253
|
+
value: list[D] = []
|
254
|
+
it_pred = _take_while(it, predicate, value)
|
255
|
+
|
256
|
+
return (it_pred, concat(value, it))
|
257
|
+
|
258
|
+
## reducing and accumulating
|
259
|
+
|
260
|
+
def accumulate[D,L](
|
261
|
+
iterable: Iterable[D],
|
262
|
+
f: Callable[[L, D], L],
|
263
|
+
initial: L|NoValue=NoValue(), /
|
264
|
+
) -> Iterator[L]:
|
265
|
+
"""Returns an iterator of accumulated values.
|
266
|
+
|
267
|
+
* pure Python version of standard library's `itertools.accumulate`
|
268
|
+
* function `f` does not default to addition (for typing flexibility)
|
269
|
+
* begins accumulation with an optional `initial` value
|
270
|
+
|
271
|
+
"""
|
272
|
+
it = iter(iterable)
|
273
|
+
try:
|
274
|
+
it0 = next(it)
|
275
|
+
except StopIteration:
|
276
|
+
if initial is NoValue():
|
277
|
+
return
|
278
|
+
else:
|
279
|
+
yield cast(L, initial)
|
280
|
+
else:
|
281
|
+
if initial is not NoValue():
|
282
|
+
init = cast(L, initial)
|
283
|
+
yield init
|
284
|
+
acc = f(init, it0)
|
285
|
+
for ii in it:
|
286
|
+
yield acc
|
287
|
+
acc = f(acc, ii)
|
288
|
+
yield acc
|
289
|
+
else:
|
290
|
+
acc = cast(L, it0) # in this case L = D
|
291
|
+
for ii in it:
|
292
|
+
yield acc
|
293
|
+
acc = f(acc, ii)
|
294
|
+
yield acc
|
295
|
+
|
296
|
+
def foldL0[D](
|
297
|
+
iterable: Iterable[D],
|
298
|
+
f: Callable[[D, D], D], /
|
299
|
+
) -> D|Never:
|
300
|
+
"""Folds an iterable left with optional initial value.
|
301
|
+
|
302
|
+
* traditional FP type order given for function `f`
|
303
|
+
* if iterable empty raises StopIteration exception
|
304
|
+
* does not catch any exception `f` raises
|
305
|
+
* never returns if `iterable` generates an infinite iterator
|
306
|
+
|
307
|
+
"""
|
308
|
+
it = iter(iterable)
|
309
|
+
try:
|
310
|
+
acc = next(it)
|
311
|
+
except StopIteration:
|
312
|
+
msg = "Attemped to left fold an empty iterable."
|
313
|
+
raise StopIteration(msg)
|
314
|
+
|
315
|
+
for v in it:
|
316
|
+
acc = f(acc, v)
|
317
|
+
|
318
|
+
return acc
|
319
|
+
|
320
|
+
def foldL1[D, L](
|
321
|
+
iterable: Iterable[D],
|
322
|
+
f: Callable[[L, D], L],
|
323
|
+
initial: L, /
|
324
|
+
) -> L|Never:
|
325
|
+
"""Folds an iterable left with optional initial value.
|
326
|
+
|
327
|
+
* traditional FP type order given for function `f`
|
328
|
+
* does not catch any exception `f` may raise
|
329
|
+
* like builtin `sum` for Python >=3.8 except
|
330
|
+
- not restricted to __add__ for the folding function
|
331
|
+
- initial value required, does not default to `0` for initial value
|
332
|
+
- handles non-numeric data just find
|
333
|
+
* never returns if `iterable` generates an infinite iterator
|
334
|
+
|
335
|
+
"""
|
336
|
+
acc = initial
|
337
|
+
for v in iterable:
|
338
|
+
acc = f(acc, v)
|
339
|
+
return acc
|
340
|
+
|
341
|
+
def mbFoldL[L, D](
|
342
|
+
iterable: Iterable[D],
|
343
|
+
f: Callable[[L, D], L],
|
344
|
+
initial: L|NoValue=NoValue()
|
345
|
+
) -> MB[L]:
|
346
|
+
"""Folds an iterable left with optional initial value.
|
347
|
+
|
348
|
+
* traditional FP type order given for function `f`
|
349
|
+
* when an initial value is not given then `~L = ~D`
|
350
|
+
* if iterable empty and no `initial` value given, return `MB()`
|
351
|
+
* never returns if iterable generates an infinite iterator
|
352
|
+
|
353
|
+
"""
|
354
|
+
acc: L
|
355
|
+
it = iter(iterable)
|
356
|
+
if initial is NoValue():
|
357
|
+
try:
|
358
|
+
acc = cast(L, next(it)) # in this case L = D
|
359
|
+
except StopIteration:
|
360
|
+
return MB()
|
361
|
+
else:
|
362
|
+
acc = cast(L, initial)
|
363
|
+
|
364
|
+
for v in it:
|
365
|
+
try:
|
366
|
+
acc = f(acc, v)
|
367
|
+
except Exception:
|
368
|
+
return MB()
|
369
|
+
|
370
|
+
return MB(acc)
|
371
|
+
|
372
|
+
|
373
|
+
#def scFoldL[D, L](iterable: Iterable[D],
|
374
|
+
# f: Callable[[L, D], L],
|
375
|
+
# initial: L|NoValue=NoValue(), /,
|
376
|
+
# start_folding: Callable[[D], bool]=lambda d: True,
|
377
|
+
# stop_folding: Callable[[D], bool]=lambda d: False,
|
378
|
+
# include_start: bool=True,
|
379
|
+
# propagate_failed: bool=True) -> tuple[MB[L], Iterable[D]]:
|
380
|
+
# """Short circuit version of a left fold. Useful for infinite or
|
381
|
+
# non-reversible iterables.
|
382
|
+
#
|
383
|
+
# * Behavior for default arguments will
|
384
|
+
# * left fold finite iterable
|
385
|
+
# * start folding immediately
|
386
|
+
# * continue folding until end (of a possibly infinite iterable)
|
387
|
+
# * Callable `start_folding` delays starting a left fold
|
388
|
+
# * Callable `stop_folding` is to prematurely stop the folding left
|
389
|
+
# * Returns an XOR of either the folded value or error string
|
390
|
+
#
|
391
|
+
# """
|
392
|
+
#
|
393
|
+
#def scFoldR[D, R](iterable: Iterable[D],
|
394
|
+
# f: Callable[[D, R], R],
|
395
|
+
# initial: R|NoValue=NoValue(), /,
|
396
|
+
# start_folding: Callable[[D], bool]=lambda d: False,
|
397
|
+
# stop_folding: Callable[[D], bool]=lambda d: False,
|
398
|
+
# include_start: bool=True,
|
399
|
+
# include_stop: bool=True) -> tuple[MB[R], Iterable[D]]:
|
400
|
+
# """Short circuit version of a right fold. Useful for infinite or
|
401
|
+
# non-reversible iterables.
|
402
|
+
#
|
403
|
+
# * Behavior for default arguments will
|
404
|
+
# * right fold finite iterable
|
405
|
+
# * start folding at end (of a possibly infinite iterable)
|
406
|
+
# * continue folding right until beginning
|
407
|
+
# * Callable `start_folding` prematurely starts a right fold
|
408
|
+
# * Callable `stop_folding` is to prematurely stops a right fold
|
409
|
+
# * Returns an XOR of either the folded value or error string
|
410
|
+
# * best practice is not to access second iterator until first is exhausted
|
411
|
+
#
|
412
|
+
# """
|
413
|
+
|
dtools/fp/lazy.py
ADDED
@@ -0,0 +1,126 @@
|
|
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.lazy - lazy function evaluations
|
16
|
+
|
17
|
+
Delayed function evaluations, if needed, usually in some inner scope. FP tools
|
18
|
+
for "non-strict" function evaluations.
|
19
|
+
|
20
|
+
#### Non-strict delayed function evaluation:
|
21
|
+
|
22
|
+
* class **Lazy:** Delay evaluation of function taking & returning single values
|
23
|
+
* function **lazy:** Delay evaluation of a function taking more than one value
|
24
|
+
|
25
|
+
"""
|
26
|
+
from __future__ import annotations
|
27
|
+
|
28
|
+
__all__ = [ 'Lazy', 'lazy' ]
|
29
|
+
|
30
|
+
from collections.abc import Callable
|
31
|
+
from typing import Final
|
32
|
+
from .err_handling import MB, XOR
|
33
|
+
from .function import sequenced
|
34
|
+
|
35
|
+
class Lazy[D, R]():
|
36
|
+
"""Delayed evaluation of a function mapping a value of type D
|
37
|
+
|
38
|
+
Class instance delays the executable of a function where `Lazy(f, arg)`
|
39
|
+
constructs an object that can evaluate the Callable `f` with its argument
|
40
|
+
at a later time.
|
41
|
+
|
42
|
+
* first argument `f` taking values of type `~D` to values of type `~R`
|
43
|
+
* second argument `arg: ~D` is the argument to be passed to `f`
|
44
|
+
* where the type `~D` is the `tuple` type of the argument types to `f`
|
45
|
+
* function is evaluated when the eval method is called
|
46
|
+
* result is cached unless `pure` is set to `False` in `__init__` method
|
47
|
+
|
48
|
+
Usually use case is to make a function "non-strict" by passing some of its
|
49
|
+
arguments wrapped in Lazy instances.
|
50
|
+
"""
|
51
|
+
__slots__ = '_f', '_d', '_result', '_pure'
|
52
|
+
|
53
|
+
def __init__(self, f: Callable[[D], R], d: D, pure: bool=True) -> None:
|
54
|
+
self._f: Final[Callable[[D], R]] = f
|
55
|
+
self._d: Final[D] = d
|
56
|
+
self._pure: Final[bool] = pure
|
57
|
+
self._result: XOR[R, MB[Exception]] = XOR(MB(), MB())
|
58
|
+
|
59
|
+
def __bool__(self) -> bool:
|
60
|
+
return True if self._result else False
|
61
|
+
|
62
|
+
def is_evaluated(self) -> bool:
|
63
|
+
return self._result != XOR(MB(), MB())
|
64
|
+
|
65
|
+
def is_exceptional(self) -> bool:
|
66
|
+
if self.is_evaluated():
|
67
|
+
return False if self._result else True
|
68
|
+
else:
|
69
|
+
return False
|
70
|
+
|
71
|
+
def is_pure(self) -> bool:
|
72
|
+
return self._pure
|
73
|
+
|
74
|
+
def eval(self) -> bool:
|
75
|
+
"""Evaluate function with its argument.
|
76
|
+
|
77
|
+
* evaluate function
|
78
|
+
* cache results or exceptions if `pure == True`
|
79
|
+
* reevaluate if `pure == False`
|
80
|
+
|
81
|
+
"""
|
82
|
+
if not self.is_evaluated() or not self._pure:
|
83
|
+
try:
|
84
|
+
result = self._f(self._d)
|
85
|
+
except Exception as exc:
|
86
|
+
self._result = XOR(MB(), MB(exc))
|
87
|
+
return False
|
88
|
+
else:
|
89
|
+
self._result = XOR(MB(result), MB())
|
90
|
+
return True
|
91
|
+
if self:
|
92
|
+
return True
|
93
|
+
else:
|
94
|
+
return False
|
95
|
+
|
96
|
+
def result(self) -> MB[R]:
|
97
|
+
if not self.is_evaluated():
|
98
|
+
self.eval()
|
99
|
+
|
100
|
+
if self._result:
|
101
|
+
return MB(self._result.getLeft())
|
102
|
+
else:
|
103
|
+
return MB()
|
104
|
+
|
105
|
+
def exception(self) -> MB[Exception]:
|
106
|
+
if not self.is_evaluated():
|
107
|
+
self.eval()
|
108
|
+
return self._result.getRight()
|
109
|
+
|
110
|
+
def lazy[R, **P](f: Callable[P, R], *args: P.args, pure: bool=True) -> Lazy[tuple[P.args], R]:
|
111
|
+
"""Delayed evaluation of a function with arbitrary positional arguments.
|
112
|
+
|
113
|
+
Function returning a delayed evaluation of a function of an arbitrary number
|
114
|
+
of positional arguments.
|
115
|
+
|
116
|
+
* first positional argument `f` takes a function
|
117
|
+
* next positional arguments are the arguments to be applied later to `f`
|
118
|
+
* `f` is evaluated when the `eval` method of the returned Lazy is called
|
119
|
+
* `f` is evaluated only once with results cached unless `pure` is `False`
|
120
|
+
* if `pure` is false, the arguments are reapplied to `f`
|
121
|
+
* useful for repeating side effects
|
122
|
+
* when arguments are or contain shared references
|
123
|
+
|
124
|
+
"""
|
125
|
+
return Lazy(sequenced(f), args, pure=pure)
|
126
|
+
|
dtools/fp/py.typed
ADDED
File without changes
|