pythonic-fp.containers 2.0.0__py3-none-any.whl
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.
- pythonic_fp/containers/__init__.py +19 -0
- pythonic_fp/containers/box.py +159 -0
- pythonic_fp/containers/functional_tuple.py +189 -0
- pythonic_fp/containers/immutable_list.py +222 -0
- pythonic_fp/containers/maybe.py +226 -0
- pythonic_fp/containers/py.typed +0 -0
- pythonic_fp/containers/xor.py +314 -0
- pythonic_fp_containers-2.0.0.dist-info/METADATA +50 -0
- pythonic_fp_containers-2.0.0.dist-info/RECORD +11 -0
- pythonic_fp_containers-2.0.0.dist-info/WHEEL +4 -0
- pythonic_fp_containers-2.0.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
"""Developer Tools - Container-like data structures"""
|
|
16
|
+
|
|
17
|
+
__author__ = 'Geoffrey R. Scheller'
|
|
18
|
+
__copyright__ = 'Copyright (c) 2023-2025 Geoffrey R. Scheller'
|
|
19
|
+
__license__ = 'Apache License 2.0'
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
"""Stateful container holds at most one item of a given type."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
__all__ = ['Box']
|
|
20
|
+
|
|
21
|
+
from collections.abc import Callable, Iterator
|
|
22
|
+
from typing import cast, Final, Never, overload, TypeVar
|
|
23
|
+
from pythonic_fp.fptools.singletons import Sentinel
|
|
24
|
+
|
|
25
|
+
D = TypeVar('D')
|
|
26
|
+
|
|
27
|
+
_sentinel: Final[Sentinel] = Sentinel('Box')
|
|
28
|
+
|
|
29
|
+
class Box[D]:
|
|
30
|
+
"""Container holding at most one item of a given type
|
|
31
|
+
|
|
32
|
+
- where ``Box(item)`` contains at most one item of type ``~D``
|
|
33
|
+
|
|
34
|
+
- ``Box()`` creates an empty container
|
|
35
|
+
- can store any item of any type, including ``None``
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__slots__ = ('_item',)
|
|
39
|
+
__match_args__ = ('_item',)
|
|
40
|
+
|
|
41
|
+
T = TypeVar('T')
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
def __init__(self) -> None: ...
|
|
45
|
+
@overload
|
|
46
|
+
def __init__(self, value: D) -> None: ...
|
|
47
|
+
|
|
48
|
+
def __init__(self, value: D | Sentinel = Sentinel('Box')) -> None:
|
|
49
|
+
"""Initialize Box with an "optional" initial value.
|
|
50
|
+
|
|
51
|
+
:param value: "optional" initial value
|
|
52
|
+
:type value: ~D
|
|
53
|
+
"""
|
|
54
|
+
self._item: D | Sentinel = value
|
|
55
|
+
|
|
56
|
+
def __bool__(self) -> bool:
|
|
57
|
+
return self._item is not Sentinel('Box')
|
|
58
|
+
|
|
59
|
+
def __iter__(self) -> Iterator[D]:
|
|
60
|
+
if self:
|
|
61
|
+
yield cast(D, self._item)
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
if self:
|
|
65
|
+
return 'Box(' + repr(self._item) + ')'
|
|
66
|
+
return 'Box()'
|
|
67
|
+
|
|
68
|
+
def __len__(self) -> int:
|
|
69
|
+
return 1 if self else 0
|
|
70
|
+
|
|
71
|
+
def __eq__(self, other: object) -> bool:
|
|
72
|
+
if not isinstance(other, type(self)):
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
if self._item is other._item:
|
|
76
|
+
return True
|
|
77
|
+
if self._item == other._item:
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def get(self) -> D | Never: ...
|
|
83
|
+
@overload
|
|
84
|
+
def get(self, alt: D) -> D: ...
|
|
85
|
+
@overload
|
|
86
|
+
def get(self, alt: Sentinel) -> D | Never: ...
|
|
87
|
+
|
|
88
|
+
def get(self, alt: D | Sentinel = Sentinel('Box')) -> D | Never:
|
|
89
|
+
"""Return the contained value if it exists, otherwise an alternate value.
|
|
90
|
+
|
|
91
|
+
:rtype: ~D
|
|
92
|
+
:raises ValueError: when an alternate value is not provided but needed
|
|
93
|
+
"""
|
|
94
|
+
if self._item is not _sentinel:
|
|
95
|
+
return cast(D, self._item)
|
|
96
|
+
if alt is _sentinel:
|
|
97
|
+
msg = 'Box: get from empty Box with no alt return value provided'
|
|
98
|
+
raise ValueError(msg)
|
|
99
|
+
return cast(D, alt)
|
|
100
|
+
|
|
101
|
+
def pop(self) -> D | Never:
|
|
102
|
+
"""Pop the value if Box is not empty.
|
|
103
|
+
|
|
104
|
+
:rtype: ~D
|
|
105
|
+
:raises ValueError: if box is empty
|
|
106
|
+
"""
|
|
107
|
+
if self._item is _sentinel:
|
|
108
|
+
msg = 'Box: Trying to pop an item from an empty Box'
|
|
109
|
+
raise ValueError(msg)
|
|
110
|
+
popped = cast(D, self._item)
|
|
111
|
+
self._item = _sentinel
|
|
112
|
+
return popped
|
|
113
|
+
|
|
114
|
+
def push(self, item: D) -> None | Never:
|
|
115
|
+
"""Push an item in an empty Box.
|
|
116
|
+
|
|
117
|
+
:raises ValueError: if box is not empty
|
|
118
|
+
"""
|
|
119
|
+
if self._item is Sentinel('Box'):
|
|
120
|
+
self._item = item
|
|
121
|
+
else:
|
|
122
|
+
msg = 'Box: Trying to push an item in an empty Box'
|
|
123
|
+
raise ValueError(msg)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def put(self, item: D) -> None:
|
|
127
|
+
"""Put an item in the Box. Discard any previous contents."""
|
|
128
|
+
self._item = item
|
|
129
|
+
|
|
130
|
+
def exchange(self, new_item: D) -> D | Never:
|
|
131
|
+
"""Exchange an item with what is in the Box.
|
|
132
|
+
|
|
133
|
+
:rtype: ~D
|
|
134
|
+
:raises ValueError: if box is empty
|
|
135
|
+
"""
|
|
136
|
+
if self._item is _sentinel:
|
|
137
|
+
msg = 'Box: Trying to exchange items using an empty Box'
|
|
138
|
+
raise ValueError(msg)
|
|
139
|
+
popped = cast(D, self._item)
|
|
140
|
+
self._item = new_item
|
|
141
|
+
return popped
|
|
142
|
+
|
|
143
|
+
def map[T](self, f: Callable[[D], T]) -> Box[T]:
|
|
144
|
+
"""Map function ``f`` over contents.
|
|
145
|
+
|
|
146
|
+
:return: a new instance
|
|
147
|
+
"""
|
|
148
|
+
if self._item is Sentinel('Box'):
|
|
149
|
+
return Box()
|
|
150
|
+
return Box(f(cast(D, self._item)))
|
|
151
|
+
|
|
152
|
+
def bind[T](self, f: Callable[[D], Box[T]]) -> Box[T]:
|
|
153
|
+
"""Flatmap ``Box`` with function ``f``.
|
|
154
|
+
|
|
155
|
+
:return: a new instance
|
|
156
|
+
"""
|
|
157
|
+
if self._item is _sentinel:
|
|
158
|
+
return Box()
|
|
159
|
+
return f(cast(D, self._item))
|
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
"""Pythonic FP - Functional Tuple"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Callable, Iterator
|
|
20
|
+
from typing import cast, Never, overload
|
|
21
|
+
from pythonic_fp.iterables import FM, accumulate, concat, exhaust, merge
|
|
22
|
+
|
|
23
|
+
__all__ = ['FunctionalTuple', 'functional_tuple']
|
|
24
|
+
|
|
25
|
+
class FunctionalTuple[D](tuple[D, ...]):
|
|
26
|
+
"""Functional Tuple suitable for inheritance
|
|
27
|
+
|
|
28
|
+
- Supports both indexing and slicing
|
|
29
|
+
- FunctionalTuple addition and int multiplication supported
|
|
30
|
+
|
|
31
|
+
- addition concatenates results, resulting in a Union type
|
|
32
|
+
- both left and right int multiplication supported
|
|
33
|
+
- homogeneous in its data type
|
|
34
|
+
- supports being further inherited from
|
|
35
|
+
- unslotted
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __reversed__(self) -> Iterator[D]:
|
|
39
|
+
for ii in range(len(self) - 1, -1, -1):
|
|
40
|
+
yield (self[ii])
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f'{self.__class__.__name__}(' + ', '.join(map(repr, self)) + ')'
|
|
44
|
+
|
|
45
|
+
def __eq__(self, other: object, /) -> bool:
|
|
46
|
+
if not isinstance(other, self.__class__):
|
|
47
|
+
return False
|
|
48
|
+
if (length := len(self)) != len(other):
|
|
49
|
+
return False
|
|
50
|
+
if self is other:
|
|
51
|
+
return True
|
|
52
|
+
for ii in range(length):
|
|
53
|
+
if self[ii] != other[ii]:
|
|
54
|
+
return False
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
@overload # type:ignore
|
|
58
|
+
def __getitem__(self, x: int) -> D: ...
|
|
59
|
+
@overload
|
|
60
|
+
def __getitem__(self, x: slice) -> tuple[D, ...]: ...
|
|
61
|
+
|
|
62
|
+
def __getitem__(self, idx: int | slice, /) -> tuple[D, ...] | D:
|
|
63
|
+
def newtup(tup: tuple[D, ...]) -> tuple[D, ...]:
|
|
64
|
+
return tup[0:-1] + tup[-1:]
|
|
65
|
+
|
|
66
|
+
if isinstance(idx, slice):
|
|
67
|
+
return self.__class__(newtup(super().__getitem__(idx)))
|
|
68
|
+
else:
|
|
69
|
+
return super().__getitem__(idx)
|
|
70
|
+
|
|
71
|
+
def foldl[L](
|
|
72
|
+
self,
|
|
73
|
+
f: Callable[[L, D], L],
|
|
74
|
+
/,
|
|
75
|
+
start: L | None = None,
|
|
76
|
+
default: L | None = None,
|
|
77
|
+
) -> L | None:
|
|
78
|
+
"""Fold Left
|
|
79
|
+
|
|
80
|
+
- fold left with an optional starting value
|
|
81
|
+
- first argument of function f is for the accumulated value
|
|
82
|
+
|
|
83
|
+
:raises ValueError: when FunctionalTuple empty and a start value not given
|
|
84
|
+
"""
|
|
85
|
+
it = iter(self)
|
|
86
|
+
if start is not None:
|
|
87
|
+
acc = start
|
|
88
|
+
elif self:
|
|
89
|
+
acc = cast(L, next(it))
|
|
90
|
+
else:
|
|
91
|
+
if default is None:
|
|
92
|
+
msg = 'Both start and default cannot be None for an empty FunctionalTuple'
|
|
93
|
+
raise ValueError('FunctionalTuple.foldl - ' + msg)
|
|
94
|
+
acc = default
|
|
95
|
+
for v in it:
|
|
96
|
+
acc = f(acc, v)
|
|
97
|
+
return acc
|
|
98
|
+
|
|
99
|
+
def foldr[R](
|
|
100
|
+
self,
|
|
101
|
+
f: Callable[[D, R], R],
|
|
102
|
+
/,
|
|
103
|
+
start: R | None = None,
|
|
104
|
+
default: R | None = None,
|
|
105
|
+
) -> R | None:
|
|
106
|
+
"""Fold Right
|
|
107
|
+
|
|
108
|
+
- fold right with an optional starting value
|
|
109
|
+
- second argument of function f is for the accumulated value
|
|
110
|
+
|
|
111
|
+
:raises ValueError: when FunctionalTuple empty and a start value not given
|
|
112
|
+
"""
|
|
113
|
+
it = reversed(self)
|
|
114
|
+
if start is not None:
|
|
115
|
+
acc = start
|
|
116
|
+
elif self:
|
|
117
|
+
acc = cast(R, next(it))
|
|
118
|
+
else:
|
|
119
|
+
if default is None:
|
|
120
|
+
msg = 'Both start and default cannot be None for an empty FunctionalTuple'
|
|
121
|
+
raise ValueError('FunctionalTuple.foldR - ' + msg)
|
|
122
|
+
acc = default
|
|
123
|
+
for v in it:
|
|
124
|
+
acc = f(v, acc)
|
|
125
|
+
return acc
|
|
126
|
+
|
|
127
|
+
def copy(self) -> FunctionalTuple[D]:
|
|
128
|
+
"""Return a shallow copy of ``FunctionalTuple`` in O(1) time & space complexity."""
|
|
129
|
+
return self.__class__(self)
|
|
130
|
+
|
|
131
|
+
def __add__(self, other: object, /) -> tuple[D, ...]:
|
|
132
|
+
if not isinstance(other, FunctionalTuple):
|
|
133
|
+
msg = 'FunctionalTuple being added to something not an FunctionalTuple'
|
|
134
|
+
raise ValueError(msg)
|
|
135
|
+
return self.__class__(concat(self, other))
|
|
136
|
+
|
|
137
|
+
def __mul__(self, num: int, /) -> tuple[D, ...]:
|
|
138
|
+
return self.__class__(super().__mul__(num))
|
|
139
|
+
|
|
140
|
+
def __rmul__(self, num: int, /) -> tuple[D]:
|
|
141
|
+
return self.__class__(super().__rmul__(num))
|
|
142
|
+
|
|
143
|
+
def accummulate[L](
|
|
144
|
+
self, f: Callable[[L, D], L], s: L | None = None, /
|
|
145
|
+
) -> FunctionalTuple[L]:
|
|
146
|
+
"""Accumulate partial folds
|
|
147
|
+
|
|
148
|
+
Accumulate partial fold results in an ``FunctionalTuple`` with an optional
|
|
149
|
+
starting value.
|
|
150
|
+
"""
|
|
151
|
+
if s is None:
|
|
152
|
+
return FunctionalTuple(accumulate(self, f))
|
|
153
|
+
return FunctionalTuple(accumulate(self, f, s))
|
|
154
|
+
|
|
155
|
+
def map[U](self, f: Callable[[D], U], /) -> FunctionalTuple[U]:
|
|
156
|
+
return FunctionalTuple(map(f, self))
|
|
157
|
+
|
|
158
|
+
def bind[U](
|
|
159
|
+
self, f: Callable[[D], FunctionalTuple[U]], type: FM = FM.CONCAT, /
|
|
160
|
+
) -> FunctionalTuple[U] | Never:
|
|
161
|
+
"""Bind function ``f`` to the ``FunctionalTuple``.
|
|
162
|
+
|
|
163
|
+
- FM Enum types
|
|
164
|
+
|
|
165
|
+
- CONCAT: sequentially concatenate iterables one after the other
|
|
166
|
+
- MERGE: round-robin merge iterables until one is exhausted
|
|
167
|
+
- EXHAUST: round-robin merge iterables until all are exhausted
|
|
168
|
+
|
|
169
|
+
:param ds: values to instantiate FunctionalTuple
|
|
170
|
+
:return: resulting FunctionalTuple
|
|
171
|
+
"""
|
|
172
|
+
match type:
|
|
173
|
+
case FM.CONCAT:
|
|
174
|
+
return FunctionalTuple(concat(*map(f, self)))
|
|
175
|
+
case FM.MERGE:
|
|
176
|
+
return FunctionalTuple(merge(*map(f, self)))
|
|
177
|
+
case FM.EXHAUST:
|
|
178
|
+
return FunctionalTuple(exhaust(*map(f, self)))
|
|
179
|
+
|
|
180
|
+
raise ValueError('Unknown FM type')
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def functional_tuple[D](*ds: D):
|
|
184
|
+
"""Construct a ``FunctionalTuple`` from arguments.
|
|
185
|
+
|
|
186
|
+
:param ds: values to instantiate FunctionalTuple
|
|
187
|
+
:return: resulting FunctionalTuple
|
|
188
|
+
"""
|
|
189
|
+
return FunctionalTuple(ds)
|
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
"""Pythonic FP - Immutable guaranteed hashable lists
|
|
16
|
+
|
|
17
|
+
- hashable if elements are hashable
|
|
18
|
+
- declared covariant in its generic datatype
|
|
19
|
+
- hashability should be enforced by LSP tooling
|
|
20
|
+
- hashability will be enforced at runtime
|
|
21
|
+
- ImmutableList addition supported via concatenation
|
|
22
|
+
- ImmutableList integer multiplication supported
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections.abc import Callable, Iterable, Iterator, Hashable
|
|
29
|
+
from typing import cast, Never, overload, TypeVar
|
|
30
|
+
from pythonic_fp.iterables import FM, accumulate, concat, exhaust, merge
|
|
31
|
+
|
|
32
|
+
__all__ = ['ImmutableList', 'immutable_list']
|
|
33
|
+
|
|
34
|
+
D = TypeVar('D', covariant=True)
|
|
35
|
+
T = TypeVar('T')
|
|
36
|
+
|
|
37
|
+
class ImmutableList[D](Hashable):
|
|
38
|
+
"""Immutable List like data structure.
|
|
39
|
+
|
|
40
|
+
- its method type parameters are also covariant
|
|
41
|
+
- hashability will be enforced by LSP tooling
|
|
42
|
+
- supports both indexing and slicing
|
|
43
|
+
- addition concatenates results, resulting type a Union type
|
|
44
|
+
- both left and right int multiplication supported
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
__slots__ = ('_ds', '_len', '_hash')
|
|
48
|
+
__match_args__ = ('_ds', '_len')
|
|
49
|
+
|
|
50
|
+
L = TypeVar('L')
|
|
51
|
+
R = TypeVar('R')
|
|
52
|
+
U = TypeVar('U')
|
|
53
|
+
|
|
54
|
+
def __init__(self, *dss: Iterable[D]) -> None:
|
|
55
|
+
if (size := len(dss)) > 1:
|
|
56
|
+
msg = f'ImmutableList expects at most 1 iterable argument, got {size}'
|
|
57
|
+
raise ValueError(msg)
|
|
58
|
+
else:
|
|
59
|
+
self._ds: tuple[D, ...] = tuple(dss[0]) if size == 1 else tuple()
|
|
60
|
+
self._len = len(self._ds)
|
|
61
|
+
try:
|
|
62
|
+
self._hash = hash((self._len, 42) + self._ds)
|
|
63
|
+
except TypeError as exc:
|
|
64
|
+
msg = f'ImmutableList: {exc}'
|
|
65
|
+
raise TypeError(msg)
|
|
66
|
+
|
|
67
|
+
def __hash__(self) -> int:
|
|
68
|
+
return self._hash
|
|
69
|
+
|
|
70
|
+
def __iter__(self) -> Iterator[D]:
|
|
71
|
+
return iter(self._ds)
|
|
72
|
+
|
|
73
|
+
def __reversed__(self) -> Iterator[D]:
|
|
74
|
+
return reversed(self._ds)
|
|
75
|
+
|
|
76
|
+
def __bool__(self) -> bool:
|
|
77
|
+
return bool(self._ds)
|
|
78
|
+
|
|
79
|
+
def __len__(self) -> int:
|
|
80
|
+
return len(self._ds)
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
return 'immutable_list(' + ', '.join(map(repr, self)) + ')'
|
|
84
|
+
|
|
85
|
+
def __str__(self) -> str:
|
|
86
|
+
return '((' + ', '.join(map(repr, self)) + '))'
|
|
87
|
+
|
|
88
|
+
def __eq__(self, other: object, /) -> bool:
|
|
89
|
+
if not isinstance(other, ImmutableList):
|
|
90
|
+
return NotImplemented # magic object
|
|
91
|
+
if self._len != other._len:
|
|
92
|
+
return False
|
|
93
|
+
if self._ds is other._ds:
|
|
94
|
+
return True
|
|
95
|
+
return self._ds == other._ds
|
|
96
|
+
|
|
97
|
+
@overload
|
|
98
|
+
def __getitem__(self, idx: int, /) -> D: ...
|
|
99
|
+
@overload
|
|
100
|
+
def __getitem__(self, idx: slice, /) -> ImmutableList[D]: ...
|
|
101
|
+
|
|
102
|
+
def __getitem__(self, idx: slice | int, /) -> ImmutableList[D] | D:
|
|
103
|
+
if isinstance(idx, slice):
|
|
104
|
+
return ImmutableList(self._ds[idx])
|
|
105
|
+
return self._ds[idx]
|
|
106
|
+
|
|
107
|
+
def foldl[L](
|
|
108
|
+
self,
|
|
109
|
+
f: Callable[[L, D], L],
|
|
110
|
+
/,
|
|
111
|
+
start: L | None = None,
|
|
112
|
+
default: L | None = None,
|
|
113
|
+
) -> L | None:
|
|
114
|
+
"""Fold Left
|
|
115
|
+
|
|
116
|
+
- fold left with an optional starting value
|
|
117
|
+
- first argument of function ``f`` is for the accumulated value
|
|
118
|
+
|
|
119
|
+
"raises ValueError: when empty and a start value not given
|
|
120
|
+
"""
|
|
121
|
+
it = iter(self._ds)
|
|
122
|
+
if start is not None:
|
|
123
|
+
acc = start
|
|
124
|
+
elif self:
|
|
125
|
+
acc = cast(L, next(it)) # L_co = D_co in this case
|
|
126
|
+
else:
|
|
127
|
+
if default is None:
|
|
128
|
+
msg0 = 'ImmutableList: foldl method requires '
|
|
129
|
+
msg1 = 'either start or default to be defined for '
|
|
130
|
+
msg2 = 'an empty ImmutableList'
|
|
131
|
+
raise ValueError(msg0 + msg1 + msg2)
|
|
132
|
+
acc = default
|
|
133
|
+
for v in it:
|
|
134
|
+
acc = f(acc, v)
|
|
135
|
+
return acc
|
|
136
|
+
|
|
137
|
+
def foldr[R](
|
|
138
|
+
self,
|
|
139
|
+
f: Callable[[D, R], R],
|
|
140
|
+
/,
|
|
141
|
+
start: R | None = None,
|
|
142
|
+
default: R | None = None,
|
|
143
|
+
) -> R | None:
|
|
144
|
+
"""Fold Right
|
|
145
|
+
|
|
146
|
+
- fold right with an optional starting value
|
|
147
|
+
- second argument of function ``f`` is for the accumulated value
|
|
148
|
+
|
|
149
|
+
"raises ValueError: when empty and a start value not given
|
|
150
|
+
"""
|
|
151
|
+
it = reversed(self._ds)
|
|
152
|
+
if start is not None:
|
|
153
|
+
acc = start
|
|
154
|
+
elif self:
|
|
155
|
+
acc = cast(R, next(it))
|
|
156
|
+
else:
|
|
157
|
+
if default is None:
|
|
158
|
+
msg0 = 'ImmutableList: foldr method requires '
|
|
159
|
+
msg1 = 'either start or default to be defined for '
|
|
160
|
+
msg2 = 'an empty ImmutableList'
|
|
161
|
+
raise ValueError(msg0 + msg1 + msg2)
|
|
162
|
+
acc = default
|
|
163
|
+
for v in it:
|
|
164
|
+
acc = f(v, acc)
|
|
165
|
+
return acc
|
|
166
|
+
|
|
167
|
+
def __add__(self, other: ImmutableList[D], /) -> ImmutableList[D]:
|
|
168
|
+
if not isinstance(other, ImmutableList):
|
|
169
|
+
msg = 'ImmutableList being added to something not a ImmutableList'
|
|
170
|
+
raise ValueError(msg)
|
|
171
|
+
|
|
172
|
+
return ImmutableList(concat(self, other))
|
|
173
|
+
|
|
174
|
+
def __mul__(self, num: int, /) -> ImmutableList[D]:
|
|
175
|
+
return ImmutableList(self._ds.__mul__(num if num > 0 else 0))
|
|
176
|
+
|
|
177
|
+
def __rmul__(self, num: int, /) -> ImmutableList[D]:
|
|
178
|
+
return ImmutableList(self._ds.__mul__(num if num > 0 else 0))
|
|
179
|
+
|
|
180
|
+
def accummulate[L](
|
|
181
|
+
self, f: Callable[[L, D], L], s: L | None = None, /
|
|
182
|
+
) -> ImmutableList[L]:
|
|
183
|
+
"""Accumulate partial folds
|
|
184
|
+
|
|
185
|
+
Accumulate partial fold results in an ImmutableList with
|
|
186
|
+
an optional starting value.
|
|
187
|
+
"""
|
|
188
|
+
if s is None:
|
|
189
|
+
return ImmutableList(accumulate(self, f))
|
|
190
|
+
return ImmutableList(accumulate(self, f, s))
|
|
191
|
+
|
|
192
|
+
def map[U](self, f: Callable[[D], U], /) -> ImmutableList[U]:
|
|
193
|
+
return ImmutableList(map(f, self))
|
|
194
|
+
|
|
195
|
+
def bind[U](
|
|
196
|
+
self, f: Callable[[D], ImmutableList[U]], type: FM = FM.CONCAT, /
|
|
197
|
+
) -> ImmutableList[U] | Never:
|
|
198
|
+
"""Bind function `f` to the `ImmutableList`.
|
|
199
|
+
|
|
200
|
+
- FM Enum types
|
|
201
|
+
|
|
202
|
+
- CONCAT: sequentially concatenate iterables one after the other
|
|
203
|
+
- MERGE: round-robin merge iterables until one is exhausted
|
|
204
|
+
- EXHAUST: round-robin merge iterables until all are exhausted
|
|
205
|
+
"""
|
|
206
|
+
match type:
|
|
207
|
+
case FM.CONCAT:
|
|
208
|
+
return ImmutableList(concat(*map(f, self)))
|
|
209
|
+
case FM.MERGE:
|
|
210
|
+
return ImmutableList(merge(*map(f, self)))
|
|
211
|
+
case FM.EXHAUST:
|
|
212
|
+
return ImmutableList(exhaust(*map(f, self)))
|
|
213
|
+
|
|
214
|
+
raise ValueError(f'ImmutableList: Unknown FM type: {type}')
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def immutable_list[T](*ts: T) -> ImmutableList[TabError]:
|
|
218
|
+
"""Function to produce an ``ImmutableList`` from a variable number of arguments.
|
|
219
|
+
|
|
220
|
+
:param ds: initial values to push onto a new ImmutableList from right to left
|
|
221
|
+
"""
|
|
222
|
+
return ImmutableList(ts)
|