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