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
FTuple(*dss: 'Iterable[D]')
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)
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 when FTuple 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 when FTuple 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 map(self, f: 'Callable[[D], U]', /) -> 'FTuple[U]':
179    def map[U](self, f: Callable[[D], U], /) -> FTuple[U]:
180        return FTuple(map(f, self))
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.