grscheller.fp.lazy

Module fp.lazy - lazy function evaluations

Delayed function evaluations, if needed, usually in some inner scope. FP tools for "non-strict" function evaluations.

Non-strict delayed function evaluation:

  • class Lazy: Delay evaluation of function taking & returning single values
  • function lazy: Delay evaluation of a function taking more than one value
  1# Copyright 2023-2025 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"""### Module fp.lazy - lazy function evaluations
 16
 17Delayed function evaluations, if needed, usually in some inner scope. FP tools
 18for "non-strict" function evaluations.
 19
 20#### Non-strict delayed function evaluation:
 21
 22* class **Lazy:** Delay evaluation of function taking & returning single values
 23* function **lazy:** Delay evaluation of a function taking more than one value
 24
 25"""
 26from __future__ import annotations
 27
 28__all__ = [ 'Lazy', 'lazy' ]
 29
 30from collections.abc import Callable
 31from typing import Final
 32from .err_handling import MB, XOR
 33from .function import sequenced
 34
 35class Lazy[D, R]():
 36    """Delayed evaluation of a function mapping a value of type D
 37
 38    Class instance delays the executable of a function where `Lazy(f, arg)`
 39    constructs an object that can evaluate the Callable `f` with its argument
 40    at a later time.
 41
 42    * first argument `f` taking values of type `~D` to values of type `~R`
 43    * second argument `arg: ~D` is the argument to be passed to `f`
 44      * where the type `~D` is the `tuple` type of the argument types to `f`
 45    * function is evaluated when the eval method is called
 46    * result is cached unless `pure` is set to `False` in `__init__` method
 47
 48    Usually use case is to make a function "non-strict" by passing some of its
 49    arguments wrapped in Lazy instances.
 50    """
 51    __slots__ = '_f', '_d', '_result', '_pure'
 52
 53    def __init__(self, f: Callable[[D], R], d: D, pure: bool=True) -> None:
 54        self._f: Final[Callable[[D], R]] = f
 55        self._d: Final[D] = d
 56        self._pure: Final[bool] = pure
 57        self._result: XOR[R, MB[Exception]] = XOR(MB(), MB())
 58
 59    def __bool__(self) -> bool:
 60        return True if self._result else False
 61
 62    def is_evaluated(self) -> bool:
 63        return self._result != XOR(MB(), MB())
 64
 65    def is_exceptional(self) -> bool:
 66        if self.is_evaluated():
 67            return False if self._result else True
 68        else:
 69            return False
 70
 71    def is_pure(self) -> bool:
 72        return self._pure
 73
 74    def eval(self) -> bool:
 75        """Evaluate function with its argument.
 76
 77        * evaluate function
 78        * cache results or exceptions if `pure == True`
 79        * reevaluate if `pure == False`
 80
 81        """
 82        if not self.is_evaluated() or not self._pure:
 83            try:
 84                result = self._f(self._d)
 85            except Exception as exc:
 86                self._result = XOR(MB(), MB(exc))
 87                return False
 88            else:
 89                self._result = XOR(MB(result), MB())
 90                return True
 91        if self:
 92            return True
 93        else:
 94            return False
 95
 96    def result(self) -> MB[R]:
 97        if not self.is_evaluated():
 98            self.eval()
 99
100        if self._result:
101            return MB(self._result.getLeft())
102        else:
103            return MB()
104
105    def exception(self) -> MB[Exception]:
106        if not self.is_evaluated():
107            self.eval()
108        return self._result.getRight()
109
110def lazy[R, **P](f: Callable[P, R], *args: P.args, pure: bool=True) -> Lazy[tuple[P.args], R]:
111    """Delayed evaluation of a function with arbitrary positional arguments.
112
113    Function returning a delayed evaluation of a function of an arbitrary number
114    of positional arguments.
115
116    * first positional argument `f` takes a function
117    * next positional arguments are the arguments to be applied later to `f`
118      * `f` is evaluated when the `eval` method of the returned Lazy is called
119      * `f` is evaluated only once with results cached unless `pure` is `False`
120      * if `pure` is false, the arguments are reapplied to `f`
121        * useful for repeating side effects
122        * when arguments are or contain shared references
123
124    """
125    return Lazy(sequenced(f), args, pure=pure)
class Lazy(typing.Generic[D, R]):
 36class Lazy[D, R]():
 37    """Delayed evaluation of a function mapping a value of type D
 38
 39    Class instance delays the executable of a function where `Lazy(f, arg)`
 40    constructs an object that can evaluate the Callable `f` with its argument
 41    at a later time.
 42
 43    * first argument `f` taking values of type `~D` to values of type `~R`
 44    * second argument `arg: ~D` is the argument to be passed to `f`
 45      * where the type `~D` is the `tuple` type of the argument types to `f`
 46    * function is evaluated when the eval method is called
 47    * result is cached unless `pure` is set to `False` in `__init__` method
 48
 49    Usually use case is to make a function "non-strict" by passing some of its
 50    arguments wrapped in Lazy instances.
 51    """
 52    __slots__ = '_f', '_d', '_result', '_pure'
 53
 54    def __init__(self, f: Callable[[D], R], d: D, pure: bool=True) -> None:
 55        self._f: Final[Callable[[D], R]] = f
 56        self._d: Final[D] = d
 57        self._pure: Final[bool] = pure
 58        self._result: XOR[R, MB[Exception]] = XOR(MB(), MB())
 59
 60    def __bool__(self) -> bool:
 61        return True if self._result else False
 62
 63    def is_evaluated(self) -> bool:
 64        return self._result != XOR(MB(), MB())
 65
 66    def is_exceptional(self) -> bool:
 67        if self.is_evaluated():
 68            return False if self._result else True
 69        else:
 70            return False
 71
 72    def is_pure(self) -> bool:
 73        return self._pure
 74
 75    def eval(self) -> bool:
 76        """Evaluate function with its argument.
 77
 78        * evaluate function
 79        * cache results or exceptions if `pure == True`
 80        * reevaluate if `pure == False`
 81
 82        """
 83        if not self.is_evaluated() or not self._pure:
 84            try:
 85                result = self._f(self._d)
 86            except Exception as exc:
 87                self._result = XOR(MB(), MB(exc))
 88                return False
 89            else:
 90                self._result = XOR(MB(result), MB())
 91                return True
 92        if self:
 93            return True
 94        else:
 95            return False
 96
 97    def result(self) -> MB[R]:
 98        if not self.is_evaluated():
 99            self.eval()
100
101        if self._result:
102            return MB(self._result.getLeft())
103        else:
104            return MB()
105
106    def exception(self) -> MB[Exception]:
107        if not self.is_evaluated():
108            self.eval()
109        return self._result.getRight()

Delayed evaluation of a function mapping a value of type D

Class instance delays the executable of a function where Lazy(f, arg) constructs an object that can evaluate the Callable f with its argument at a later time.

  • first argument f taking values of type ~D to values of type ~R
  • second argument arg: ~D is the argument to be passed to f
    • where the type ~D is the tuple type of the argument types to f
  • function is evaluated when the eval method is called
  • result is cached unless pure is set to False in __init__ method

Usually use case is to make a function "non-strict" by passing some of its arguments wrapped in Lazy instances.

Lazy(f: 'Callable[[D], R]', d: 'D', pure: bool = True)
54    def __init__(self, f: Callable[[D], R], d: D, pure: bool=True) -> None:
55        self._f: Final[Callable[[D], R]] = f
56        self._d: Final[D] = d
57        self._pure: Final[bool] = pure
58        self._result: XOR[R, MB[Exception]] = XOR(MB(), MB())
def is_evaluated(self) -> bool:
63    def is_evaluated(self) -> bool:
64        return self._result != XOR(MB(), MB())
def is_exceptional(self) -> bool:
66    def is_exceptional(self) -> bool:
67        if self.is_evaluated():
68            return False if self._result else True
69        else:
70            return False
def is_pure(self) -> bool:
72    def is_pure(self) -> bool:
73        return self._pure
def eval(self) -> bool:
75    def eval(self) -> bool:
76        """Evaluate function with its argument.
77
78        * evaluate function
79        * cache results or exceptions if `pure == True`
80        * reevaluate if `pure == False`
81
82        """
83        if not self.is_evaluated() or not self._pure:
84            try:
85                result = self._f(self._d)
86            except Exception as exc:
87                self._result = XOR(MB(), MB(exc))
88                return False
89            else:
90                self._result = XOR(MB(result), MB())
91                return True
92        if self:
93            return True
94        else:
95            return False

Evaluate function with its argument.

  • evaluate function
  • cache results or exceptions if pure == True
  • reevaluate if pure == False
def result(self) -> 'MB[R]':
 97    def result(self) -> MB[R]:
 98        if not self.is_evaluated():
 99            self.eval()
100
101        if self._result:
102            return MB(self._result.getLeft())
103        else:
104            return MB()
def exception(self) -> grscheller.fp.err_handling.MB[Exception]:
106    def exception(self) -> MB[Exception]:
107        if not self.is_evaluated():
108            self.eval()
109        return self._result.getRight()
def lazy( f: 'Callable[P, R]', *args: 'P.args', pure: bool = True) -> 'Lazy[tuple[P.args], R]':
111def lazy[R, **P](f: Callable[P, R], *args: P.args, pure: bool=True) -> Lazy[tuple[P.args], R]:
112    """Delayed evaluation of a function with arbitrary positional arguments.
113
114    Function returning a delayed evaluation of a function of an arbitrary number
115    of positional arguments.
116
117    * first positional argument `f` takes a function
118    * next positional arguments are the arguments to be applied later to `f`
119      * `f` is evaluated when the `eval` method of the returned Lazy is called
120      * `f` is evaluated only once with results cached unless `pure` is `False`
121      * if `pure` is false, the arguments are reapplied to `f`
122        * useful for repeating side effects
123        * when arguments are or contain shared references
124
125    """
126    return Lazy(sequenced(f), args, pure=pure)

Delayed evaluation of a function with arbitrary positional arguments.

Function returning a delayed evaluation of a function of an arbitrary number of positional arguments.

  • first positional argument f takes a function
  • next positional arguments are the arguments to be applied later to f
    • f is evaluated when the eval method of the returned Lazy is called
    • f is evaluated only once with results cached unless pure is False
    • if pure is false, the arguments are reapplied to f
      • useful for repeating side effects
      • when arguments are or contain shared references