Blog

It's minimal, but I'm posting things.


A monadic Result type using generics in Python

from typing import TypeVar, Generic, Optional, Callable
T = TypeVar('T')
U = TypeVar('U')
E = TypeVar('E', bound=Exception)

class Result(Generic[T, E]):
    """Stores a result, with either a value or an error."""

    def __init__(self, value: Optional[T] = None, throwable: Optional[E] = None):
        if value is None and throwable is None:
            raise ValueError("value and throwable cannot both be none.")
        if value is not None and throwable is not None:
            raise ValueError("value and throwable cannot both be not none.")
        if throwable and not isinstance(throwable, Exception):
            raise ValueError("throwable must be an Exception")
        if value:
            self.__value = value
            self.__throwable = None
        if throwable:
            self.__value = None
            self.__throwable = throwable

    def get_value(self) -> T:
        if self.is_error():
            raise self.__throwable
        return self.__value

    def get_error(self) -> E:
        if self.is_error():
            return self.__throwable
        raise Exception("Cannot retrieve error on result without error.")

    def is_error(self) -> bool:
        return self.__throwable is not None

    def __repr__(self):
        return f"""Result(value=`{"None" if self.is_error() else self.get_value()}`, error=`{"None" if not self.is_error() else self.get_error()}`)"""

    def __str__(self):
        if self.is_error():
            return str(self.__throwable)
        else:
            return str(self.__value)

    @staticmethod
    def of(value: T) -> 'Result[T, E]':
        if isinstance(value, Exception):
            return Result.of_error(value)
        return Result(value, None)

    @staticmethod
    def of_error(error: E) -> 'Result[T, E]':
        if not isinstance(error, Exception):
            return Result.of(error)
        return Result(None, error)



class Lazy(Generic[T, E]):
    """Chains function calls and only executes then by calling apply."""

    def __init__(self, _callable: Callable[[], U]):
        if _callable is None:
            raise ValueError("_callable cannot be none.")
        self._callable = _callable

    def map(self, _new_lambda: Callable[[T], U]) -> 'Lazy[U, E]':
        return Lazy(lambda: _new_lambda(self._callable()))

    def apply(self) -> Result[T, E]:
        try:
            return Result.of(self._callable())
        except Exception as ex:
            return Result.of_error(ex)

Simple test suite

def test_result_value():
    result = Result.of("abc")
    assert result.get_value() == "abc"
    assert not result.is_error()
    result = Result.of_error("abc")
    assert result.get_value() == "abc"
    assert not result.is_error()

def test_result_error():
    result = Result.of_error(Exception("Error"))
    assert result.is_error()
    result = Result.of(Exception("Error"))
    assert result.is_error()

def test_lazy_apply():
    result = Lazy(lambda: 1).apply()
    assert result.get_value() == 1

def test_lazy_map():
    result = Lazy(lambda: 1)\
            .map(lambda i: i+1)\
            .apply()
    assert result.get_value() == 2

def test_result_map_error():
    result = Lazy(lambda: 1)\
            .map(lambda i: i+1)\
            .map(lambda i: i/0)\
            .apply()
    assert result.is_error()
    assert isinstance(result.get_error(), ZeroDivisionError)


Published on 2025-03-14T15:40:56.87091Z
Last updated on 2025-03-14T15:40:56.87091Z