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