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
FTuple(*dss: Iterable[~D])
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)
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 when FTuple 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 when FTuple empty and a start value not given
def copy(self) -> FTuple[~D]:
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.

def accummulate( self, f: Callable[[~L, ~D], ~L], s: Optional[~L] = None, /) -> FTuple[~L]:
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 map(self, f: Callable[[~D], ~U], /) -> FTuple[~U]:
194    def map[U](self, f: Callable[[D], U], /) -> FTuple[U]:
195        return FTuple(map(f, self))
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