grscheller.datastructures.tuples

Functional Tuple

FTuple

Immutable Tuple-like data structure with a functional interfaces.


  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### Functional Tuple
 17
 18##### FTuple
 19
 20Immutable Tuple-like data structure with a functional interfaces.
 21
 22---
 23
 24"""
 25
 26from __future__ import annotations
 27
 28from enum import auto, Enum
 29from typing import Callable, Iterator, Generic, Optional, TypeVar
 30from grscheller.fp.iterables import FM, accumulate, concat, exhaust, merge
 31
 32__all__ = ['FTuple']
 33
 34T = TypeVar('T')
 35S = TypeVar('S')
 36
 37class FTuple(Generic[T]):
 38    """
 39    #### Class FTuple
 40
 41    Implements a Tuple-like object with FP behaviors.
 42
 43    """
 44    __slots__ = '_ds'
 45
 46    def __init__(self, *ds: T):
 47        self._ds = ds
 48
 49    def __iter__(self) -> Iterator[T]:
 50        return iter(self._ds)
 51
 52    def __reversed__(self) -> Iterator[T]:
 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[T]|Optional[T]:
 73        """Supports both indexing and slicing."""
 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(self, f: Callable[[S, T], S], start: Optional[S]=None, default: Optional[S]=None) -> S:
 83        """
 84        ##### Fold Left
 85
 86        * fold left with an optional starting value
 87        * first argument of function f is for the accumulated value
 88        * if empty, return `start` if given, otherwise raise ValueError
 89
 90        """
 91        it = iter(self._ds)
 92        if start is not None:
 93            acc = start
 94        elif not self:
 95            if default is None:
 96                msg = 'Both start and default cannot be None for an empty FTuple'
 97                raise ValueError('FTuple.foldL - ' + msg)
 98            acc = default
 99        else:
100            acc = next(it)                # type: ignore # in this case _S == _T
101        for v in it:
102            acc = f(acc, v)
103        return acc
104
105    def foldR(self, f: Callable[[T, S], S], start: Optional[S]=None, default: Optional[S]=None) -> S:
106        """
107        ##### Fold Right
108
109        * fold right with an optional starting value
110        * second argument of function f is for the accumulated value
111        * if empty, return `start` if given, otherwise raise ValueError
112
113        """
114        it = reversed(self._ds)
115        if start is not None:
116            acc = start
117        elif not self:
118            if default is None:
119                msg = 'Both start and default cannot be None for an empty FTuple'
120                raise ValueError('FTuple.foldR - ' + msg)
121            acc = default
122        else:
123            acc = next(it)                # type: ignore # in this case _S == _T
124        for v in it:
125            acc = f(v, acc)
126        return acc
127
128    def copy(self) -> FTuple[T]:
129        """
130        ##### Shallow Copy
131
132        Return shallow copy of the FTuple in O(1) time & space complexity.
133
134        """
135        return FTuple(*self)
136
137    def map(self, f: Callable[[T], S]) -> FTuple[S]:
138        return FTuple(*map(f, self))
139
140    def __add__(self, other: FTuple[T]) -> FTuple[T]:
141        """
142        ##### Concatenate two FTuples
143
144        """
145        return FTuple(*concat(iter(self), other))
146
147    def __mul__(self, num: int) -> FTuple[T]:
148        """
149        ##### Mult by int
150
151        Return an FTuple which repeats another FTuple num times.
152
153        """
154        return FTuple(*self._ds.__mul__(num if num > 0 else 0))
155
156    def accummulate(self, f: Callable[[S, T], S], s: Optional[S]=None) -> FTuple[S]:
157        """
158        ##### Accumulate
159
160        Accumulate partial fold results in an FTuple with an optional starting value.
161
162        """
163        if s is None:
164            return FTuple(*accumulate(self, f))
165        else:
166            return FTuple(*accumulate(self, f, s))
167
168    def flatMap(self, f: Callable[[T], FTuple[S]], type: FM=FM.CONCAT) -> FTuple[S]:
169        """
170        ##### Bind function to FTuple
171
172        Bind function `f` to the FTuple.
173
174        * type = CONCAT: sequentially concatenate iterables one after the other
175        * type = MERGE: merge iterables together until one is exhausted
176        * type = Exhaust: merge iterables together until all are exhausted
177
178        """
179        match type:
180            case FM.CONCAT:
181                return FTuple(*concat(*map(lambda x: iter(x), map(f, self))))
182            case FM.MERGE:
183                return FTuple(*merge(*map(lambda x: iter(x), map(f, self))))
184            case FM.EXHAUST:
185                return FTuple(*exhaust(*map(lambda x: iter(x), map(f, self))))
186            case '*':
187                raise ValueError('Unknown FM type')
class FTuple(typing.Generic[~T]):
 38class FTuple(Generic[T]):
 39    """
 40    #### Class FTuple
 41
 42    Implements a Tuple-like object with FP behaviors.
 43
 44    """
 45    __slots__ = '_ds'
 46
 47    def __init__(self, *ds: T):
 48        self._ds = ds
 49
 50    def __iter__(self) -> Iterator[T]:
 51        return iter(self._ds)
 52
 53    def __reversed__(self) -> Iterator[T]:
 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[T]|Optional[T]:
 74        """Supports both indexing and slicing."""
 75        if isinstance(sl, slice):
 76            return FTuple(*self._ds[sl])
 77        try:
 78            item = self._ds[sl]
 79        except IndexError:
 80            item = None
 81        return item
 82
 83    def foldL(self, f: Callable[[S, T], S], start: Optional[S]=None, default: Optional[S]=None) -> S:
 84        """
 85        ##### Fold Left
 86
 87        * fold left with an optional starting value
 88        * first argument of function f is for the accumulated value
 89        * if empty, return `start` if given, otherwise raise ValueError
 90
 91        """
 92        it = iter(self._ds)
 93        if start is not None:
 94            acc = start
 95        elif not self:
 96            if default is None:
 97                msg = 'Both start and default cannot be None for an empty FTuple'
 98                raise ValueError('FTuple.foldL - ' + msg)
 99            acc = default
100        else:
101            acc = next(it)                # type: ignore # in this case _S == _T
102        for v in it:
103            acc = f(acc, v)
104        return acc
105
106    def foldR(self, f: Callable[[T, S], S], start: Optional[S]=None, default: Optional[S]=None) -> S:
107        """
108        ##### Fold Right
109
110        * fold right with an optional starting value
111        * second argument of function f is for the accumulated value
112        * if empty, return `start` if given, otherwise raise ValueError
113
114        """
115        it = reversed(self._ds)
116        if start is not None:
117            acc = start
118        elif not self:
119            if default is None:
120                msg = 'Both start and default cannot be None for an empty FTuple'
121                raise ValueError('FTuple.foldR - ' + msg)
122            acc = default
123        else:
124            acc = next(it)                # type: ignore # in this case _S == _T
125        for v in it:
126            acc = f(v, acc)
127        return acc
128
129    def copy(self) -> FTuple[T]:
130        """
131        ##### Shallow Copy
132
133        Return shallow copy of the FTuple in O(1) time & space complexity.
134
135        """
136        return FTuple(*self)
137
138    def map(self, f: Callable[[T], S]) -> FTuple[S]:
139        return FTuple(*map(f, self))
140
141    def __add__(self, other: FTuple[T]) -> FTuple[T]:
142        """
143        ##### Concatenate two FTuples
144
145        """
146        return FTuple(*concat(iter(self), other))
147
148    def __mul__(self, num: int) -> FTuple[T]:
149        """
150        ##### Mult by int
151
152        Return an FTuple which repeats another FTuple num times.
153
154        """
155        return FTuple(*self._ds.__mul__(num if num > 0 else 0))
156
157    def accummulate(self, f: Callable[[S, T], S], s: Optional[S]=None) -> FTuple[S]:
158        """
159        ##### Accumulate
160
161        Accumulate partial fold results in an FTuple with an optional starting value.
162
163        """
164        if s is None:
165            return FTuple(*accumulate(self, f))
166        else:
167            return FTuple(*accumulate(self, f, s))
168
169    def flatMap(self, f: Callable[[T], FTuple[S]], type: FM=FM.CONCAT) -> FTuple[S]:
170        """
171        ##### Bind function to FTuple
172
173        Bind function `f` to the FTuple.
174
175        * type = CONCAT: sequentially concatenate iterables one after the other
176        * type = MERGE: merge iterables together until one is exhausted
177        * type = Exhaust: merge iterables together until all are exhausted
178
179        """
180        match type:
181            case FM.CONCAT:
182                return FTuple(*concat(*map(lambda x: iter(x), map(f, self))))
183            case FM.MERGE:
184                return FTuple(*merge(*map(lambda x: iter(x), map(f, self))))
185            case FM.EXHAUST:
186                return FTuple(*exhaust(*map(lambda x: iter(x), map(f, self))))
187            case '*':
188                raise ValueError('Unknown FM type')

Class FTuple

Implements a Tuple-like object with FP behaviors.

FTuple(*ds: ~T)
47    def __init__(self, *ds: T):
48        self._ds = ds
def foldL( self, f: Callable[[~S, ~T], ~S], start: Optional[~S] = None, default: Optional[~S] = None) -> ~S:
 83    def foldL(self, f: Callable[[S, T], S], start: Optional[S]=None, default: Optional[S]=None) -> S:
 84        """
 85        ##### Fold Left
 86
 87        * fold left with an optional starting value
 88        * first argument of function f is for the accumulated value
 89        * if empty, return `start` if given, otherwise raise ValueError
 90
 91        """
 92        it = iter(self._ds)
 93        if start is not None:
 94            acc = start
 95        elif not self:
 96            if default is None:
 97                msg = 'Both start and default cannot be None for an empty FTuple'
 98                raise ValueError('FTuple.foldL - ' + msg)
 99            acc = default
100        else:
101            acc = next(it)                # type: ignore # in this case _S == _T
102        for v in it:
103            acc = f(acc, v)
104        return acc
Fold Left
  • fold left with an optional starting value
  • first argument of function f is for the accumulated value
  • if empty, return start if given, otherwise raise ValueError
def foldR( self, f: Callable[[~T, ~S], ~S], start: Optional[~S] = None, default: Optional[~S] = None) -> ~S:
106    def foldR(self, f: Callable[[T, S], S], start: Optional[S]=None, default: Optional[S]=None) -> S:
107        """
108        ##### Fold Right
109
110        * fold right with an optional starting value
111        * second argument of function f is for the accumulated value
112        * if empty, return `start` if given, otherwise raise ValueError
113
114        """
115        it = reversed(self._ds)
116        if start is not None:
117            acc = start
118        elif not self:
119            if default is None:
120                msg = 'Both start and default cannot be None for an empty FTuple'
121                raise ValueError('FTuple.foldR - ' + msg)
122            acc = default
123        else:
124            acc = next(it)                # type: ignore # in this case _S == _T
125        for v in it:
126            acc = f(v, acc)
127        return acc
Fold Right
  • fold right with an optional starting value
  • second argument of function f is for the accumulated value
  • if empty, return start if given, otherwise raise ValueError
def copy(self) -> FTuple[~T]:
129    def copy(self) -> FTuple[T]:
130        """
131        ##### Shallow Copy
132
133        Return shallow copy of the FTuple in O(1) time & space complexity.
134
135        """
136        return FTuple(*self)
Shallow Copy

Return shallow copy of the FTuple in O(1) time & space complexity.

def map( self, f: Callable[[~T], ~S]) -> FTuple[~S]:
138    def map(self, f: Callable[[T], S]) -> FTuple[S]:
139        return FTuple(*map(f, self))
def accummulate( self, f: Callable[[~S, ~T], ~S], s: Optional[~S] = None) -> FTuple[~S]:
157    def accummulate(self, f: Callable[[S, T], S], s: Optional[S]=None) -> FTuple[S]:
158        """
159        ##### Accumulate
160
161        Accumulate partial fold results in an FTuple with an optional starting value.
162
163        """
164        if s is None:
165            return FTuple(*accumulate(self, f))
166        else:
167            return FTuple(*accumulate(self, f, s))
Accumulate

Accumulate partial fold results in an FTuple with an optional starting value.

def flatMap( self, f: Callable[[~T], FTuple[~S]], type: grscheller.fp.iterables.FM = <FM.CONCAT: 1>) -> FTuple[~S]:
169    def flatMap(self, f: Callable[[T], FTuple[S]], type: FM=FM.CONCAT) -> FTuple[S]:
170        """
171        ##### Bind function to FTuple
172
173        Bind function `f` to the FTuple.
174
175        * type = CONCAT: sequentially concatenate iterables one after the other
176        * type = MERGE: merge iterables together until one is exhausted
177        * type = Exhaust: merge iterables together until all are exhausted
178
179        """
180        match type:
181            case FM.CONCAT:
182                return FTuple(*concat(*map(lambda x: iter(x), map(f, self))))
183            case FM.MERGE:
184                return FTuple(*merge(*map(lambda x: iter(x), map(f, self))))
185            case FM.EXHAUST:
186                return FTuple(*exhaust(*map(lambda x: iter(x), map(f, self))))
187            case '*':
188                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