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.
@@ -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.
@@ -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,9 @@
1
+ # funstruct
2
+
3
+ Some of my fav lil data structures.
4
+
5
+ These are not meant to be highly performant.
6
+
7
+ They are useful for smaller datasets and for personal scripts.
8
+
9
+ Like all functional stuctures, they play very well with recursive algos.
@@ -0,0 +1,2 @@
1
+ from funstruct._frozendict import frozendict
2
+ from funstruct._cons import CList, Cons, Nil
@@ -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,17 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Nil2_: ...
6
+
7
+
8
+ Nil2 = Nil2_()
9
+
10
+
11
+ @dataclass
12
+ class Cons2[A]:
13
+ head: A
14
+ tail: "Cons2[A]"
15
+
16
+ def __repr__(self):
17
+ return f"Cons({self.head}, {self.tail})"
@@ -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"