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.
@@ -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)