Skip to content

Include helper methods to enhance development when using strawberry.Maybe types #3983

@BuriP

Description

@BuriP

Feature Request Type

  • Core functionality
  • Alteration (enhancement/optimization) of existing feature(s)
  • New behavior

Description

Hi again!! I would like to propose the inclusion of some helper methods to strawberry.Maybe to enhance the use of this type.

I believe that the philosophy which the team is trying to go towards greatly resembles the Rust implementation of the Option enum. See here the docs.

I believe that methods to unwrap ,is_some, is_none and similar (which can be found in the above link) would greatly benefit strawberry's ease of use when it comes to the Maybe type.

I have started working on a implementation for it ,but since Im not very familiar with the library although want to start contributing more actively, I will leave here below the initial implementation to gather opinions from the team and whether this inclusions are even desirable.

from typing import Callable, NoReturn, overload, TypeVar, Generic, Any

U = TypeVar("U")
R = TypeVar("R")


def is_some(m: "Maybe[T]") -> bool:
    return isinstance(m, Some)

def is_none(m: "Maybe[Any]") -> bool:
    return m is None



@overload
def unwrap(m: "Some[T]") -> T: ...
@overload
def unwrap(m: None) -> NoReturn: ...
def unwrap(m):
    """Return contained value; raise ValueError if None."""
    if isinstance(m, Some):
        return m.value
    raise ValueError("Tried to unwrap a missing value (None)")

@overload
def expect(m: "Some[T]", message: str) -> T: ...
@overload
def expect(m: None, message: str) -> NoReturn: ...
def expect(m, message: str):
    """Like unwrap() with a custom error message."""
    if isinstance(m, Some):
        return m.value
    raise ValueError(message)

def unwrap_or(m: "Maybe[T]", default: T) -> T:
    """Return value if present; otherwise the eager default."""
    return m.value if isinstance(m, Some) else default

def unwrap_or_else(m: "Maybe[T]", default: Callable[[], T]) -> T:
    """Return value if present; otherwise compute a lazy default()."""
    return m.value if isinstance(m, Some) else default()

def unwrap_or_default(m: "Maybe[T]", *, factory: Callable[[], T]) -> T:
    """
    Pythonic Option::unwrap_or_default.
    Pass a zero-arg factory for T (e.g. int, str, list, dict, or any callable).
    """
    return m.value if isinstance(m, Some) else factory()

@overload
def unwrap_unchecked(m: "Some[T]") -> T: ...
@overload
def unwrap_unchecked(m: None) -> T: ...
def unwrap_unchecked(m):
    """
    UNSAFE: return value without checking.
    Calling on None will raise AttributeError (undefined behavior in Rust terms).
    """
    return m.value  # type: ignore[attr-defined]


@overload
def map(m: "Some[T]", f: Callable[[T], U]) -> "Some[U]": ...
@overload
def map(m: None, f: Callable[[T], U]) -> None: ...
def map(m, f):
    """Apply f to the value if Some; pass through None."""
    if isinstance(m, Some):
        return Some(f(m.value))
    return None

@overload
def inspect(m: "Some[T]", f: Callable[[T], None]) -> "Some[T]": ...
@overload
def inspect(m: None, f: Callable[[T], None]) -> None: ...
def inspect(m, f):
    """Call f(value) for side effects if Some; return original Maybe."""
    if isinstance(m, Some):
        f(m.value)
        return m
    return None

def map_or(m: "Maybe[T]", default: U, f: Callable[[T], U]) -> U:
    """If Some, return f(value); else return eager default."""
    return f(m.value) if isinstance(m, Some) else default

def map_or_else(m: "Maybe[T]", default: Callable[[], U], f: Callable[[T], U]) -> U:
    """If Some, return f(value); else compute lazy default()."""
    return f(m.value) if isinstance(m, Some) else default()



# I dont really like this approach with the class but couldn't figure out how to better include it 

class MaybeMethods(Generic[T]):
    __slots__ = ("_m",)
    def __init__(self, m: "Maybe[T]") -> None:
        self._m = m

    # Introspection
    def is_some(self) -> bool: return is_some(self._m)
    def is_none(self) -> bool: return is_none(self._m)

    # unwrapping Maybe
    def unwrap(self) -> T: return unwrap(self._m)  
    def expect(self, message: str) -> T: return expect(self._m, message)  
    def unwrap_or(self, default: T) -> T: return unwrap_or(self._m, default)
    def unwrap_or_else(self, default: Callable[[], T]) -> T: return unwrap_or_else(self._m, default)
    def unwrap_or_default(self, *, factory: Callable[[], T]) -> T: return unwrap_or_default(self._m, factory=factory)
    def unwrap_unchecked(self) -> T: return unwrap_unchecked(self._m)  

    # transformations
    def map(self, f: Callable[[T], U]) -> "MaybeMethods[U]":
        return MaybeMethods(map(self._m, f))
    def inspect(self, f: Callable[[T], None]) -> "MaybeMethods[T]":
        return MaybeMethods(inspect(self._m, f))
    def map_or(self, default: U, f: Callable[[T], U]) -> U:
        return map_or(self._m, default, f)
    def map_or_else(self, default: Callable[[], U], f: Callable[[T], U]) -> U:
        return map_or_else(self._m, default, f)

def methods(m: "Maybe[T]") -> MaybeMethods[T]:
    return MaybeMethods(m)


__all__ += [
    "is_some",
    "is_none",
    "unwrap",
    "expect",
    "unwrap_or",
    "unwrap_or_else",
    "unwrap_or_default",
    "unwrap_unchecked",
    "map",
    "inspect",
    "map_or",
    "map_or_else",
    "MaybeMethods",
    "methods",
]

I believe this could perhaps be added to the strawberry.types.maybe file although it may increase the complexity there.

Would love to hear your opinions here!!

Ty again!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions