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/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