Source code for deluxe.functional

# Copyright (c) 2024 - Gilles Coissac
# This file is part of standard-deluxe library.
#
# standard-deluxe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# standard-deluxe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with standard-deluxe. If not, see <https://www.gnu.org/licenses/>
#
"""Functional programming utilities and monadic types.

This module provides functional programming constructs including monads,
lazy evaluation, and caching utilities. It implements the monad pattern
for composing computations in a functional style, with support for
chaining operations and deferred evaluation.

Examples:
    Using :class:`Maybe` for optional value handling::

        from deluxe.functional import Maybe

        def parse_int(value: str) -> Maybe[int]:
            try:
                return Maybe.pure(int(value))
            except ValueError:
                return Maybe()

        result = parse_int("42")
        match result:
            case Maybe(value):
                print(f"Parsed: {value}")
            case _:
                print("Invalid input")

    Using :class:`Lazy` for deferred computation::

        from deluxe.functional import Lazy

        def expensive_computation() -> int:
            print("Computing...")
            return 42

        # Computation not executed yet
        lazy_value = Lazy(expensive_computation, int)

        # Computation executed here
        result = lazy_value.unwrap()  # Prints "Computing..."

    Using :class:`MaybeCallable` with enumerations::

        from enum import Enum, member
        from deluxe.functional import MaybeCallable

        class Key(MaybeCallable[bytes]):
            def __call__(self, *args: bytes) -> bytes:
                return self._callable_(*args)

        class KeyStroke(Key[bytes], Enum):
            @member
            @staticmethod
            def Ctrl(key: bytes) -> str:
                return f"C-{key.decode()}"

            Space = b"Space"

See Also:
    - :mod:`deluxe.types`: Additional type utilities.
    - :mod:`deluxe.enums`: Enhanced enumeration support.
"""

from __future__ import annotations

from types import NoneType
from typing import (
    TYPE_CHECKING,
    Any,
    ClassVar,
    Generic,
    Never,
    Protocol,
    Self,
    TypeVar,
    cast,
    no_type_check,
    overload,
    runtime_checkable,
)

from deluxe.types import Unset


if TYPE_CHECKING:
    from collections.abc import Callable


__all__ = ("Lazy", "Maybe", "MaybeCallable", "Monad", "cached_property")


_T_co = TypeVar("_T_co", covariant=True)


# NOTE: based on the code from Python 3.14: https://github.com/python/cpython/blob/
# 5507eff19c757a908a2ff29dfe423e35595fda00/Lib/functools.py#L1089-L1138
# Copyright (C) 2006 Python Software Foundation.
# vendored under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
#
# Prior to Python 3.12 cached_property used threading.Lock,
# which makes it very slow.
[docs] class cached_property(Generic[_T_co]): # noqa: N801 """Similar to property(), with the addition of caching. Transform a method of a class into a property whose value is computed once and then cached as a normal attribute for the life of the instance. Useful for expensive computed properties of instances that are otherwise effectively immutable. Keys Differences from `functools.cached_property`: * support pure python class as well as `mypyc` compiled Native and Non-Native extension class. * prevent reset or deletion of the property on the instance * allow usage in class with or without writable __dict__ (eg., class with __slots__) """ def __init__(self, func: Callable[[Any], _T_co]) -> None: self.__cache__: dict[int, Any] = {} self.func: Callable[[Any], _T_co] = func self.__doc__: str = func.__doc__ or "" self.attrname: str self.__objclass__: type[Any] self.__module__: str def __set_name__(self, owner: type[Any], name: str) -> None: # NOTE: with mypyc Native extension class, __set_name__ # will never be called, but as the implicit name of # the property is the func.__name__, we could set it latter # in the __get__ method body if not hasattr(self, "attrname"): self.attrname = name elif name != self.attrname: msg = ( "Cannot assign the same cached_property to two different names " f"({self.attrname!r} and {name!r})." ) raise TypeError(msg) self.__objclass__ = owner self.__module__ = owner.__module__ @overload def __get__(self, instance: None, owner: type[Any]) -> Self: ... @overload def __get__(self, instance: object, owner: type[Any]) -> _T_co: ... def __get__(self, instance: object | None, owner: type[Any]) -> _T_co | Self: if instance is None: return self if not hasattr(self, "attrname"): self.__set_name__(owner, self.func.__name__) key: int = id(instance) if (val := self.__cache__.get(key, Unset)) is Unset: val = self.func(instance) self.__cache__[key] = val return val def __set__(self, instance: Any, value: object) -> Never: msg = f"property of '{object.__class__.__name__}' object has no setter" raise AttributeError(msg) def __delete__(self, instance: Any) -> Never: msg = f"property of '{object.__class__.__name__}' object has no deleter" raise AttributeError(msg)
_T = TypeVar("_T") _U = TypeVar("_U")
[docs] @runtime_checkable class Monad(Protocol[_T]): # pragma: no cover """Protocol defining the interface for monadic types. A monad is a design pattern from functional programming that provides a way to structure computations. It defines a standard interface for values that can be "wrapped" in a context, enabling consistent handling of computations that might involve side effects, error handling, or delayed evaluation. The protocol requires implementations to provide methods for wrapping values in the monadic context (:meth:`pure`), transforming wrapped values (:meth:`map`), and chaining computations that produce monadic results (:meth:`bind`). Examples: Basic usage with the :class:`Lazy` implementation:: from deluxe.functional import Lazy # Wrap a value using pure lazy_value = Lazy.pure(42) # Transform the value using map lazy_doubled = lazy_value.map(lambda x: x * 2) print(lazy_doubled.unwrap()) # 84 # Chain computations using bind def to_lazy(x: int) -> Lazy[str]: return Lazy.pure(str(x)) lazy_string = lazy_value.bind(to_lazy) print(lazy_string.unwrap()) # "42" Protocol Methods: - :meth:`pure`: Wrap a plain value in the monadic context. - :meth:`map`: Apply a function to the wrapped value (functorial map). - :meth:`bind`: Chain a function that returns cast(_OST, a monadic value.) - :meth:`join`: Flatten a monad of monads into a single monad. - :meth:`unwrap`: Extract the wrapped value from the monadic context. - :meth:`__call__`: Alias for :meth:`unwrap`, allowing monads to be called. See Also: - :class:`Lazy`: A concrete implementation of the :class:`Monad` protocol for lazy evaluation. - :class:`deluxe.environ.evar` """ # __hash__: ClassVar[None] = None
[docs] @classmethod def pure(cls, value: _T) -> Self: """Wrap a plain value into the monadic context. This is a class method that creates a new monadic instance containing the given plain value. This is the primary way to enter the monadic context from a non-monadic value. Args: value: The plain value to wrap in the monadic context. Returns: A monadic instance of the same type as ``cls`` containing the wrapped ``value``. """ ...
[docs] def map(self, func: Callable[[_T], _U], *args: Any, **kwds: Any) -> Monad[_U]: """Apply a function to the wrapped value (functorial map). This method transforms the value inside the monadic context by applying the given function. It preserves the monadic structure, returning a new monad containing the transformed value. The ``map`` method implements the functor pattern, which is typically derived from :meth:`bind` and :meth:`pure` in functional programming theory. Args: func: A callable that receives the wrapped value and returns a transformed value of a (potentially different) type. *args: Additional positional arguments to pass to ``func``. **kwds: Additional keyword arguments to pass to ``func``. Returns: A monadic instance wrapping the result of ``func``, whose type matches the return type of ``func``. """ ...
[docs] def bind(self, func: Callable[[_T], Monad[_U]], *args: Any, **kwds: Any) -> Monad[_U]: """Chain a function that returns a monadic value. This method allows for chaining computations that produce monadic results. It applies the function to the wrapped value, which itself returns a monad, and then flattens the result to avoid nested monads. The ``bind`` operation (also known as ``flatMap`` or ``>>=`` in other languages) is the fundamental operation that gives monads their power for sequencing computations. Args: func: A callable that receives the wrapped value and returns a monadic instance wrapping a value of a (potentially different) type. *args: Additional positional arguments to pass to ``func``. **kwds: Additional keyword arguments to pass to ``func``. Returns: A monadic instance wrapping the result of ``func``, whose type matches the return type of ``func``, with the nested monadic structure flattened. """ ...
[docs] def join(self) -> Self: """Flatten a monad of monads into a single monad. This method unwraps one level of monadic structure, which is useful when dealing with nested monadic contexts. For monads wrapping other monads of the same type, ``join`` provides a way to reduce the nesting level. The ``join`` operation is related to ``bind`` and is defined as: ``m.join() == m.bind(lambda x: x)``. Returns: A monadic instance with one level of nesting removed. """ ...
[docs] def unwrap(self) -> _T: """Extract the wrapped value from the monadic context. This method returns the plain value contained within the monadic structure. This operation is sometimes called ``run`` or ``value`` in other monad implementations. Note that not all monads support this operation, as some monadic contexts (like IO monads) are meant to remain encapsulated to maintain referential transparency. Returns: The plain value that was wrapped in the monadic context. """ ...
def __call__(self) -> _T: """Alias for :meth:`unwrap`, allowing monads to be called as functions. This method enables syntactic sugar by allowing monadic instances to be called directly to extract their wrapped value, rather than explicitly calling :meth:`unwrap`. Returns: The plain value that was wrapped in the monadic context. """ ...
[docs] class Maybe(Generic[_T]): """Maybe class. Usage ----- .. code-block:: python import deluxe.types as types def parse_int(value: str) -> Maybe[int]: try: return Maybe(int(value)) except ValueError: return Maybe() def is_positive(value: int) -> Maybe[int]: return Maybe(value) if value > 0 else Maybe() def process(value: str) -> Maybe[int]: result = Maybe(value).bind(parse_int) result = result.bind(lambda n: Maybe(2 * n)) return result result = process("4") match result: case Maybe(types.Unset): print("error") case Maybe(8): print("eight") case Maybe(other): print("value:", other) Note: For pattern matching against :class:`deluxe.types.Unset` you can't use `Maybe(Unset)`, this won't work at runtime nor statically with typechecker. You should instead import the :py:mod:`deluxe.types` module and use `Maybe(types.Unset)`. """ __slots__: ClassVar[tuple[str, ...]] = ("__objclass__", "_name_", "_type", "_value") __hash__: ClassVar[None] = None # pyright: ignore[reportIncompatibleMethodOverride] __match_args__ = ("_value",) # pyright: ignore[reportUnannotatedClassAttribute] def __init__(self, value: _T = Unset) -> None: self._value: _T = value self._type: type[_T] = type(self._value) self._name_: str = Unset self.__objclass__: type = Unset def __set_name__(self, owner: type, name: str) -> None: self.__objclass__ = owner self._name_ = name @property def type(self) -> type[_T]: """Return the Python type of the wrapped value. Returns: The type of the value stored in this :class:`Maybe` instance. """ return self._type
[docs] @classmethod def pure(cls, value: _T) -> Self: """Wrap a plain value into the :class:`Maybe` monadic context. Creates a new :class:`Maybe` instance containing the given value. This is the primary way to enter the :class:`Maybe` context from a non-monadic value. Args: value: The plain value to wrap in the :class:`Maybe` context. Returns: A :class:`Maybe` instance containing the wrapped ``value``. Raises: ValueError: If ``value`` is :class:`deluxe.types.Unset`. Examples:: >>> from deluxe.functional import Maybe >>> Maybe.pure(42) Maybe[int](42) >>> Maybe.pure("hello") Maybe[str](hello) """ if value is Unset: msg = "Invalid 'Unset' value" raise ValueError(msg) return cls(value)
[docs] def map(self, func: Callable[[_T], _U], *_args: Any, **_kwds: Any) -> Maybe[_U]: """Apply a function to the wrapped value (functorial map). This method transforms the value inside the :class:`Maybe` context by applying the given function. It preserves the monadic structure, returning a new :class:`Maybe` containing the transformed value. If this :class:`Maybe` is empty (contains :class:`deluxe.types.Unset`), the function is not called and an empty :class:`Maybe` is returned. Args: func: A callable that receives the wrapped value and returns a transformed value of a (potentially different) type. Returns: A new :class:`Maybe` wrapping the result of ``func``, whose type matches the return type of ``func``. Returns an empty :class:`Maybe` if this instance is empty. Examples:: >>> from deluxe.functional import Maybe >>> Maybe.pure(5).map(lambda x: x * 2) Maybe[int](10) >>> Maybe().map(lambda x: x * 2) Maybe[NoneType](Unset) """ if self._value is Unset: return cast("Maybe[_U]", Maybe()) return Maybe(func(self._value))
[docs] def bind(self, func: Callable[[_T], Maybe[_U]], *_args: Any, **_kwds: Any) -> Maybe[_U]: """Chain a function that returns a :class:`Maybe` value. This method allows for chaining computations that produce :class:`Maybe` results. It applies the function to the wrapped value, which itself returns a :class:`Maybe`, and then flattens the result to avoid nested monads. If this :class:`Maybe` is empty (contains :class:`deluxe.types.Unset`), the function is not called and an empty :class:`Maybe` is returned. The ``bind`` operation (also known as ``flatMap`` or ``>>=`` in other languages) is the fundamental operation that gives monads their power for sequencing computations. Args: func: A callable that receives the wrapped value and returns a :class:`Maybe` wrapping a value of a (potentially different) type. Returns: A new :class:`Maybe` wrapping the result of ``func``, whose type matches the return type of ``func``, with the nested :class:`Maybe` structure flattened. Examples:: >>> from deluxe.functional import Maybe >>> def double_if_positive(x: int) -> Maybe[int]: ... return Maybe(x * 2) if x > 0 else Maybe() >>> Maybe.pure(5).bind(double_if_positive) Maybe[int](10) >>> Maybe.pure(-1).bind(double_if_positive) Maybe[NoneType](Unset) """ if self._value is Unset: return cast("Maybe[_U]", Maybe()) return func(self._value)
[docs] def join(self) -> Self: """Flatten a nested :class:`Maybe` into a single :class:`Maybe`. This method unwraps one level of monadic structure, which is useful when dealing with nested :class:`Maybe` contexts. It extracts the value and re-wraps it in a fresh :class:`Maybe`. The ``join`` operation is related to ``bind`` and is defined as: ``m.join() == m.bind(lambda x: x)``. Returns: A :class:`Maybe` instance with one level of nesting removed. Examples:: >>> from deluxe.functional import Maybe >>> nested = Maybe(Maybe(42)) >>> nested.join() Maybe[int](42) """ return self.pure(self.unwrap())
[docs] def unwrap(self) -> _T: """Extract the wrapped value from the :class:`Maybe` context. This method returns the plain value contained within the monadic structure. If this :class:`Maybe` is empty, it returns :class:`deluxe.types.Unset`. Returns: The plain value of type ``_T`` that was wrapped in the :class:`Maybe` context, or :class:`deluxe.types.Unset` if empty. Examples:: >>> from deluxe.functional import Maybe >>> Maybe.pure(42).unwrap() 42 >>> Maybe().unwrap() Unset """ return self._value
def __call__(self) -> _T: return self.unwrap() def __repr__(self) -> str: # pragma: no cover return f"{self.__class__.__name__}[{self._type.__name__}]({self._value})"
## # NOTE: https://blog.ploeh.dk/2022/05/30/the-lazy-monad/
[docs] class Lazy(Generic[_T]): """Implementation of the :class:`Monad` protocol for lazy evaluation. The :class:`Lazy` class wraps a callable that defers computation until explicitly requested. This enables lazy evaluation, where computations are only performed when their results are needed, and results are cached across multiple accesses. The :meth:`__set_name__` method is present solely to allow usage of :class:`Lazy` as enumeration's member in :class:`deluxe.enums.Enum`. Examples: Creating a lazy value from a callable:: from deluxe.functional import Lazy def expensive_computation() -> int: print("Computing...") return 42 lazy_value = Lazy(expensive_computation, int) # Nothing printed yet - computation not executed result = lazy_value.unwrap() # Prints "Computing..." # result is now 42 result2 = lazy_value.unwrap() # Prints again - result is not cached Using :meth:`pure` to wrap static values:: from deluxe.functional import Lazy lazy_int = Lazy.pure(42) print(lazy_int.unwrap()) # 42 Chaining transformations with :meth:`map`:: from deluxe.functional import Lazy lazy_value = Lazy.pure(5) lazy_doubled = lazy_value.map(lambda x: x * 2, int) lazy_squared = lazy_doubled.map(lambda x: x * x, int) print(lazy_squared.unwrap()) # 100 Chaining monadic computations with :meth:`bind`:: from deluxe.functional import Lazy def parse_int(s: str) -> Lazy[int]: return Lazy.pure(int(s)) lazy_str = Lazy.pure("42") lazy_int = lazy_str.bind(parse_int, int) print(lazy_int.unwrap()) # 42 Calling lazy values as functions:: from deluxe.functional import Lazy lazy_value = Lazy.pure(10) result = lazy_value() # Equivalent to lazy_value.unwrap() print(result) # 10 Note: While this class has a :meth:`__set_name__` method, it is **not** a descriptor. The method is used only to track metadata (``__objclass__`` and ``_name_``) when instances are assigned as class attributes. To access the value, explicitly call :meth:`unwrap` or use the callable syntax ``instance()``. Args: value: A callable (typically a function or lambda) that will be executed when the lazy value is unwrapped. type: The type of the value that will be produced when ``value`` is called. This is used for type annotations and can be different from the callable's actual return type. Note: The ``value`` callable is not executed during initialization. The computation is deferred until :meth:`unwrap` or :meth:`__call__` is invoked. See Also: - :class:`Monad`: The protocol that this class implements. - :class:`deluxe.environ.evar` """ __slots__: ClassVar[tuple[str, ...]] = ("__objclass__", "_name_", "_type", "_value") __hash__: ClassVar[None] = None # pyright: ignore[reportIncompatibleMethodOverride] def __new__(cls, value: Callable[[], _T], type: type[_T]) -> Self: # noqa: A002, D102 self = object.__new__(cls) self._value = value self._name_ = "" self.__objclass__ = NoneType self._type = cast("_T", value.type) if isinstance(value, Lazy) else type # pyright: ignore[reportAttributeAccessIssue] return self def __init__(self, value: Callable[[], _T], type: type[_T]) -> None: # noqa: A002, ARG002 self._value: Callable[[], _T] self._name_: str self.__objclass__: type self._type: type[_T] def __set_name__(self, owner: type, name: str) -> None: self.__objclass__ = owner self._name_ = name @property def type(self) -> type[_T]: """Return the Python type that this :class:`Lazy` instance will produce. Returns: The type annotation of the value that will be produced when this lazy computation is evaluated. Examples: >>> from deluxe.functional import Lazy >>> lazy_int = Lazy(lambda: 42, int) >>> lazy_int.type <class 'int'> """ return self._type
[docs] @classmethod def pure(cls, value: _U) -> Lazy[_U]: """Wrap a plain value into the lazy monadic context. Creates a :class:`Lazy` instance that will always return the given static value when unwrapped. This is the primary way to create a lazy value from a non-callable. Args: value: The plain value to wrap in the lazy context. Returns: A :class:`Lazy` instance that will produce ``value`` when unwrapped. Examples: >>> from deluxe.functional import Lazy >>> lazy_int = Lazy.pure(42) >>> lazy_int.unwrap() 42 """ def pure() -> _U: return value return cast("Lazy[_U]", cls(pure, type(value))) # pyright: ignore[reportArgumentType]
[docs] def map(self, func: Callable[[_T], _U], type: type[_U]) -> Lazy[_U]: # noqa: A002 """Apply a function to the lazy value using functorial map. Creates a new :class:`Lazy` instance that will apply ``func`` to the result of this lazy computation when unwrapped. This allows for chaining transformations in a lazy context. Args: func: A callable that receives the unwrapped value and returns a transformed value of a (potentially different) type. type: The type of the value that ``func`` will return. Returns: A new :class:`Lazy` instance that will produce ``func(value)`` when unwrapped, where ``value`` is the result of unwrapping this instance. Examples: >>> from deluxe.functional import Lazy >>> lazy_value = Lazy.pure(5) >>> lazy_doubled = lazy_value.map(lambda x: x * 2, int) >>> lazy_doubled.unwrap() 10 """ def lambda_() -> _U: return func(self._value()) lambda_.__name__ = func.__name__ return Lazy(lambda_, type)
[docs] def bind(self, func: Callable[[_T], Lazy[_U]], type: type[_U]) -> Lazy[_U]: # noqa: A002 """Chain a function that returns a lazy value using monadic bind. Applies ``func`` to the result of this lazy computation and returns the resulting :class:`Lazy` instance directly. This allows for chaining computations where each step produces a new lazy context. Args: func: A callable that receives the unwrapped value and returns a :class:`Lazy` wrapping a value of a (potentially different) type. type: The type of the value that the returned :class:`Lazy` will produce. Returns: The :class:`Lazy` instance returned by ``func(value)``, where ``value`` is the result of unwrapping this instance. Examples: >>> from deluxe.functional import Lazy >>> def parse_int(s: str) -> Lazy[int]: ... return Lazy.pure(int(s)) >>> lazy_str = Lazy.pure("42") >>> lazy_int = lazy_str.bind(parse_int, int) >>> lazy_int.unwrap() 42 """ def lambda_() -> _U: return func(self._value())._value() lambda_.__name__ = func.__name__ return Lazy(lambda_, type)
[docs] def join(self) -> Self: """Flatten this lazy value by wrapping it again. Creates a new :class:`Lazy` instance that will produce the result of unwrapping this instance. This is useful for normalizing lazy values and ensuring a consistent lazy context. Returns: A :class:`Lazy` instance that will produce the same value as calling :meth:`unwrap` on this instance. Examples: >>> from deluxe.functional import Lazy >>> lazy_value = Lazy.pure(42) >>> lazy_joined = lazy_value.join() >>> lazy_joined.unwrap() 42 """ return self.pure(self.unwrap()) # pyright: ignore[reportReturnType]
def __call__(self) -> _T: # noqa: D102 return self._value()
[docs] def unwrap(self) -> _T: """Execute the lazy computation and return its result. This method invokes the wrapped callable and returns its result. The callable is executed each time :meth:`unwrap` is called; results are not automatically cached. Returns: The result of executing the wrapped callable of type ``_T``. Examples: >>> from deluxe.functional import Lazy >>> def compute() -> int: ... print("Computing...") ... return 42 >>> lazy_value = Lazy(compute, int) >>> result = lazy_value.unwrap() Computing... >>> print(result) 42 Note: lazy_value() is equivalent to lazy_value.unwrap() """ return self._value()
@no_type_check def __repr__(self) -> str: # pragma: no cover if isinstance(self._value, Lazy): intern = repr(self._value) value = f"{self._value.__name__}({intern})" elif self._value.__name__ == "pure": value = repr(self._value()) else: value = self._value.__name__ return f"{self.__class__.__name__}[{self._type.__name__}]({value})"
[docs] class MaybeCallable(Generic[_T]): """Monad wrapping up a type or a callable returning this same type. The :class:`MaybeCallable` type is designed to represent values that can be either plain values or callables that produce values of the same type. This is particularly useful when defining enumerations where some members are simple values while others are callable (factory) members. When used as a base class for enumerations, :class:`MaybeCallable` enables a hybrid pattern where enum members can be either: - **Plain values**: Direct values of the wrapped type. - **Callable members**: Methods decorated with ``@member`` that accept arguments and return values of the wrapped type. Examples: Basic usage with plain values and callables:: from deluxe.functional import MaybeCallable # Wrapping a plain value plain = MaybeCallable(42) print(plain.unwrap()) # 42 # Wrapping a callable def double(x: int) -> int: return x * 2 callable_val = MaybeCallable(double) print(callable_val(21)) # 42 Usage as an enumeration base class:: from enum import Enum, member from deluxe.enums import Enum as DocEnum from deluxe.functional import MaybeCallable class Key(MaybeCallable[bytes]): def __call__(self, *args: bytes) -> bytes: return self._callable_(*args) class KeyStroke(Key[bytes], Enum): @staticmethod def _join(string: bytes, other: bytes) -> str: return string.decode() + other.decode() @member @staticmethod def Ctrl(key: bytes) -> str: return KeyStroke._join(b"C-", key) @member @staticmethod def Meta(key: bytes) -> str: return KeyStroke._join(b"M-", key) Space = b"Space" Tab = b"Tab" Enter = b"Enter" # Plain enum members print(KeyStroke.Space) # b"Space" print(KeyStroke.Tab) # b"Tab" # Callable enum members print(KeyStroke.Ctrl(b"h")) # "C-h" print(KeyStroke.Meta(b"a")) # "M-a" Another example with byte strings:: from deluxe.functional import MaybeCallable from deluxe.enums import Enum as DocEnum class BString(MaybeCallable[bytes]): @staticmethod def format(value: bytes) -> bytes: return b"".join((b"#{", value, b"}")) def map(self, func): return self.pure(func(self.unwrap())) class Format(BString, DocEnum): @member @staticmethod def str(string: bytes) -> bytes: return BString(string) @member @staticmethod def var(string: bytes) -> bytes: name = string.decode() return getattr(Enum("Format", ((name, string),), type=BString), name) active_window_index = b"active_window_index" pane_id = b"pane_id" # Plain enum members print(Format.active_window_index) # b"active_window_index" # Callable enum members print(Format.var(b"opt")) # creates a new Format member Note: When using :class:`MaybeCallable` as a base for enumerations, the ``__call__`` method should be overridden in the subclass to properly handle callable enum members. The ``@member`` decorator from :mod:`enum` is used to mark static methods as callable enum members. Args: value: Either a plain value or a callable that accepts no arguments and returns a value of the same type. See Also: - :class:`Lazy`: A monad for lazy evaluation of computations. - :class:`Maybe`: A monad representing optional values. - :class:`deluxe.enums.Enum`: Enhanced enumeration base class. """ __slots__: ClassVar[tuple[str, ...]] = ("_callable_", "_value_") def __new__(cls, value: _T | Callable[[_T], _T]) -> Self: # noqa: D102 self = object.__new__(cls) self._value_ = value if callable(value): self._callable_ = cast("Callable[[_T], _T]", value) else: def _u(*_: _T) -> _T: msg = f"'{type(value).__name__}' object is not callable" raise TypeError(msg) self._callable_ = _u return self def __init__(self, value: _T | Callable[[_T], _T]) -> None: # noqa: ARG002 self._callable_: Callable[[_T], _T] self._value_: _T | Callable[[_T], _T]
[docs] @classmethod def pure(cls, value: _T | Callable[[_T], _T]) -> Self: """Wrap a plain value or callable into the monadic context. Creates a new :class:`MaybeCallable` instance containing the given value. This is the primary way to enter the :class:`MaybeCallable` context from a non-monadic value. Args: value: A plain value or a callable that returns a value. Returns: A new :class:`MaybeCallable` wrapping the given value. """ return cls(value)
[docs] def map(self, func: Callable[[_T | Callable[[_T], _T]], _T]) -> Self: """Apply a function to the wrapped value (functorial map). This method transforms the value inside the :class:`MaybeCallable` context by applying the given function. It preserves the monadic structure, returning a new :class:`MaybeCallable` containing the transformed value. Args: func: A callable that receives the wrapped value and returns a transformed value of a (potentially different) type. Returns: A new :class:`MaybeCallable` wrapping the result of ``func``, whose type matches the return type of ``func``. """ return self.pure(func(self._value_))
[docs] def bind(self, func: Callable[[_T | Callable[[_T], _T]], Self]) -> Self: """Chain a function that returns a :class:`MaybeCallable` value. This method allows for chaining computations that produce :class:`MaybeCallable` results. It applies the function to the wrapped value, which itself returns a :class:`MaybeCallable`, and then flattens the result to avoid nested monads. The ``bind`` operation (also known as ``flatMap`` or ``>>=`` in other languages) is the fundamental operation that gives monads their power for sequencing computations. Args: func: A callable that receives the wrapped value and returns a :class:`MaybeCallable` wrapping a value of a (potentially different) type. Returns: A new :class:`MaybeCallable` wrapping the result of ``func``, whose type matches the return type of ``func``, with the nested :class:`MaybeCallable` structure flattened. """ return func(self._value_)
[docs] def join(self) -> Self: """Flatten a nested :class:`MaybeCallable` into a single instance. This method unwraps one level of monadic structure, which is useful when dealing with nested :class:`MaybeCallable` contexts. It extracts the value and re-wraps it in a fresh :class:`MaybeCallable`. The ``join`` operation is related to ``bind`` and is defined as: ``m.join() == m.bind(lambda x: x)``. Returns: A :class:`MaybeCallable` instance with one level of nesting removed. """ return self.pure(self.unwrap())
[docs] def unwrap(self) -> _T: """Extract the plain wrapped value from the monadic context. This method returns the plain value contained within the monadic structure. Raises an error if the wrapped value is a callable rather than a plain value. Returns: The plain value that was wrapped in the :class:`MaybeCallable` context. Raises: TypeError: If the wrapped value is a callable. """ if callable(self._value_): msg = "could only unwrap plain value" raise TypeError(msg) return self._value_
def __call__(self, *args: _T) -> _T: """Call the wrapped callable with the given arguments. This method invokes the wrapped callable and returns its result. If the wrapped value is not callable, a :class:`TypeError` is raised. Args: *args: Arguments to pass to the wrapped callable. Returns: The result of calling the wrapped callable. """ return self._callable_(*args) def __repr__(self) -> str: if not isinstance(self, MaybeCallable) and callable( # pyright: ignore[reportUnnecessaryIsInstance] self ): return f"{self.__class__.__name__}" # Enum's member case return f"{self.__class__.__name__}({self})" def __str__(self) -> str: return str(self._value_) if hasattr(self, "_value_") else self.__repr__()