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