grscheller.datastructures.split_ends

Stack type Data Structures

SplitEnd

LIFO stacks which can safely share immutable data between themselves.


  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### Stack type Data Structures
 17
 18##### SplitEnd
 19
 20LIFO stacks which can safely share immutable data between themselves.
 21
 22---
 23
 24"""
 25
 26from __future__ import annotations
 27
 28from typing import Callable, cast, Generic, Iterator, Optional, overload, TypeVar
 29from grscheller.fp.iterables import FM, concat, exhaust, merge
 30from grscheller.fp.nada import Nada, nada
 31from .nodes import SL_Node as Node
 32
 33__all__ = ['SplitEnd']
 34
 35D = TypeVar('D')
 36S = TypeVar('S')
 37T = TypeVar('T')
 38
 39class SplitEnd(Generic[D, S]):
 40    """
 41    #### SplitEnd
 42    
 43    Class implementing a stack type data structures called a *split end*.
 44
 45    * each *split end* is a very simple stateful LIFO stack
 46    * contains a count of nodes & reference to first node of a linked list
 47    * different *split ends* can safely share the same *tail*
 48    * each *split end* sees itself as a singularly linked list
 49    * bush-like datastructures can be formed using multiple *split ends*
 50    * len() returns the number of elements on the stack
 51    * in a boolean context, return `True` if SplitEnd is not empty
 52
 53    """
 54    __slots__ = '_head', '_count', '_sentinel'
 55
 56    @overload
 57    def __init__(self, *ds: D, s: S) -> None:
 58        ...
 59    @overload
 60    def __init__(self, *ds: D, s: Nada) -> None:
 61        ...
 62    @overload
 63    def __init__(self, *ds: D) -> None:
 64        ...
 65    def __init__(self, *ds: D, s: S|Nada=nada) -> None:
 66        self._head: Optional[Node[D]] = None
 67        self._count: int = 0
 68        self._sentinel = s
 69        for d in ds:
 70            node: Node[D] = Node(d, self._head)
 71            self._head = node
 72            self._count += 1
 73
 74    def __iter__(self) -> Iterator[D]:
 75        node = self._head
 76        while node:
 77            yield node._data
 78            node = node._next
 79
 80    def reverse(self) -> SplitEnd[D, S]:
 81        """
 82        ##### Return a Reversed SplitEnd
 83
 84        Return shallow reversed copy of a SplitEnd.
 85
 86        * Returns a new Stack object with shallow copied new data
 87        * creates all new nodes
 88        * O(1) space & time complexity
 89
 90        """
 91        return SplitEnd(*self, s=self._sentinel)
 92
 93    def __reversed__(self) -> Iterator[D]:
 94        return iter(self.reverse())
 95
 96    def __repr__(self) -> str:
 97        if self._sentinel is nada:
 98            return 'SplitEnd(' + ', '.join(map(repr, reversed(self))) + ')'
 99        elif self:
100            return ('SplitEnd('
101                    + ', '.join(map(repr, reversed(self)))
102                    + ', s=' + repr(self._sentinel) + ')')
103        else:
104            return ('SplitEnd('
105                    + 's=' + repr(self._sentinel) + ')')
106
107
108    def __str__(self) -> str:
109        """Display the data in the Stack, left to right."""
110        if self._sentinel is nada:
111            return ('>< '
112                    + ' -> '.join(map(str, self))
113                    + ' ||')
114        else:
115            return ('>< '
116                    + ' -> '.join(map(str, self))
117                    + ' |' + repr(self._sentinel) + '|')
118
119    def __bool__(self) -> bool:
120        return self._count > 0
121
122    def __len__(self) -> int:
123        return self._count
124
125    def __eq__(self, other: object) -> bool:
126        if not isinstance(other, type(self)):
127            return False
128
129        if self._count != other._count:
130            return False
131        if self._sentinel is not other._sentinel:
132            if self._sentinel != other._sentinel:
133                return False
134
135        left = self._head
136        right = other._head
137        nn = self._count
138        while nn > 0:
139            if left is right:
140                return True
141            if left is None or right is None:
142                return True
143            if left._data != right._data:
144                return False
145            left = left._next
146            right = right._next
147            nn -= 1
148        return True
149
150    def copy(self) -> SplitEnd[D, S]:
151        """
152        ##### Shallow Copy
153
154        Return a swallow copy of the SplitEnd in O(1) space & time complexity.
155
156        """
157        stack: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
158        stack._head, stack._count = self._head, self._count
159        return stack
160
161    def push(self, *ds: D) -> None:
162        """
163        ##### Push Data
164
165        Push data onto top of the SplitEnd.
166
167        * ignore "non-existent" Nothing() values pushed on the SplitEnd
168
169        """
170        for d in ds:
171            if d is not nada:
172                node = Node(d, self._head)
173                self._head, self._count = node, self._count+1
174
175    @overload
176    def pop(self, default: D) -> D|S:
177        ...
178    @overload
179    def pop(self) -> D|S:
180        ...
181    def pop(self, default: D|Nada=nada) -> D|S|Nada:
182        """
183        ##### Pop Data
184
185        Pop data off of the top of the SplitEnd.
186
187        * if empty, return a default value
188        * if empty and a default value not given, return the sentinel value
189
190        """
191        if self._head is None:
192            if default is nada:
193                return self._sentinel
194            else:
195                return default
196        else:
197            data = self._head._data
198            self._head, self._count = self._head._next, self._count-1
199            return data
200
201    @overload
202    def peak(self, default: D) -> D:
203        ...
204    @overload
205    def peak(self) -> D|S:
206        ...
207    def peak(self, default: D|Nada=nada) -> D|S|Nada:
208        """
209        ##### Peak at top of SplitEnd
210
211        Returns the data at the top of the SplitEnd.
212
213        * does not consume the data
214        * if empty, data does not exist, so in that case return default
215        * if empty and no default given, return nothing: Nothing
216
217        """
218        if self._head is None:
219            return default
220        return self._head._data
221
222    @overload
223    def head(self, default: D|S) -> D|S:
224        ...
225    @overload
226    def head(self) -> D|S:
227        ...
228    def head(self, default: D|S|Nada=nada) -> D|S|Nada:
229        """
230        ##### Head of SplitEnd
231
232        Returns the data at the top of the SplitEnd.
233
234        * does not consume the data
235        * for an empty SplitEnd, head does not exist, so return default
236        * otherwise return the sentinel value
237        * the sentinel value cannot be overridden by nada
238          * of course self._sentinel can always be set to nada
239
240        """
241        if self._head is None:
242            if default is nada:
243                return self._sentinel
244            else:
245                return default
246        return self._head._data
247
248    @overload
249    def tail(self, default: S) -> SplitEnd[D, S]|S:
250        ...
251    @overload
252    def tail(self) -> SplitEnd[D, S]|S:
253        ...
254    def tail(self, default: S|Nada=nada) -> SplitEnd[D, S]|S|Nada:
255        """
256        ##### Tail of SplitEnd
257        
258        Returns the tail of the SplitEnd if it exists, otherwise returns the
259        sentinel value, or a default value of the same type as the sentinel
260        value.
261
262        * optional default needs to be of the same type as the sentinel value
263          * example:
264            * sentinel: tuple(int) = (0,)
265            * default: tuple(int) = (42,)
266          * decided not to let default: SplitEnd[D, S] as a return option
267            * made end code more confusing to reason about
268              * not worth cognitive overload when used in client code
269              * tended to hide the occurrence of an unusual event occurring
270        * the sentinel value cannot be overridden by nada
271          * of course self._sentinel can always be set to nada
272
273        """
274        if self._head:
275            se: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
276            se._head = self._head._next
277            se._count = self._count - 1
278            return se
279        else:
280            return default
281
282    @overload
283    def cons(self, d: D) -> SplitEnd[D, S]: 
284        ...
285    @overload
286    def cons(self, d: Nada) -> Nada: 
287        ...
288    def cons(self, d: D|Nada) -> SplitEnd[D, S]|Nada:
289        """
290        ##### Cons SplitEnd with a Head
291
292        Return a new SplitEnd with data as head and self as tail.
293
294        Constructing a SplitEnd using a non-existent value as head results in
295        a non-existent SplitEnd. In that case, return sentinel: _S.
296
297        """
298        if d is nada:
299            return nada
300        else:
301            stack: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
302            stack._head = Node(cast(D, d), self._head)
303            stack._count = self._count + 1
304            return stack
305
306    def fold(self, f:Callable[[D, D], D]) -> Optional[D]:
307        """
308        ##### Reduce with `f`
309
310        * returns a value of the of type _T if self is not empty
311        * returns None if self is empty
312        * folds in natural LIFO Order
313        * TODO: consolidate fold & fold1
314
315        """
316        node: Optional[Node[D]] = self._head
317        if not node:
318            return None
319        acc: D = node._data
320        while node:
321            if (node := node._next) is None:
322                break
323            acc = f(acc, node._data)
324        return acc
325
326    def fold1(self, f:Callable[[T, D], T], s: T) -> T:
327        """Reduce with f.
328
329        * returns a value of type ~T
330        * type ~T can be same type as ~D
331        * folds in natural LIFO Order
332        * TODO: consolidate fold & fold1
333
334        """
335        node: Optional[Node[D]] = self._head
336        if not node:
337            return s
338        acc: T = s
339        while node:
340            acc = f(acc, node._data)
341            node = node._next
342        return acc
343
344    def flatMap(self, f: Callable[[D], SplitEnd[T, S]], type: FM=FM.CONCAT) -> SplitEnd[T, S]:
345        """
346        ##### Bind function to SplitEnd
347
348        Bind function `f` to the SplitEnd.
349
350        * type = CONCAT: sequentially concatenate iterables one after the other
351        * type = MERGE: merge iterables together until one is exhausted
352        * type = Exhaust: merge iterables together until all are exhausted
353
354        """
355        match type:
356            case FM.CONCAT:
357                return SplitEnd(*concat(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
358            case FM.MERGE:
359                return SplitEnd(*merge(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
360            case FM.EXHAUST:
361                return SplitEnd(*exhaust(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
362            case '*':
363                raise ValueError('Unknown FM type')
364
365    def map(self, f: Callable[[D], T]) -> SplitEnd[T, S]:
366        """
367        ##### Map `f` over the SplitEnd
368
369        Maps a function (or callable object) over the values on the SplitEnd Stack.
370
371        * TODO: Redo in "natural" order?
372        * Returns a new Stack object with shallow copied new data
373        * O(n) complexity
374
375        """
376        return self.flatMap(lambda a: SplitEnd(f(a)))
class SplitEnd(typing.Generic[~D, ~S]):
 40class SplitEnd(Generic[D, S]):
 41    """
 42    #### SplitEnd
 43    
 44    Class implementing a stack type data structures called a *split end*.
 45
 46    * each *split end* is a very simple stateful LIFO stack
 47    * contains a count of nodes & reference to first node of a linked list
 48    * different *split ends* can safely share the same *tail*
 49    * each *split end* sees itself as a singularly linked list
 50    * bush-like datastructures can be formed using multiple *split ends*
 51    * len() returns the number of elements on the stack
 52    * in a boolean context, return `True` if SplitEnd is not empty
 53
 54    """
 55    __slots__ = '_head', '_count', '_sentinel'
 56
 57    @overload
 58    def __init__(self, *ds: D, s: S) -> None:
 59        ...
 60    @overload
 61    def __init__(self, *ds: D, s: Nada) -> None:
 62        ...
 63    @overload
 64    def __init__(self, *ds: D) -> None:
 65        ...
 66    def __init__(self, *ds: D, s: S|Nada=nada) -> None:
 67        self._head: Optional[Node[D]] = None
 68        self._count: int = 0
 69        self._sentinel = s
 70        for d in ds:
 71            node: Node[D] = Node(d, self._head)
 72            self._head = node
 73            self._count += 1
 74
 75    def __iter__(self) -> Iterator[D]:
 76        node = self._head
 77        while node:
 78            yield node._data
 79            node = node._next
 80
 81    def reverse(self) -> SplitEnd[D, S]:
 82        """
 83        ##### Return a Reversed SplitEnd
 84
 85        Return shallow reversed copy of a SplitEnd.
 86
 87        * Returns a new Stack object with shallow copied new data
 88        * creates all new nodes
 89        * O(1) space & time complexity
 90
 91        """
 92        return SplitEnd(*self, s=self._sentinel)
 93
 94    def __reversed__(self) -> Iterator[D]:
 95        return iter(self.reverse())
 96
 97    def __repr__(self) -> str:
 98        if self._sentinel is nada:
 99            return 'SplitEnd(' + ', '.join(map(repr, reversed(self))) + ')'
100        elif self:
101            return ('SplitEnd('
102                    + ', '.join(map(repr, reversed(self)))
103                    + ', s=' + repr(self._sentinel) + ')')
104        else:
105            return ('SplitEnd('
106                    + 's=' + repr(self._sentinel) + ')')
107
108
109    def __str__(self) -> str:
110        """Display the data in the Stack, left to right."""
111        if self._sentinel is nada:
112            return ('>< '
113                    + ' -> '.join(map(str, self))
114                    + ' ||')
115        else:
116            return ('>< '
117                    + ' -> '.join(map(str, self))
118                    + ' |' + repr(self._sentinel) + '|')
119
120    def __bool__(self) -> bool:
121        return self._count > 0
122
123    def __len__(self) -> int:
124        return self._count
125
126    def __eq__(self, other: object) -> bool:
127        if not isinstance(other, type(self)):
128            return False
129
130        if self._count != other._count:
131            return False
132        if self._sentinel is not other._sentinel:
133            if self._sentinel != other._sentinel:
134                return False
135
136        left = self._head
137        right = other._head
138        nn = self._count
139        while nn > 0:
140            if left is right:
141                return True
142            if left is None or right is None:
143                return True
144            if left._data != right._data:
145                return False
146            left = left._next
147            right = right._next
148            nn -= 1
149        return True
150
151    def copy(self) -> SplitEnd[D, S]:
152        """
153        ##### Shallow Copy
154
155        Return a swallow copy of the SplitEnd in O(1) space & time complexity.
156
157        """
158        stack: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
159        stack._head, stack._count = self._head, self._count
160        return stack
161
162    def push(self, *ds: D) -> None:
163        """
164        ##### Push Data
165
166        Push data onto top of the SplitEnd.
167
168        * ignore "non-existent" Nothing() values pushed on the SplitEnd
169
170        """
171        for d in ds:
172            if d is not nada:
173                node = Node(d, self._head)
174                self._head, self._count = node, self._count+1
175
176    @overload
177    def pop(self, default: D) -> D|S:
178        ...
179    @overload
180    def pop(self) -> D|S:
181        ...
182    def pop(self, default: D|Nada=nada) -> D|S|Nada:
183        """
184        ##### Pop Data
185
186        Pop data off of the top of the SplitEnd.
187
188        * if empty, return a default value
189        * if empty and a default value not given, return the sentinel value
190
191        """
192        if self._head is None:
193            if default is nada:
194                return self._sentinel
195            else:
196                return default
197        else:
198            data = self._head._data
199            self._head, self._count = self._head._next, self._count-1
200            return data
201
202    @overload
203    def peak(self, default: D) -> D:
204        ...
205    @overload
206    def peak(self) -> D|S:
207        ...
208    def peak(self, default: D|Nada=nada) -> D|S|Nada:
209        """
210        ##### Peak at top of SplitEnd
211
212        Returns the data at the top of the SplitEnd.
213
214        * does not consume the data
215        * if empty, data does not exist, so in that case return default
216        * if empty and no default given, return nothing: Nothing
217
218        """
219        if self._head is None:
220            return default
221        return self._head._data
222
223    @overload
224    def head(self, default: D|S) -> D|S:
225        ...
226    @overload
227    def head(self) -> D|S:
228        ...
229    def head(self, default: D|S|Nada=nada) -> D|S|Nada:
230        """
231        ##### Head of SplitEnd
232
233        Returns the data at the top of the SplitEnd.
234
235        * does not consume the data
236        * for an empty SplitEnd, head does not exist, so return default
237        * otherwise return the sentinel value
238        * the sentinel value cannot be overridden by nada
239          * of course self._sentinel can always be set to nada
240
241        """
242        if self._head is None:
243            if default is nada:
244                return self._sentinel
245            else:
246                return default
247        return self._head._data
248
249    @overload
250    def tail(self, default: S) -> SplitEnd[D, S]|S:
251        ...
252    @overload
253    def tail(self) -> SplitEnd[D, S]|S:
254        ...
255    def tail(self, default: S|Nada=nada) -> SplitEnd[D, S]|S|Nada:
256        """
257        ##### Tail of SplitEnd
258        
259        Returns the tail of the SplitEnd if it exists, otherwise returns the
260        sentinel value, or a default value of the same type as the sentinel
261        value.
262
263        * optional default needs to be of the same type as the sentinel value
264          * example:
265            * sentinel: tuple(int) = (0,)
266            * default: tuple(int) = (42,)
267          * decided not to let default: SplitEnd[D, S] as a return option
268            * made end code more confusing to reason about
269              * not worth cognitive overload when used in client code
270              * tended to hide the occurrence of an unusual event occurring
271        * the sentinel value cannot be overridden by nada
272          * of course self._sentinel can always be set to nada
273
274        """
275        if self._head:
276            se: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
277            se._head = self._head._next
278            se._count = self._count - 1
279            return se
280        else:
281            return default
282
283    @overload
284    def cons(self, d: D) -> SplitEnd[D, S]: 
285        ...
286    @overload
287    def cons(self, d: Nada) -> Nada: 
288        ...
289    def cons(self, d: D|Nada) -> SplitEnd[D, S]|Nada:
290        """
291        ##### Cons SplitEnd with a Head
292
293        Return a new SplitEnd with data as head and self as tail.
294
295        Constructing a SplitEnd using a non-existent value as head results in
296        a non-existent SplitEnd. In that case, return sentinel: _S.
297
298        """
299        if d is nada:
300            return nada
301        else:
302            stack: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
303            stack._head = Node(cast(D, d), self._head)
304            stack._count = self._count + 1
305            return stack
306
307    def fold(self, f:Callable[[D, D], D]) -> Optional[D]:
308        """
309        ##### Reduce with `f`
310
311        * returns a value of the of type _T if self is not empty
312        * returns None if self is empty
313        * folds in natural LIFO Order
314        * TODO: consolidate fold & fold1
315
316        """
317        node: Optional[Node[D]] = self._head
318        if not node:
319            return None
320        acc: D = node._data
321        while node:
322            if (node := node._next) is None:
323                break
324            acc = f(acc, node._data)
325        return acc
326
327    def fold1(self, f:Callable[[T, D], T], s: T) -> T:
328        """Reduce with f.
329
330        * returns a value of type ~T
331        * type ~T can be same type as ~D
332        * folds in natural LIFO Order
333        * TODO: consolidate fold & fold1
334
335        """
336        node: Optional[Node[D]] = self._head
337        if not node:
338            return s
339        acc: T = s
340        while node:
341            acc = f(acc, node._data)
342            node = node._next
343        return acc
344
345    def flatMap(self, f: Callable[[D], SplitEnd[T, S]], type: FM=FM.CONCAT) -> SplitEnd[T, S]:
346        """
347        ##### Bind function to SplitEnd
348
349        Bind function `f` to the SplitEnd.
350
351        * type = CONCAT: sequentially concatenate iterables one after the other
352        * type = MERGE: merge iterables together until one is exhausted
353        * type = Exhaust: merge iterables together until all are exhausted
354
355        """
356        match type:
357            case FM.CONCAT:
358                return SplitEnd(*concat(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
359            case FM.MERGE:
360                return SplitEnd(*merge(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
361            case FM.EXHAUST:
362                return SplitEnd(*exhaust(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
363            case '*':
364                raise ValueError('Unknown FM type')
365
366    def map(self, f: Callable[[D], T]) -> SplitEnd[T, S]:
367        """
368        ##### Map `f` over the SplitEnd
369
370        Maps a function (or callable object) over the values on the SplitEnd Stack.
371
372        * TODO: Redo in "natural" order?
373        * Returns a new Stack object with shallow copied new data
374        * O(n) complexity
375
376        """
377        return self.flatMap(lambda a: SplitEnd(f(a)))

SplitEnd

Class implementing a stack type data structures called a split end.

  • each split end is a very simple stateful LIFO stack
  • contains a count of nodes & reference to first node of a linked list
  • different split ends can safely share the same tail
  • each split end sees itself as a singularly linked list
  • bush-like datastructures can be formed using multiple split ends
  • len() returns the number of elements on the stack
  • in a boolean context, return True if SplitEnd is not empty
SplitEnd(*ds: ~D, s: Union[~S, grscheller.fp.nada.Nada] = nada)
66    def __init__(self, *ds: D, s: S|Nada=nada) -> None:
67        self._head: Optional[Node[D]] = None
68        self._count: int = 0
69        self._sentinel = s
70        for d in ds:
71            node: Node[D] = Node(d, self._head)
72            self._head = node
73            self._count += 1
def reverse(self) -> SplitEnd[~D, ~S]:
81    def reverse(self) -> SplitEnd[D, S]:
82        """
83        ##### Return a Reversed SplitEnd
84
85        Return shallow reversed copy of a SplitEnd.
86
87        * Returns a new Stack object with shallow copied new data
88        * creates all new nodes
89        * O(1) space & time complexity
90
91        """
92        return SplitEnd(*self, s=self._sentinel)
Return a Reversed SplitEnd

Return shallow reversed copy of a SplitEnd.

  • Returns a new Stack object with shallow copied new data
  • creates all new nodes
  • O(1) space & time complexity
def copy(self) -> SplitEnd[~D, ~S]:
151    def copy(self) -> SplitEnd[D, S]:
152        """
153        ##### Shallow Copy
154
155        Return a swallow copy of the SplitEnd in O(1) space & time complexity.
156
157        """
158        stack: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
159        stack._head, stack._count = self._head, self._count
160        return stack
Shallow Copy

Return a swallow copy of the SplitEnd in O(1) space & time complexity.

def push(self, *ds: ~D) -> None:
162    def push(self, *ds: D) -> None:
163        """
164        ##### Push Data
165
166        Push data onto top of the SplitEnd.
167
168        * ignore "non-existent" Nothing() values pushed on the SplitEnd
169
170        """
171        for d in ds:
172            if d is not nada:
173                node = Node(d, self._head)
174                self._head, self._count = node, self._count+1
Push Data

Push data onto top of the SplitEnd.

  • ignore "non-existent" Nothing() values pushed on the SplitEnd
def pop( self, default: Union[~D, grscheller.fp.nada.Nada] = nada) -> Union[~D, ~S, grscheller.fp.nada.Nada]:
182    def pop(self, default: D|Nada=nada) -> D|S|Nada:
183        """
184        ##### Pop Data
185
186        Pop data off of the top of the SplitEnd.
187
188        * if empty, return a default value
189        * if empty and a default value not given, return the sentinel value
190
191        """
192        if self._head is None:
193            if default is nada:
194                return self._sentinel
195            else:
196                return default
197        else:
198            data = self._head._data
199            self._head, self._count = self._head._next, self._count-1
200            return data
Pop Data

Pop data off of the top of the SplitEnd.

  • if empty, return a default value
  • if empty and a default value not given, return the sentinel value
def peak( self, default: Union[~D, grscheller.fp.nada.Nada] = nada) -> Union[~D, ~S, grscheller.fp.nada.Nada]:
208    def peak(self, default: D|Nada=nada) -> D|S|Nada:
209        """
210        ##### Peak at top of SplitEnd
211
212        Returns the data at the top of the SplitEnd.
213
214        * does not consume the data
215        * if empty, data does not exist, so in that case return default
216        * if empty and no default given, return nothing: Nothing
217
218        """
219        if self._head is None:
220            return default
221        return self._head._data
Peak at top of SplitEnd

Returns the data at the top of the SplitEnd.

  • does not consume the data
  • if empty, data does not exist, so in that case return default
  • if empty and no default given, return nothing: Nothing
def head( self, default: Union[~D, ~S, grscheller.fp.nada.Nada] = nada) -> Union[~D, ~S, grscheller.fp.nada.Nada]:
229    def head(self, default: D|S|Nada=nada) -> D|S|Nada:
230        """
231        ##### Head of SplitEnd
232
233        Returns the data at the top of the SplitEnd.
234
235        * does not consume the data
236        * for an empty SplitEnd, head does not exist, so return default
237        * otherwise return the sentinel value
238        * the sentinel value cannot be overridden by nada
239          * of course self._sentinel can always be set to nada
240
241        """
242        if self._head is None:
243            if default is nada:
244                return self._sentinel
245            else:
246                return default
247        return self._head._data
Head of SplitEnd

Returns the data at the top of the SplitEnd.

  • does not consume the data
  • for an empty SplitEnd, head does not exist, so return default
  • otherwise return the sentinel value
  • the sentinel value cannot be overridden by nada
    • of course self._sentinel can always be set to nada
def tail( self, default: Union[~S, grscheller.fp.nada.Nada] = nada) -> Union[SplitEnd[~D, ~S], ~S, grscheller.fp.nada.Nada]:
255    def tail(self, default: S|Nada=nada) -> SplitEnd[D, S]|S|Nada:
256        """
257        ##### Tail of SplitEnd
258        
259        Returns the tail of the SplitEnd if it exists, otherwise returns the
260        sentinel value, or a default value of the same type as the sentinel
261        value.
262
263        * optional default needs to be of the same type as the sentinel value
264          * example:
265            * sentinel: tuple(int) = (0,)
266            * default: tuple(int) = (42,)
267          * decided not to let default: SplitEnd[D, S] as a return option
268            * made end code more confusing to reason about
269              * not worth cognitive overload when used in client code
270              * tended to hide the occurrence of an unusual event occurring
271        * the sentinel value cannot be overridden by nada
272          * of course self._sentinel can always be set to nada
273
274        """
275        if self._head:
276            se: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
277            se._head = self._head._next
278            se._count = self._count - 1
279            return se
280        else:
281            return default
Tail of SplitEnd

Returns the tail of the SplitEnd if it exists, otherwise returns the sentinel value, or a default value of the same type as the sentinel value.

  • optional default needs to be of the same type as the sentinel value
    • example:
      • sentinel: tuple(int) = (0,)
      • default: tuple(int) = (42,)
    • decided not to let default: SplitEnd[D, S] as a return option
      • made end code more confusing to reason about
        • not worth cognitive overload when used in client code
        • tended to hide the occurrence of an unusual event occurring
  • the sentinel value cannot be overridden by nada
    • of course self._sentinel can always be set to nada
def cons( self, d: Union[~D, grscheller.fp.nada.Nada]) -> Union[SplitEnd[~D, ~S], grscheller.fp.nada.Nada]:
289    def cons(self, d: D|Nada) -> SplitEnd[D, S]|Nada:
290        """
291        ##### Cons SplitEnd with a Head
292
293        Return a new SplitEnd with data as head and self as tail.
294
295        Constructing a SplitEnd using a non-existent value as head results in
296        a non-existent SplitEnd. In that case, return sentinel: _S.
297
298        """
299        if d is nada:
300            return nada
301        else:
302            stack: SplitEnd[D, S] = SplitEnd(s=self._sentinel)
303            stack._head = Node(cast(D, d), self._head)
304            stack._count = self._count + 1
305            return stack
Cons SplitEnd with a Head

Return a new SplitEnd with data as head and self as tail.

Constructing a SplitEnd using a non-existent value as head results in a non-existent SplitEnd. In that case, return sentinel: _S.

def fold(self, f: Callable[[~D, ~D], ~D]) -> Optional[~D]:
307    def fold(self, f:Callable[[D, D], D]) -> Optional[D]:
308        """
309        ##### Reduce with `f`
310
311        * returns a value of the of type _T if self is not empty
312        * returns None if self is empty
313        * folds in natural LIFO Order
314        * TODO: consolidate fold & fold1
315
316        """
317        node: Optional[Node[D]] = self._head
318        if not node:
319            return None
320        acc: D = node._data
321        while node:
322            if (node := node._next) is None:
323                break
324            acc = f(acc, node._data)
325        return acc
Reduce with f
  • returns a value of the of type _T if self is not empty
  • returns None if self is empty
  • folds in natural LIFO Order
  • TODO: consolidate fold & fold1
def fold1(self, f: Callable[[~T, ~D], ~T], s: ~T) -> ~T:
327    def fold1(self, f:Callable[[T, D], T], s: T) -> T:
328        """Reduce with f.
329
330        * returns a value of type ~T
331        * type ~T can be same type as ~D
332        * folds in natural LIFO Order
333        * TODO: consolidate fold & fold1
334
335        """
336        node: Optional[Node[D]] = self._head
337        if not node:
338            return s
339        acc: T = s
340        while node:
341            acc = f(acc, node._data)
342            node = node._next
343        return acc

Reduce with f.

  • returns a value of type ~T
  • type ~T can be same type as ~D
  • folds in natural LIFO Order
  • TODO: consolidate fold & fold1
def flatMap( self, f: Callable[[~D], SplitEnd[~T, ~S]], type: grscheller.fp.iterables.FM = <FM.CONCAT: 1>) -> SplitEnd[~T, ~S]:
345    def flatMap(self, f: Callable[[D], SplitEnd[T, S]], type: FM=FM.CONCAT) -> SplitEnd[T, S]:
346        """
347        ##### Bind function to SplitEnd
348
349        Bind function `f` to the SplitEnd.
350
351        * type = CONCAT: sequentially concatenate iterables one after the other
352        * type = MERGE: merge iterables together until one is exhausted
353        * type = Exhaust: merge iterables together until all are exhausted
354
355        """
356        match type:
357            case FM.CONCAT:
358                return SplitEnd(*concat(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
359            case FM.MERGE:
360                return SplitEnd(*merge(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
361            case FM.EXHAUST:
362                return SplitEnd(*exhaust(*map(lambda x: iter(x), map(f, self))), s=self._sentinel)
363            case '*':
364                raise ValueError('Unknown FM type')
Bind function to SplitEnd

Bind function f to the SplitEnd.

  • 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 map( self, f: Callable[[~D], ~T]) -> SplitEnd[~T, ~S]:
366    def map(self, f: Callable[[D], T]) -> SplitEnd[T, S]:
367        """
368        ##### Map `f` over the SplitEnd
369
370        Maps a function (or callable object) over the values on the SplitEnd Stack.
371
372        * TODO: Redo in "natural" order?
373        * Returns a new Stack object with shallow copied new data
374        * O(n) complexity
375
376        """
377        return self.flatMap(lambda a: SplitEnd(f(a)))
Map f over the SplitEnd

Maps a function (or callable object) over the values on the SplitEnd Stack.

  • TODO: Redo in "natural" order?
  • Returns a new Stack object with shallow copied new data
  • O(n) complexity