grscheller.datastructures.tuples
Tuple based data structures
Only example here is the ftuple, basically an FP interface wrapping a tuple. Originally it inherited from tuple, but I found containing the tuple in a "has-a" relationship makes for a faster implementation. Buried in the git history is another example called a "process array" (parray) which I might return to someday. The idea of the parray is a fixed length sequence with sentinel values.
FTuple and FT factory function.
- class FTuple: Wrapped tuple with a Functional Programming API
- function FE:
1# Copyright 2023-2024 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"""### Tuple based data structures 16 17Only example here is the ftuple, basically an FP interface wrapping a tuple. 18Originally it inherited from tuple, but I found containing the tuple in a 19"has-a" relationship makes for a faster implementation. Buried in the git 20history is another example called a "process array" (parray) which I might 21return to someday. The idea of the parray is a fixed length sequence with 22sentinel values. 23 24#### FTuple and FT factory function. 25 26* class FTuple: Wrapped tuple with a Functional Programming API 27* function FE: 28 29""" 30 31from __future__ import annotations 32 33from collections.abc import Callable, Iterable, Iterator, Sequence 34from typing import cast, overload 35from grscheller.fp.iterables import FM, accumulate, concat, exhaust, merge 36 37__all__ = ['FTuple', 'FT'] 38 39class FTuple[D](Sequence[D]): 40 """ 41 #### Functional Tuple 42 43 * immutable tuple-like data structure with a functional interface 44 * supports both indexing and slicing 45 * `FTuple` addition & `int` multiplication supported 46 * addition concatenates results, resulting type a Union type 47 * both left and right int multiplication supported 48 49 """ 50 __slots__ = '_ds' 51 52 def __init__(self, *dss: Iterable[D]) -> None: 53 if len(dss) < 2: 54 self._ds: tuple[D, ...] = tuple(*dss) 55 else: 56 msg = f'FTuple expected at most 1 iterable argument, got {len(dss)}' 57 raise TypeError(msg) 58 59 def __iter__(self) -> Iterator[D]: 60 return iter(self._ds) 61 62 def __reversed__(self) -> Iterator[D]: 63 return reversed(self._ds) 64 65 def __bool__(self) -> bool: 66 return bool(len(self._ds)) 67 68 def __len__(self) -> int: 69 return len(self._ds) 70 71 def __repr__(self) -> str: 72 return 'FT(' + ', '.join(map(repr, self)) + ')' 73 74 def __str__(self) -> str: 75 return "((" + ", ".join(map(repr, self)) + "))" 76 77 def __eq__(self, other: object, /) -> bool: 78 if self is other: 79 return True 80 if not isinstance(other, type(self)): 81 return False 82 return self._ds == other._ds 83 84 @overload 85 def __getitem__(self, idx: int, /) -> D: ... 86 @overload 87 def __getitem__(self, idx: slice, /) -> FTuple[D]: ... 88 89 def __getitem__(self, idx: slice|int, /) -> FTuple[D]|D: 90 if isinstance(idx, slice): 91 return FTuple(self._ds[idx]) 92 else: 93 return self._ds[idx] 94 95 def foldL[L](self, 96 f: Callable[[L, D], L], /, 97 start: L|None=None, 98 default: L|None=None) -> L|None: 99 """ 100 **Fold Left** 101 102 * fold left with an optional starting value 103 * first argument of function `f` is for the accumulated value 104 * throws `ValueError` when `FTuple` empty and a start value not given 105 106 """ 107 it = iter(self._ds) 108 if start is not None: 109 acc = start 110 elif self: 111 acc = cast(L, next(it)) # L = D in this case 112 else: 113 if default is None: 114 msg = 'Both start and default cannot be None for an empty FTuple' 115 raise ValueError('FTuple.foldL - ' + msg) 116 acc = default 117 for v in it: 118 acc = f(acc, v) 119 return acc 120 121 def foldR[R](self, 122 f: Callable[[D, R], R], /, 123 start: R|None=None, 124 default: R|None=None) -> R|None: 125 """ 126 **Fold Right** 127 128 * fold right with an optional starting value 129 * second argument of function `f` is for the accumulated value 130 * throws `ValueError` when `FTuple` empty and a start value not given 131 132 """ 133 it = reversed(self._ds) 134 if start is not None: 135 acc = start 136 elif self: 137 acc = cast(R, next(it)) # R = D in this case 138 else: 139 if default is None: 140 msg = 'Both start and default cannot be None for an empty FTuple' 141 raise ValueError('FTuple.foldR - ' + msg) 142 acc = default 143 for v in it: 144 acc = f(v, acc) 145 return acc 146 147 def copy(self) -> FTuple[D]: 148 """ 149 **Copy** 150 151 Return a shallow copy of the FTuple in O(1) time & space complexity. 152 153 """ 154 return FTuple(self) 155 156 def __add__[E](self, other: FTuple[E], /) -> FTuple[D|E]: 157 return FTuple(concat(self, other)) 158 159 def __mul__(self, num: int, /) -> FTuple[D]: 160 return FTuple(self._ds.__mul__(num if num > 0 else 0)) 161 162 def __rmul__(self, num: int, /) -> FTuple[D]: 163 return FTuple(self._ds.__mul__(num if num > 0 else 0)) 164 165 def accummulate[L](self, f: Callable[[L, D], L], s: L|None=None, /) -> FTuple[L]: 166 """ 167 **Accumulate partial folds** 168 169 Accumulate partial fold results in an FTuple with an optional starting 170 value. 171 172 """ 173 if s is None: 174 return FTuple(accumulate(self, f)) 175 else: 176 return FTuple(accumulate(self, f, s)) 177 178 def map[U](self, f: Callable[[D], U], /) -> FTuple[U]: 179 return FTuple(map(f, self)) 180 181 def bind[U](self, f: Callable[[D], FTuple[U]], type: FM=FM.CONCAT, /) -> FTuple[U]: 182 """ 183 Bind function `f` to the `FTuple`. 184 185 * type = CONCAT: sequentially concatenate iterables one after the other 186 * type = MERGE: merge iterables together until one is exhausted 187 * type = Exhaust: merge iterables together until all are exhausted 188 189 """ 190 match type: 191 case FM.CONCAT: 192 return FTuple(concat(*map(lambda x: iter(x), map(f, self)))) 193 case FM.MERGE: 194 return FTuple(merge(*map(lambda x: iter(x), map(f, self)))) 195 case FM.EXHAUST: 196 return FTuple(exhaust(*map(lambda x: iter(x), map(f, self)))) 197 case '*': 198 raise ValueError('Unknown FM type') 199 200def FT[D](*ds: D) -> FTuple[D]: 201 """Return an FTuple whose values are the function arguments.""" 202 return FTuple(ds)
class
FTuple(collections.abc.Sequence[D], typing.Generic[D]):
40class FTuple[D](Sequence[D]): 41 """ 42 #### Functional Tuple 43 44 * immutable tuple-like data structure with a functional interface 45 * supports both indexing and slicing 46 * `FTuple` addition & `int` multiplication supported 47 * addition concatenates results, resulting type a Union type 48 * both left and right int multiplication supported 49 50 """ 51 __slots__ = '_ds' 52 53 def __init__(self, *dss: Iterable[D]) -> None: 54 if len(dss) < 2: 55 self._ds: tuple[D, ...] = tuple(*dss) 56 else: 57 msg = f'FTuple expected at most 1 iterable argument, got {len(dss)}' 58 raise TypeError(msg) 59 60 def __iter__(self) -> Iterator[D]: 61 return iter(self._ds) 62 63 def __reversed__(self) -> Iterator[D]: 64 return reversed(self._ds) 65 66 def __bool__(self) -> bool: 67 return bool(len(self._ds)) 68 69 def __len__(self) -> int: 70 return len(self._ds) 71 72 def __repr__(self) -> str: 73 return 'FT(' + ', '.join(map(repr, self)) + ')' 74 75 def __str__(self) -> str: 76 return "((" + ", ".join(map(repr, self)) + "))" 77 78 def __eq__(self, other: object, /) -> bool: 79 if self is other: 80 return True 81 if not isinstance(other, type(self)): 82 return False 83 return self._ds == other._ds 84 85 @overload 86 def __getitem__(self, idx: int, /) -> D: ... 87 @overload 88 def __getitem__(self, idx: slice, /) -> FTuple[D]: ... 89 90 def __getitem__(self, idx: slice|int, /) -> FTuple[D]|D: 91 if isinstance(idx, slice): 92 return FTuple(self._ds[idx]) 93 else: 94 return self._ds[idx] 95 96 def foldL[L](self, 97 f: Callable[[L, D], L], /, 98 start: L|None=None, 99 default: L|None=None) -> L|None: 100 """ 101 **Fold Left** 102 103 * fold left with an optional starting value 104 * first argument of function `f` is for the accumulated value 105 * throws `ValueError` when `FTuple` empty and a start value not given 106 107 """ 108 it = iter(self._ds) 109 if start is not None: 110 acc = start 111 elif self: 112 acc = cast(L, next(it)) # L = D in this case 113 else: 114 if default is None: 115 msg = 'Both start and default cannot be None for an empty FTuple' 116 raise ValueError('FTuple.foldL - ' + msg) 117 acc = default 118 for v in it: 119 acc = f(acc, v) 120 return acc 121 122 def foldR[R](self, 123 f: Callable[[D, R], R], /, 124 start: R|None=None, 125 default: R|None=None) -> R|None: 126 """ 127 **Fold Right** 128 129 * fold right with an optional starting value 130 * second argument of function `f` is for the accumulated value 131 * throws `ValueError` when `FTuple` empty and a start value not given 132 133 """ 134 it = reversed(self._ds) 135 if start is not None: 136 acc = start 137 elif self: 138 acc = cast(R, next(it)) # R = D in this case 139 else: 140 if default is None: 141 msg = 'Both start and default cannot be None for an empty FTuple' 142 raise ValueError('FTuple.foldR - ' + msg) 143 acc = default 144 for v in it: 145 acc = f(v, acc) 146 return acc 147 148 def copy(self) -> FTuple[D]: 149 """ 150 **Copy** 151 152 Return a shallow copy of the FTuple in O(1) time & space complexity. 153 154 """ 155 return FTuple(self) 156 157 def __add__[E](self, other: FTuple[E], /) -> FTuple[D|E]: 158 return FTuple(concat(self, other)) 159 160 def __mul__(self, num: int, /) -> FTuple[D]: 161 return FTuple(self._ds.__mul__(num if num > 0 else 0)) 162 163 def __rmul__(self, num: int, /) -> FTuple[D]: 164 return FTuple(self._ds.__mul__(num if num > 0 else 0)) 165 166 def accummulate[L](self, f: Callable[[L, D], L], s: L|None=None, /) -> FTuple[L]: 167 """ 168 **Accumulate partial folds** 169 170 Accumulate partial fold results in an FTuple with an optional starting 171 value. 172 173 """ 174 if s is None: 175 return FTuple(accumulate(self, f)) 176 else: 177 return FTuple(accumulate(self, f, s)) 178 179 def map[U](self, f: Callable[[D], U], /) -> FTuple[U]: 180 return FTuple(map(f, self)) 181 182 def bind[U](self, f: Callable[[D], FTuple[U]], type: FM=FM.CONCAT, /) -> FTuple[U]: 183 """ 184 Bind function `f` to the `FTuple`. 185 186 * type = CONCAT: sequentially concatenate iterables one after the other 187 * type = MERGE: merge iterables together until one is exhausted 188 * type = Exhaust: merge iterables together until all are exhausted 189 190 """ 191 match type: 192 case FM.CONCAT: 193 return FTuple(concat(*map(lambda x: iter(x), map(f, self)))) 194 case FM.MERGE: 195 return FTuple(merge(*map(lambda x: iter(x), map(f, self)))) 196 case FM.EXHAUST: 197 return FTuple(exhaust(*map(lambda x: iter(x), map(f, self)))) 198 case '*': 199 raise ValueError('Unknown FM type')
Functional Tuple
- immutable tuple-like data structure with a functional interface
- supports both indexing and slicing
FTuple
addition &int
multiplication supported- addition concatenates results, resulting type a Union type
- both left and right int multiplication supported
def
foldL( self, f: 'Callable[[L, D], L]', /, start: 'L | None' = None, default: 'L | None' = None) -> 'L | None':
96 def foldL[L](self, 97 f: Callable[[L, D], L], /, 98 start: L|None=None, 99 default: L|None=None) -> L|None: 100 """ 101 **Fold Left** 102 103 * fold left with an optional starting value 104 * first argument of function `f` is for the accumulated value 105 * throws `ValueError` when `FTuple` empty and a start value not given 106 107 """ 108 it = iter(self._ds) 109 if start is not None: 110 acc = start 111 elif self: 112 acc = cast(L, next(it)) # L = D in this case 113 else: 114 if default is None: 115 msg = 'Both start and default cannot be None for an empty FTuple' 116 raise ValueError('FTuple.foldL - ' + msg) 117 acc = default 118 for v in it: 119 acc = f(acc, v) 120 return acc
Fold Left
- fold left with an optional starting value
- first argument of function
f
is for the accumulated value - throws
ValueError
whenFTuple
empty and a start value not given
def
foldR( self, f: 'Callable[[D, R], R]', /, start: 'R | None' = None, default: 'R | None' = None) -> 'R | None':
122 def foldR[R](self, 123 f: Callable[[D, R], R], /, 124 start: R|None=None, 125 default: R|None=None) -> R|None: 126 """ 127 **Fold Right** 128 129 * fold right with an optional starting value 130 * second argument of function `f` is for the accumulated value 131 * throws `ValueError` when `FTuple` empty and a start value not given 132 133 """ 134 it = reversed(self._ds) 135 if start is not None: 136 acc = start 137 elif self: 138 acc = cast(R, next(it)) # R = D in this case 139 else: 140 if default is None: 141 msg = 'Both start and default cannot be None for an empty FTuple' 142 raise ValueError('FTuple.foldR - ' + msg) 143 acc = default 144 for v in it: 145 acc = f(v, acc) 146 return acc
Fold Right
- fold right with an optional starting value
- second argument of function
f
is for the accumulated value - throws
ValueError
whenFTuple
empty and a start value not given
def
copy(self) -> 'FTuple[D]':
148 def copy(self) -> FTuple[D]: 149 """ 150 **Copy** 151 152 Return a shallow copy of the FTuple in O(1) time & space complexity. 153 154 """ 155 return FTuple(self)
Copy
Return a shallow copy of the FTuple in O(1) time & space complexity.
def
accummulate(self, f: 'Callable[[L, D], L]', s: 'L | None' = None, /) -> 'FTuple[L]':
166 def accummulate[L](self, f: Callable[[L, D], L], s: L|None=None, /) -> FTuple[L]: 167 """ 168 **Accumulate partial folds** 169 170 Accumulate partial fold results in an FTuple with an optional starting 171 value. 172 173 """ 174 if s is None: 175 return FTuple(accumulate(self, f)) 176 else: 177 return FTuple(accumulate(self, f, s))
Accumulate partial folds
Accumulate partial fold results in an FTuple with an optional starting value.
def
bind( self, f: 'Callable[[D], FTuple[U]]', type: grscheller.fp.iterables.FM = <FM.CONCAT: 1>, /) -> 'FTuple[U]':
182 def bind[U](self, f: Callable[[D], FTuple[U]], type: FM=FM.CONCAT, /) -> FTuple[U]: 183 """ 184 Bind function `f` to the `FTuple`. 185 186 * type = CONCAT: sequentially concatenate iterables one after the other 187 * type = MERGE: merge iterables together until one is exhausted 188 * type = Exhaust: merge iterables together until all are exhausted 189 190 """ 191 match type: 192 case FM.CONCAT: 193 return FTuple(concat(*map(lambda x: iter(x), map(f, self)))) 194 case FM.MERGE: 195 return FTuple(merge(*map(lambda x: iter(x), map(f, self)))) 196 case FM.EXHAUST: 197 return FTuple(exhaust(*map(lambda x: iter(x), map(f, self)))) 198 case '*': 199 raise ValueError('Unknown FM type')
Bind function f
to the FTuple
.
- type = CONCAT: sequentially concatenate iterables one after the other
- type = MERGE: merge iterables together until one is exhausted
- type = Exhaust: merge iterables together until all are exhausted
def
FT(*ds: 'D') -> 'FTuple[D]':
201def FT[D](*ds: D) -> FTuple[D]: 202 """Return an FTuple whose values are the function arguments.""" 203 return FTuple(ds)
Return an FTuple whose values are the function arguments.