funstruct 0.1.0__tar.gz
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.
- funstruct-0.1.0/LICENSE +21 -0
- funstruct-0.1.0/PKG-INFO +26 -0
- funstruct-0.1.0/README.md +9 -0
- funstruct-0.1.0/funstruct/__init__.py +2 -0
- funstruct-0.1.0/funstruct/_cons.py +661 -0
- funstruct-0.1.0/funstruct/_cons2.py +17 -0
- funstruct-0.1.0/funstruct/_frozendict.py +268 -0
- funstruct-0.1.0/funstruct/yo.py +223 -0
- funstruct-0.1.0/pyproject.toml +24 -0
funstruct-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Andrew Stefanich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
funstruct-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: funstruct
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: fun & functional structures
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: functional,immutable,fp
|
|
7
|
+
Author: Andrew Stefanich
|
|
8
|
+
Author-email: andrewstefanich@gmail.com
|
|
9
|
+
Requires-Python: >=3.12,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: pytest-parametrization (>=2022.2.1,<2023.0.0)
|
|
14
|
+
Requires-Dist: returns (>=0.23.0,<0.24.0)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# funstruct
|
|
18
|
+
|
|
19
|
+
Some of my fav lil data structures.
|
|
20
|
+
|
|
21
|
+
These are not meant to be highly performant.
|
|
22
|
+
|
|
23
|
+
They are useful for smaller datasets and for personal scripts.
|
|
24
|
+
|
|
25
|
+
Like all functional stuctures, they play very well with recursive algos.
|
|
26
|
+
|
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
from abc import abstractmethod, ABC
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Callable, Iterable, Tuple
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
A = TypeVar("A")
|
|
8
|
+
|
|
9
|
+
type _CList = "CList[A]"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CList[A](ABC):
|
|
13
|
+
"""
|
|
14
|
+
A Lisp/ML/Scala style singly linked list (cons list).
|
|
15
|
+
|
|
16
|
+
Provides an interface for working with a singly linked list, including methods for
|
|
17
|
+
traversal, transformation, and manipulation of list elements.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def append(self, other: _CList) -> _CList:
|
|
22
|
+
"""
|
|
23
|
+
Append another list to the end of this list.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
other: The list to append.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
A new list with the elements of `other` appended to this list.
|
|
30
|
+
"""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def fold_right[B](self, acc: B, f: Callable[[A, B], B]) -> B:
|
|
35
|
+
"""
|
|
36
|
+
Fold the list from right to left.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
acc: The initial accumulator value.
|
|
40
|
+
f: A function to apply, taking an element and the current accumulator.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The result of folding the list from right to left.
|
|
44
|
+
"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def fold_left[B](self, acc: B, f: Callable[[B, A], B]) -> B:
|
|
49
|
+
"""
|
|
50
|
+
Fold the list from left to right.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
acc: The initial accumulator value.
|
|
54
|
+
f: A function to apply, taking the current accumulator and an element.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The result of folding the list from left to right.
|
|
58
|
+
"""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def drop(self, n: int) -> _CList:
|
|
63
|
+
"""
|
|
64
|
+
Drop the first `n` elements from the list.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
n: The number of elements to drop.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A new list with the first `n` elements removed.
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def drop_while(self, f: Callable[[A], bool]) -> _CList:
|
|
76
|
+
"""
|
|
77
|
+
Drop elements from the list as long as the predicate function `f` is true.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
f: A predicate function to apply to each element.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
A new list with elements removed while `f` is true.
|
|
84
|
+
"""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def take(self, n: int) -> _CList:
|
|
89
|
+
"""
|
|
90
|
+
Take the first `n` elements from the list.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
n: The number of elements to take.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
A new list containing the first `n` elements.
|
|
97
|
+
"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def take_while(self, f: Callable[[A], bool]) -> _CList:
|
|
102
|
+
"""
|
|
103
|
+
Take elements from the list as long as the predicate function `f` is true.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
f: A predicate function to apply to each element.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
A new list with elements taken while `f` is true.
|
|
110
|
+
"""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def split_at(self, i: int) -> Tuple[_CList, _CList]:
|
|
115
|
+
"""
|
|
116
|
+
Split the list into two lists at index `i`.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
i: The index to split at.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
A tuple of two lists: the first containing elements up to `i`,
|
|
123
|
+
and the second containing the rest.
|
|
124
|
+
"""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def partition(self, f: Callable[[A], bool]) -> Tuple[_CList, _CList]:
|
|
128
|
+
"""
|
|
129
|
+
Partition the list into two lists based on a predicate function.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
f: A predicate function to apply to each element.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
A tuple of two lists: the first containing elements that satisfy `f`,
|
|
136
|
+
and the second containing the rest.
|
|
137
|
+
"""
|
|
138
|
+
accum = lambda a, x: (a << x[0], x[1]) if f(a) else (x[0], a << x[1])
|
|
139
|
+
return self.fold_right((Nil(), Nil()), accum)
|
|
140
|
+
|
|
141
|
+
def length(self) -> int:
|
|
142
|
+
"""
|
|
143
|
+
Compute the length of the list.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The number of elements in the list.
|
|
147
|
+
"""
|
|
148
|
+
return self.fold_right(0, lambda _, acc: acc + 1)
|
|
149
|
+
|
|
150
|
+
def prepend(self, new_head: A) -> _CList:
|
|
151
|
+
"""
|
|
152
|
+
Prepend an element to the list.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
new_head: The element to prepend.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
A new list with `new_head` added to the beginning.
|
|
159
|
+
"""
|
|
160
|
+
return Cons(new_head, self)
|
|
161
|
+
|
|
162
|
+
def reversed(self) -> _CList:
|
|
163
|
+
"""
|
|
164
|
+
Reverse the order of the elements in the list.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
A new list with the elements in reversed order.
|
|
168
|
+
"""
|
|
169
|
+
return self.fold_left(Nil(), lambda acc, h: Cons(h, acc))
|
|
170
|
+
|
|
171
|
+
def map[B](self, f: Callable[[A], B]) -> "CList[B]":
|
|
172
|
+
"""
|
|
173
|
+
Apply a function to each element of the list, producing a new list
|
|
174
|
+
with the results.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
f: A function to apply to each element.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
A new list with the results of applying `f` to each element.
|
|
181
|
+
"""
|
|
182
|
+
return self.fold_right(Nil(), lambda a, acc: Cons(f(a), acc))
|
|
183
|
+
|
|
184
|
+
def filter(self, f: Callable[[A], bool]) -> _CList:
|
|
185
|
+
"""
|
|
186
|
+
Filter the elements of the list based on a predicate function.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
f: A predicate function to apply to each element.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
A new list containing only the elements that satisfy `f`.
|
|
193
|
+
"""
|
|
194
|
+
return self.fold_right(Nil(), lambda a, acc: Cons(a, acc) if f(a) else acc)
|
|
195
|
+
|
|
196
|
+
def flatten(self) -> _CList:
|
|
197
|
+
"""
|
|
198
|
+
Flatten a list of lists into a single list.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
A new list with all nested lists flattened into a single list.
|
|
202
|
+
"""
|
|
203
|
+
return CList.flatten_(self)
|
|
204
|
+
|
|
205
|
+
def flat_map[B](self, f: Callable[[A], "CList[B]"]) -> "CList[B]":
|
|
206
|
+
"""
|
|
207
|
+
Apply a function to each element of the list,
|
|
208
|
+
then flatten the resulting lists.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
f: A function that returns a list for each element.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
A new list with the results of applying `f` to each element,
|
|
215
|
+
flattened into a single list.
|
|
216
|
+
"""
|
|
217
|
+
return self.map(f).flatten()
|
|
218
|
+
|
|
219
|
+
def bind[B](self, f: Callable[[A], "CList[B]"]) -> "CList[B]":
|
|
220
|
+
"""
|
|
221
|
+
Apply a function to each element of the list and flatten the results.
|
|
222
|
+
(alias for 'flat_map')
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
f: A function that returns a list for each element.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
A new list with the results of applying `f` to each element,
|
|
229
|
+
flattened into a single list.
|
|
230
|
+
"""
|
|
231
|
+
return self.flat_map(f)
|
|
232
|
+
|
|
233
|
+
def sorted(self, cmp: Callable[[A, A], int]) -> _CList:
|
|
234
|
+
"""
|
|
235
|
+
Sort the list using a comparison function.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
cmp: A comparison function to use for sorting.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
A new list with the elements sorted according to `cmp`.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def merge(left: _CList, right: _CList) -> _CList:
|
|
245
|
+
match left, right:
|
|
246
|
+
case Nil(), r:
|
|
247
|
+
return r
|
|
248
|
+
case l, Nil():
|
|
249
|
+
return l
|
|
250
|
+
case Cons(lh, lt), Cons(rh, rt):
|
|
251
|
+
if cmp(lh, rh) <= 0:
|
|
252
|
+
return lh << merge(lt, right)
|
|
253
|
+
return rh << merge(left, rt)
|
|
254
|
+
case _:
|
|
255
|
+
return Nil()
|
|
256
|
+
|
|
257
|
+
length = len(self)
|
|
258
|
+
if length <= 1:
|
|
259
|
+
return self
|
|
260
|
+
left, right = self.split_at(length // 2)
|
|
261
|
+
return merge(left.sorted(cmp), right.sorted(cmp))
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def flatten_(lst: "CList[CList[A]]") -> _CList:
|
|
265
|
+
"""
|
|
266
|
+
Flatten a nested list of lists into a single list.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
lst: A list of lists to be flattened.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
A new list with all nested lists flattened into a single list.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def concat(left, right):
|
|
276
|
+
match left:
|
|
277
|
+
case Nil():
|
|
278
|
+
return right
|
|
279
|
+
case Cons(h, t):
|
|
280
|
+
return Cons(h, concat(t, right))
|
|
281
|
+
|
|
282
|
+
def flatten(lst: "CList[CList[A]]") -> "CList[A]":
|
|
283
|
+
match lst:
|
|
284
|
+
case Nil():
|
|
285
|
+
return Nil()
|
|
286
|
+
case Cons(h, t):
|
|
287
|
+
match h:
|
|
288
|
+
case Cons(_, _):
|
|
289
|
+
return concat(flatten(h), flatten(t))
|
|
290
|
+
case _:
|
|
291
|
+
return Cons(h, flatten(t))
|
|
292
|
+
|
|
293
|
+
return flatten(lst)
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def cons(a: A) -> _CList:
|
|
297
|
+
"""
|
|
298
|
+
Create a new list with a single element.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
a: The element to add to the list.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
A new list containing the single element `a`.
|
|
305
|
+
"""
|
|
306
|
+
return Cons(a)
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def empty() -> _CList:
|
|
310
|
+
"""
|
|
311
|
+
Create an empty list.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
An empty list.
|
|
315
|
+
"""
|
|
316
|
+
return Nil()
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def new(*xs: A) -> _CList:
|
|
320
|
+
"""
|
|
321
|
+
Create a new list from the given elements.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
*xs: The elements to add to the list.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
A new list containing the elements `xs`.
|
|
328
|
+
"""
|
|
329
|
+
return Cons(xs[0], CList.new(*xs[1:])) if xs else Nil()
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def from_iterable(iterable: Iterable[A]) -> _CList:
|
|
333
|
+
"""
|
|
334
|
+
Create a new list from an iterable of elements.
|
|
335
|
+
Ex:
|
|
336
|
+
CList.from_iterable([1,2]) == Cons(1, Cons(2))
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
iterable: An iterable of elements.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
A new list containing the elements from the iterable.
|
|
343
|
+
"""
|
|
344
|
+
return CList.new(*iterable)
|
|
345
|
+
|
|
346
|
+
def __rlshift__(self, other) -> _CList:
|
|
347
|
+
"""
|
|
348
|
+
Prepend an element to the list using the `<<` operator.
|
|
349
|
+
Ex:
|
|
350
|
+
1 << Nil() == Cons(1)
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
other: The element to prepend.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
A new list with `other` added to the beginning.
|
|
357
|
+
"""
|
|
358
|
+
return self.prepend(other)
|
|
359
|
+
|
|
360
|
+
def __add__(self, other) -> _CList:
|
|
361
|
+
"""
|
|
362
|
+
Append another list to the end of this list using the `+` operator.
|
|
363
|
+
(alias for 'append')
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
other: The list to append.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
A new list with the elements of `other` appended to this list.
|
|
370
|
+
"""
|
|
371
|
+
return self.append(other)
|
|
372
|
+
|
|
373
|
+
def __len__(self) -> int:
|
|
374
|
+
"""
|
|
375
|
+
Compute the length of the list.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
The number of elements in the list.
|
|
379
|
+
"""
|
|
380
|
+
return self.fold_right(0, lambda _, acc: acc + 1)
|
|
381
|
+
|
|
382
|
+
def __iter__(self):
|
|
383
|
+
"""
|
|
384
|
+
Iterate over the elements of the list.
|
|
385
|
+
|
|
386
|
+
Yields:
|
|
387
|
+
Each element of the list.
|
|
388
|
+
"""
|
|
389
|
+
current = self
|
|
390
|
+
while isinstance(current, Cons):
|
|
391
|
+
yield current.head
|
|
392
|
+
current = current.tail
|
|
393
|
+
|
|
394
|
+
def __eq__(self, other) -> bool:
|
|
395
|
+
"""
|
|
396
|
+
Check if this list is equal to another list.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
other: The list to compare with.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
True if the lists are equal, False otherwise.
|
|
403
|
+
"""
|
|
404
|
+
match self, other:
|
|
405
|
+
case Cons(sh, st), Cons(oh, ot):
|
|
406
|
+
return sh == oh and st == ot
|
|
407
|
+
case Nil(), Nil():
|
|
408
|
+
return True
|
|
409
|
+
case _:
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class Nil(CList):
|
|
414
|
+
"""
|
|
415
|
+
A singleton representing the empty list/end of a singly linked list.
|
|
416
|
+
"""
|
|
417
|
+
|
|
418
|
+
_instance = None
|
|
419
|
+
|
|
420
|
+
def __new__(cls):
|
|
421
|
+
if cls._instance is None:
|
|
422
|
+
cls._instance = super(Nil, cls).__new__(cls)
|
|
423
|
+
return cls._instance
|
|
424
|
+
|
|
425
|
+
def __repr__(self):
|
|
426
|
+
"""
|
|
427
|
+
Return a string representation of the empty list.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
A string representing the empty list.
|
|
431
|
+
"""
|
|
432
|
+
return "Nil()"
|
|
433
|
+
|
|
434
|
+
def append(self, other: _CList) -> _CList:
|
|
435
|
+
"""
|
|
436
|
+
Append another list to the empty list.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
other: The list to append.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
The list `other`, since appending to an empty list results in `other`.
|
|
443
|
+
"""
|
|
444
|
+
return other
|
|
445
|
+
|
|
446
|
+
def fold_right[A, B](self, acc: B, f: Callable[[A, B], B]) -> B:
|
|
447
|
+
"""
|
|
448
|
+
Fold the empty list from right to left.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
acc: The initial accumulator value.
|
|
452
|
+
f: A function to apply, taking an element and the current accumulator.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
The accumulator value, since folding an empty list results
|
|
456
|
+
in the initial value.
|
|
457
|
+
"""
|
|
458
|
+
return acc
|
|
459
|
+
|
|
460
|
+
def fold_left[A, B](self, acc: B, f: Callable[[B, A], B]) -> B:
|
|
461
|
+
"""
|
|
462
|
+
Fold the empty list from left to right.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
acc: The initial accumulator value.
|
|
466
|
+
f: A function to apply, taking the current accumulator and an element.
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
The accumulator value, since folding an empty list results
|
|
470
|
+
in the initial value.
|
|
471
|
+
"""
|
|
472
|
+
return acc
|
|
473
|
+
|
|
474
|
+
def drop(self, n: int) -> _CList:
|
|
475
|
+
"""
|
|
476
|
+
Drop the first `n` elements from the empty list.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
n: The number of elements to drop.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
The empty list, since dropping elements from an empty list results
|
|
483
|
+
in an empty list.
|
|
484
|
+
"""
|
|
485
|
+
return self
|
|
486
|
+
|
|
487
|
+
def drop_while(self, f: Callable[[A], bool]) -> _CList:
|
|
488
|
+
"""
|
|
489
|
+
Drop elements from the empty list as long as the predicate function
|
|
490
|
+
`f` is true.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
f: A predicate function to apply to each element.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
The empty list, since dropping elements from an empty list results
|
|
497
|
+
in an empty list.
|
|
498
|
+
"""
|
|
499
|
+
return self
|
|
500
|
+
|
|
501
|
+
def take(self, n: int) -> _CList:
|
|
502
|
+
"""
|
|
503
|
+
Take the first `n` elements from the empty list.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
n: The number of elements to take.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
The empty list, since taking elements from an empty list results
|
|
510
|
+
in an empty list.
|
|
511
|
+
"""
|
|
512
|
+
return self
|
|
513
|
+
|
|
514
|
+
def take_while(self, f: Callable[[A], bool]) -> _CList:
|
|
515
|
+
"""
|
|
516
|
+
Take elements from the empty list as long as the predicate function
|
|
517
|
+
`f` is true.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
f: A predicate function to apply to each element.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
The empty list, since taking elements from an empty list results
|
|
524
|
+
in an empty list.
|
|
525
|
+
"""
|
|
526
|
+
return self
|
|
527
|
+
|
|
528
|
+
def split_at(self, i: int) -> Tuple[_CList, _CList]:
|
|
529
|
+
"""
|
|
530
|
+
Split the empty list into two lists at index `i`.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
i: The index to split at.
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
A tuple of two empty lists.
|
|
537
|
+
"""
|
|
538
|
+
return self, self
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@dataclass(frozen=True)
|
|
542
|
+
class Cons[A](CList[A]):
|
|
543
|
+
"""
|
|
544
|
+
Represents a non-empty list with a head element and a tail list.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
head: A
|
|
548
|
+
tail: CList[A] = field(default_factory=Nil)
|
|
549
|
+
|
|
550
|
+
def __repr__(self):
|
|
551
|
+
"""
|
|
552
|
+
Return a string representation of the non-empty list.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
A string representing the list, showing the head and tail.
|
|
556
|
+
"""
|
|
557
|
+
match self.tail:
|
|
558
|
+
case Nil():
|
|
559
|
+
return f"Cons({self.head})"
|
|
560
|
+
return f"Cons({self.head}, {self.tail})"
|
|
561
|
+
|
|
562
|
+
def append(self, other: _CList) -> _CList:
|
|
563
|
+
"""
|
|
564
|
+
Append another list to the end of this non-empty list.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
other: The list to append.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
A new list with `other` appended to the end of this list.
|
|
571
|
+
"""
|
|
572
|
+
return Cons(self.head, self.tail.append(other))
|
|
573
|
+
|
|
574
|
+
def fold_right[B](self, acc: B, f: Callable[[A, B], B]) -> B:
|
|
575
|
+
"""
|
|
576
|
+
Fold the non-empty list from right to left.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
acc: The initial accumulator value.
|
|
580
|
+
f: A function to apply, taking an element and the current accumulator.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
The result of folding the list from right to left.
|
|
584
|
+
"""
|
|
585
|
+
return f(self.head, self.tail.fold_right(acc, f))
|
|
586
|
+
|
|
587
|
+
def fold_left[B](self, acc: B, f: Callable[[B, A], B]) -> B:
|
|
588
|
+
"""
|
|
589
|
+
Fold the non-empty list from left to right.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
acc: The initial accumulator value.
|
|
593
|
+
f: A function to apply, taking the current accumulator and an element.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
The result of folding the list from left to right.
|
|
597
|
+
"""
|
|
598
|
+
return self.tail.fold_left(f(acc, self.head), f)
|
|
599
|
+
|
|
600
|
+
def drop(self, n: int) -> _CList:
|
|
601
|
+
"""
|
|
602
|
+
Drop the first `n` elements from the non-empty list.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
n: The number of elements to drop.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
A new list with the first `n` elements removed.
|
|
609
|
+
"""
|
|
610
|
+
return self if n <= 0 else self.tail.drop(n - 1)
|
|
611
|
+
|
|
612
|
+
def drop_while(self, f: Callable[[A], bool]) -> _CList:
|
|
613
|
+
"""
|
|
614
|
+
Drop elements from the non-empty list as long as the predicate function
|
|
615
|
+
`f` is true.
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
f: A predicate function to apply to each element.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
A new list with elements removed while `f` is true.
|
|
622
|
+
"""
|
|
623
|
+
return self if not f(self.head) else self.tail.drop_while(f)
|
|
624
|
+
|
|
625
|
+
def take(self, n: int) -> _CList:
|
|
626
|
+
"""
|
|
627
|
+
Take the first `n` elements from the non-empty list.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
n: The number of elements to take.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
A new list containing the first `n` elements.
|
|
634
|
+
"""
|
|
635
|
+
return Cons(self.head) if n <= 1 else self.head << self.tail.take(n - 1)
|
|
636
|
+
|
|
637
|
+
def take_while(self, f: Callable[[A], bool]) -> _CList:
|
|
638
|
+
"""
|
|
639
|
+
Take elements from the non-empty list as long as the predicate function
|
|
640
|
+
`f` is true.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
f: A predicate function to apply to each element.
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
A new list with elements taken while `f` is true.
|
|
647
|
+
"""
|
|
648
|
+
return self.head << self.tail.take_while(f) if f(self.head) else Nil()
|
|
649
|
+
|
|
650
|
+
def split_at(self, i: int) -> Tuple[_CList, _CList]:
|
|
651
|
+
"""
|
|
652
|
+
Split the non-empty list into two lists at index `i`.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
i: The index to split at.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
A tuple of two lists: the first containing elements up to `i`,
|
|
659
|
+
and the second containing the rest.
|
|
660
|
+
"""
|
|
661
|
+
return self.take(i), self.drop(i)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from returns.maybe import Maybe, Some, Nothing
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class frozendict[K, V]:
|
|
7
|
+
"""
|
|
8
|
+
An immutable wrapper around a mutable dict.
|
|
9
|
+
|
|
10
|
+
This class provides an immutable dictionary-like object. Once created, the dictionary cannot be modified.
|
|
11
|
+
It supports standard dictionary operations and some additional methods for immutability and safe usage.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
"""
|
|
16
|
+
Initializes a frozendict instance with the given arguments.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
*args: Positional arguments passed to the dictionary constructor.
|
|
20
|
+
**kwargs: Keyword arguments passed to the dictionary constructor.
|
|
21
|
+
"""
|
|
22
|
+
self._dict = dict(*args, **kwargs)
|
|
23
|
+
self._hash = None
|
|
24
|
+
|
|
25
|
+
def __getitem__(self, key: K) -> V:
|
|
26
|
+
"""
|
|
27
|
+
Retrieves the value associated with the given key.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
key: The key to look up in the dictionary.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The value associated with the key.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
KeyError: If the key is not found in the dictionary.
|
|
37
|
+
"""
|
|
38
|
+
return self._dict[key]
|
|
39
|
+
|
|
40
|
+
def get(self, key: K) -> Optional[V]:
|
|
41
|
+
"""
|
|
42
|
+
Retrieves the value associated with the given key, or None if the key is not found.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
key: The key to look up in the dictionary.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The value associated with the key, or None if the key is not found.
|
|
49
|
+
"""
|
|
50
|
+
item = self._dict.get(key)
|
|
51
|
+
if item is not None:
|
|
52
|
+
return item
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def get_maybe(self, key: K) -> Maybe[V]:
|
|
56
|
+
"""
|
|
57
|
+
Retrieves the value associated with the given key as a Maybe type.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
key: The key to look up in the dictionary.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A Maybe instance containing the value if the key is found, or Nothing if the key is not found.
|
|
64
|
+
"""
|
|
65
|
+
item = self._dict.get(key)
|
|
66
|
+
if item is not None:
|
|
67
|
+
return Some(item)
|
|
68
|
+
return Nothing
|
|
69
|
+
|
|
70
|
+
def __eq__(self, other) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Checks if the current frozendict is equal to another dictionary or frozendict.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
other: The object to compare with the current frozendict.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if the other object is equal to the current frozendict, False otherwise.
|
|
79
|
+
"""
|
|
80
|
+
match other:
|
|
81
|
+
case frozendict():
|
|
82
|
+
return self._dict == other._dict
|
|
83
|
+
case dict():
|
|
84
|
+
return self._dict == other
|
|
85
|
+
case _:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
def __contains__(self, key) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Checks if the dictionary contains the given key.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
key: The key to check for in the dictionary.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if the key is present in the dictionary, False otherwise.
|
|
97
|
+
"""
|
|
98
|
+
return key in self._dict
|
|
99
|
+
|
|
100
|
+
def __len__(self) -> int:
|
|
101
|
+
"""
|
|
102
|
+
Returns the number of items in the dictionary.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The number of key-value pairs in the dictionary.
|
|
106
|
+
"""
|
|
107
|
+
return len(self._dict)
|
|
108
|
+
|
|
109
|
+
def keys(self):
|
|
110
|
+
"""
|
|
111
|
+
Returns an iterator over the dictionary's keys.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
An iterator over the keys of the dictionary.
|
|
115
|
+
"""
|
|
116
|
+
return self._dict.keys()
|
|
117
|
+
|
|
118
|
+
def values(self):
|
|
119
|
+
"""
|
|
120
|
+
Returns an iterator over the dictionary's values.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
An iterator over the values of the dictionary.
|
|
124
|
+
"""
|
|
125
|
+
return self._dict.values()
|
|
126
|
+
|
|
127
|
+
def items(self):
|
|
128
|
+
"""
|
|
129
|
+
Returns an iterator over the dictionary's key-value pairs.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
An iterator over the key-value pairs of the dictionary.
|
|
133
|
+
"""
|
|
134
|
+
return self._dict.items()
|
|
135
|
+
|
|
136
|
+
def __iter__(self):
|
|
137
|
+
"""
|
|
138
|
+
Returns an iterator over the dictionary's keys.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
An iterator over the keys of the dictionary.
|
|
142
|
+
"""
|
|
143
|
+
return iter(self._dict)
|
|
144
|
+
|
|
145
|
+
def __repr__(self) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Returns a string representation of the frozendict instance.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A string representation of the frozendict.
|
|
151
|
+
"""
|
|
152
|
+
return f"frozendict({self._dict})"
|
|
153
|
+
|
|
154
|
+
def __str__(self) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Returns a string representation of the frozendict instance.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A string representation of the frozendict.
|
|
160
|
+
"""
|
|
161
|
+
return f"frozendict({self._dict})"
|
|
162
|
+
|
|
163
|
+
def __hash__(self) -> int:
|
|
164
|
+
"""
|
|
165
|
+
Returns the hash value of the frozendict.
|
|
166
|
+
|
|
167
|
+
The hash is computed based on the key-value pairs in the dictionary.
|
|
168
|
+
This method ensures that the hash value is consistent for the lifetime
|
|
169
|
+
of the frozendict.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The hash value of the frozendict.
|
|
173
|
+
"""
|
|
174
|
+
if self._hash is None:
|
|
175
|
+
h = 0
|
|
176
|
+
for key, value in self._dict.items():
|
|
177
|
+
h ^= hash((key, value))
|
|
178
|
+
self._hash = h
|
|
179
|
+
return self._hash
|
|
180
|
+
|
|
181
|
+
def put(self, k: K, v: V) -> "frozendict":
|
|
182
|
+
"""
|
|
183
|
+
Returns a new frozendict with an updated value for the given key.
|
|
184
|
+
|
|
185
|
+
This method creates a new frozendict instance with the same contents as the current instance, but with
|
|
186
|
+
the value for the specified key updated.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
k: The key to update.
|
|
190
|
+
v: The new value for the key.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A new frozendict instance with the updated value.
|
|
194
|
+
"""
|
|
195
|
+
new_dict = deepcopy(self._dict)
|
|
196
|
+
new_dict[k] = v
|
|
197
|
+
return frozendict(new_dict)
|
|
198
|
+
|
|
199
|
+
def combine(self, other) -> "frozendict":
|
|
200
|
+
"""
|
|
201
|
+
Combines the current frozendict with another frozendict.
|
|
202
|
+
|
|
203
|
+
This method returns a new frozendict that contains all key-value pairs
|
|
204
|
+
from both frozendicts. If there are duplicate keys,
|
|
205
|
+
the values from the other frozendict will overwrite the values
|
|
206
|
+
from the current frozendict.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
other: The other frozendict to combine with.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
A new frozendict containing all key-value pairs from both frozendicts.
|
|
213
|
+
"""
|
|
214
|
+
return frozendict({**self.raw, **other.raw})
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def raw(self) -> dict:
|
|
218
|
+
"""
|
|
219
|
+
Gets the underlying dictionary in its raw form.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
The underlying dictionary.
|
|
223
|
+
"""
|
|
224
|
+
return self._dict
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def fromkeys(cls, *args, **kwargs) -> "frozendict":
|
|
228
|
+
"""
|
|
229
|
+
Creates a new frozendict with keys from the given iterable
|
|
230
|
+
and values set to a specified value.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
*args: Positional arguments passed to the dict.fromkeys method.
|
|
234
|
+
**kwargs: Keyword arguments passed to the dict.fromkeys method.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A new frozendict with the specified keys and values.
|
|
238
|
+
"""
|
|
239
|
+
return cls(dict.fromkeys(*args, **kwargs))
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def new() -> "frozendict":
|
|
243
|
+
"""
|
|
244
|
+
Creates a new, empty frozendict.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
A new, empty frozendict.
|
|
248
|
+
"""
|
|
249
|
+
return frozendict()
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def combine_dicts(fd1, fd2) -> "frozendict":
|
|
253
|
+
"""
|
|
254
|
+
Combines two frozendicts into a new frozendict.
|
|
255
|
+
|
|
256
|
+
This method returns a new frozendict that contains all key-value pairs
|
|
257
|
+
from both frozendicts. If there are duplicate keys,
|
|
258
|
+
the values from the second frozendict will overwrite the values
|
|
259
|
+
from the first frozendict.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
fd1: The first frozendict.
|
|
263
|
+
fd2: The second frozendict.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
A new frozendict containing all key-value pairs from both frozendicts.
|
|
267
|
+
"""
|
|
268
|
+
return fd1.combine(fd2)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A Lisp/ML/Scala style singly linked list ("cons list")
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import abstractmethod, ABC
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Callable, Iterable, Tuple
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
A = TypeVar("A")
|
|
12
|
+
|
|
13
|
+
type _CList = "CList[A]"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CList[A](ABC):
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def append(self, other: _CList) -> _CList: ...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def fold_right[B](self, acc: B, f: Callable[[A, B], B]) -> B: ...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def fold_left[B](self, acc: B, f: Callable[[B, A], B]) -> B: ...
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def drop(self, n: int) -> _CList: ...
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def drop_while(self, f: Callable[[A], bool]) -> _CList: ...
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def take(self, n: int) -> _CList: ...
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def take_while(self, f: Callable[[A], bool]) -> _CList: ...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def split_at(self, i: int) -> Tuple[_CList, _CList]: ...
|
|
41
|
+
|
|
42
|
+
def partition(self, f: Callable[[A], bool]) -> Tuple[_CList, _CList]:
|
|
43
|
+
accum = lambda a, x: (a << x[0], x[1]) if f(a) else (x[0], a << x[1])
|
|
44
|
+
return self.fold_right((Nil(), Nil()), accum)
|
|
45
|
+
|
|
46
|
+
def length(self) -> int:
|
|
47
|
+
return self.fold_right(0, lambda _, acc: acc + 1)
|
|
48
|
+
|
|
49
|
+
def prepend(self, new_head: A) -> _CList:
|
|
50
|
+
return Cons(new_head, self)
|
|
51
|
+
|
|
52
|
+
def reversed(self) -> _CList:
|
|
53
|
+
return self.fold_left(Nil(), lambda acc, h: Cons(h, acc))
|
|
54
|
+
|
|
55
|
+
def map[B](self, f: Callable[[A], B]) -> "CList[B]":
|
|
56
|
+
return self.fold_right(Nil(), lambda a, acc: Cons(f(a), acc))
|
|
57
|
+
|
|
58
|
+
def filter[A](self, f: Callable[[A], bool]) -> _CList:
|
|
59
|
+
return self.fold_right(Nil(), lambda a, acc: Cons(a, acc) if f(a) else acc)
|
|
60
|
+
|
|
61
|
+
def flatten(self) -> _CList:
|
|
62
|
+
return CList.flatten_(self)
|
|
63
|
+
|
|
64
|
+
def flat_map[B](self, f: Callable[[A], "CList[B]"]) -> "CList[B]":
|
|
65
|
+
return self.map(f).flatten()
|
|
66
|
+
|
|
67
|
+
def bind[B](self, f: Callable[[A], "CList[B]"]) -> "CList[B]":
|
|
68
|
+
return self.map(f).flatten()
|
|
69
|
+
|
|
70
|
+
def sorted(self, cmp: Callable[[A, A], int]) -> _CList:
|
|
71
|
+
def merge(left: _CList, right: _CList) -> _CList:
|
|
72
|
+
match left, right:
|
|
73
|
+
case Nil(), r:
|
|
74
|
+
return r
|
|
75
|
+
case l, Nil():
|
|
76
|
+
return l
|
|
77
|
+
case Cons(lh, lt), Cons(rh, rt):
|
|
78
|
+
if cmp(lh, rh) <= 0:
|
|
79
|
+
return lh << merge(lt, right)
|
|
80
|
+
return rh << merge(left, rt)
|
|
81
|
+
case _:
|
|
82
|
+
return Nil()
|
|
83
|
+
|
|
84
|
+
length = len(self)
|
|
85
|
+
if length <= 1:
|
|
86
|
+
return self
|
|
87
|
+
left, right = self.split_at(length // 2)
|
|
88
|
+
return merge(left.sorted(cmp), right.sorted(cmp))
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def flatten_(lst: "CList[CList[A]]") -> _CList:
|
|
92
|
+
def concat(left, right):
|
|
93
|
+
match left:
|
|
94
|
+
case Nil():
|
|
95
|
+
return right
|
|
96
|
+
case Cons(h, t):
|
|
97
|
+
return Cons(h, concat(t, right))
|
|
98
|
+
|
|
99
|
+
def flatten(lst: "CList[CList[A]]") -> "CList[A]":
|
|
100
|
+
match lst:
|
|
101
|
+
case Nil():
|
|
102
|
+
return Nil()
|
|
103
|
+
case Cons(h, t):
|
|
104
|
+
match h:
|
|
105
|
+
case Cons(_, _):
|
|
106
|
+
return concat(flatten(h), flatten(t))
|
|
107
|
+
case _:
|
|
108
|
+
return Cons(h, flatten(t))
|
|
109
|
+
|
|
110
|
+
return flatten(lst)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def cons(a: A) -> _CList:
|
|
114
|
+
return Cons(a)
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def empty() -> _CList:
|
|
118
|
+
return Nil()
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def new(*xs: A) -> _CList:
|
|
122
|
+
return Cons(xs[0], CList.new(*xs[1:])) if xs else Nil()
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def from_iterable[A](iterable: Iterable[A]) -> _CList:
|
|
126
|
+
return CList.new(*iterable)
|
|
127
|
+
|
|
128
|
+
def __rlshift__(self, other) -> _CList:
|
|
129
|
+
return self.prepend(other)
|
|
130
|
+
|
|
131
|
+
def __add__(self, other) -> _CList:
|
|
132
|
+
return self.append(other)
|
|
133
|
+
|
|
134
|
+
def __len__(self) -> int:
|
|
135
|
+
return self.fold_right(0, lambda _, acc: acc + 1)
|
|
136
|
+
|
|
137
|
+
def __iter__(self):
|
|
138
|
+
current = self
|
|
139
|
+
while isinstance(current, Cons):
|
|
140
|
+
yield current.head
|
|
141
|
+
current = current.tail
|
|
142
|
+
|
|
143
|
+
def __eq__(self, other) -> bool:
|
|
144
|
+
match self, other:
|
|
145
|
+
case Cons(sh, st), Cons(oh, ot):
|
|
146
|
+
return sh == oh and st == ot
|
|
147
|
+
case Nil(), Nil():
|
|
148
|
+
return True
|
|
149
|
+
case _:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Nil(CList):
|
|
154
|
+
|
|
155
|
+
_instance = None
|
|
156
|
+
|
|
157
|
+
def __new__(cls):
|
|
158
|
+
if cls._instance is None:
|
|
159
|
+
cls._instance = super(Nil, cls).__new__(cls)
|
|
160
|
+
return cls._instance
|
|
161
|
+
|
|
162
|
+
def __repr__(self):
|
|
163
|
+
return "Nil()"
|
|
164
|
+
|
|
165
|
+
def append(self, other: _CList) -> _CList:
|
|
166
|
+
return other
|
|
167
|
+
|
|
168
|
+
def fold_right[A, B](self, acc: B, f: Callable[[A, B], B]) -> B:
|
|
169
|
+
return acc
|
|
170
|
+
|
|
171
|
+
def fold_left[A, B](self, acc: B, f: Callable[[B, A], B]) -> B:
|
|
172
|
+
return acc
|
|
173
|
+
|
|
174
|
+
def drop(self, n: int) -> _CList:
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def drop_while[A](self, f: Callable[[A], bool]) -> _CList:
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def take(self, n: int) -> _CList:
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def take_while[A](self, f: Callable[[A], bool]) -> _CList:
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def split_at(self, i: int) -> Tuple[_CList, _CList]:
|
|
187
|
+
return self, self
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class Cons[A](CList[A]):
|
|
192
|
+
head: A
|
|
193
|
+
tail: CList[A] = field(default_factory=Nil)
|
|
194
|
+
|
|
195
|
+
def __repr__(self):
|
|
196
|
+
match self.tail:
|
|
197
|
+
case Nil():
|
|
198
|
+
return f"Cons({self.head})"
|
|
199
|
+
return f"Cons({self.head}, {self.tail})"
|
|
200
|
+
|
|
201
|
+
def append(self, other: _CList) -> _CList:
|
|
202
|
+
return Cons(self.head, self.tail.append(other))
|
|
203
|
+
|
|
204
|
+
def fold_right[B](self, acc: B, f: Callable[[A, B], B]) -> B:
|
|
205
|
+
return f(self.head, self.tail.fold_right(acc, f))
|
|
206
|
+
|
|
207
|
+
def fold_left[B](self, acc: B, f: Callable[[B, A], B]) -> B:
|
|
208
|
+
return self.tail.fold_left(f(acc, self.head), f)
|
|
209
|
+
|
|
210
|
+
def drop(self, n: int) -> _CList:
|
|
211
|
+
return self if n <= 0 else self.tail.drop(n - 1)
|
|
212
|
+
|
|
213
|
+
def drop_while(self, f: Callable[[A], bool]) -> _CList:
|
|
214
|
+
return self if not f(self.head) else self.tail.drop_while(f)
|
|
215
|
+
|
|
216
|
+
def take(self, n: int) -> _CList:
|
|
217
|
+
return Cons(self.head) if n <= 1 else self.head << self.tail.take(n - 1)
|
|
218
|
+
|
|
219
|
+
def take_while(self, f: Callable[[A], bool]) -> _CList:
|
|
220
|
+
return self.head << self.tail.take_while(f) if f(self.head) else Nil()
|
|
221
|
+
|
|
222
|
+
def split_at(self, i: int) -> Tuple[_CList, _CList]:
|
|
223
|
+
return self.take(i), self.drop(i)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "funstruct"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "fun & functional structures"
|
|
5
|
+
authors = ["Andrew Stefanich <andrewstefanich@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
keywords = ["functional", "immutable", "fp"]
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.12"
|
|
12
|
+
returns = "^0.23.0"
|
|
13
|
+
pytest-parametrization = "^2022.2.1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
[tool.poetry.group.dev.dependencies]
|
|
17
|
+
pytest = "^8.3.2"
|
|
18
|
+
debugpy = "^1.8.2"
|
|
19
|
+
black = "^24.4.2"
|
|
20
|
+
pre-commit = "^3.7.1"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["poetry-core"]
|
|
24
|
+
build-backend = "poetry.core.masonry.api"
|