grscheller.datastructures.tuples

Tuple based datastructures:

Tuple-like object with FP behaviors.

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

Bind function to FTuple

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