Usage
The purpose of MayPy is to provide a type-level solution for representing and manipulating optional values instead of None reference. This away around, no need to worry about the optional value and testing it, before applying process on it.
There are two types of Maybe container, either it contains a value or it is empty.
A Maybe object become empty when the value it should contain is None.
Create Maybe objects¶
Empty¶
There are 3 ways to have a empty Maybe.
Note
maybe(None) is here as an example, for readability use the others to instantiate an empty Maybe.
Valuated¶
Valuated Maybe or Some, is quite straightforward, provide some value whatever it is, as long as is not None to of .
Checking value presence¶
There are three methods to check if the value is present or not.
Either by using is_present
to know if the container has a value
or use is_empty to verify the absence of value.
Another approach is to examine the "truthiness" of Maybe.
Get wrapped value¶
To retrieve the value contained inside a Maybe, the method get
returns it or raises an EmptyMaybeException.
Handle Emptiness¶
Default value¶
When Maybe is empty, it's possible to provide a default value using or_else.
Taking either a value or a Supplier
(with mypy the value provide as default should be the same type as the hypothetical value wrapped).
assert Maybe.empty().or_else(lambda: 12) == 12
assert maybe("present").or_else(lambda: "absent") == "present"
Tips
We may wonder what is the different between passing the function and calling it as the default value. The function is used only if Maybe is empty, whereas in the other hands, it will be invoked no matter what.
def populate_data() -> list[str]:
print("invocation of populate_data")
return ["python", "c++", "c", "java"]
assert Maybe.empty().or_else(populate_data) == ["python", "c++", "c", "java"]
>>> "invocation of populate_data"
assert maybe(["ruby", "kotlin"]).or_else(populate_data) == ["ruby", "kotlin"]
assert maybe(["ruby", "kotlin"]).or_else(populate_data()) == ["ruby", "kotlin"]
>>> "invocation of populate_data"
Raise error¶
Another approach for handling value absence, is to raise a custom exception by
or_else_raise when Maybe is empty.
class CustomError(Exception):
pass
assert Maybe.empty().or_else_raise(CustomError())
>>> CustomError
assert maybe(12).or_else_raise(CustomError()) == 12
Manipulating the value¶
Note
In the further examples, I keep using lambda function to keep it simple and easy to read. It totally possible to use named function, no matter what you use as long as it respects the API contract.
By the way mypy will infer types even with lambda !!!
Filtering¶
It is possible to perform inline condition on our wrapped value with filter.
Taking a Predicate, it will check if the value matches the predicate and returning Maybe itself when passing,
otherwise an empty Maybe is returned.
Built-in Predicates¶
Some built-in predicates have been added.
price = 999.99
assert maybe(price).filter(lambda x: x <= 1000).is_present()
assert maybe(price).filter(lambda x: x >= 1000).is_empty()
You may wonder why using it and what is the gain. Let's dive on a more concrete example!
You want to watch a movie, and you only care about its release date. It should be in certain interval.
from dataclasses import dataclass, field
@dataclass
class Movie:
director: str
title: str
year: int
genre: list[str] = field(default_factory=list)
oscars: list[str] | None = field(default=None)
from typing import Callable
def interval_checker(start_year: int, end_year: int) -> Callable[[Movie], bool]:
def is_in_range(movie: Movie | None) -> bool:
if movie is not None:
return start_year <= movie.year <= end_year
return False
return is_in_range
movie = Movie("Luc Besson", "Taxi", 1998, ["comedy"])
assert interval_checker(1990, 2005)(movie)
assert not interval_checker(2000, 2020)(movie)
assert not interval_checker(2000, 2020)(None)
Mapping¶
With a similar syntax, we can transform the value inside Maybe using
map.
Reusing last example, getting the number of oscars rewarding the movie.
movie = Movie("Luc Besson", "Taxi", 1998, ["comedy"])
assert (
maybe(movie)
.map(lambda film: movie.oscars)
.map(lambda oscars: len(oscars))
.or_else(0) == 0
)
It is powerful to chain filter and map together. Like checking the correctness of an input by a user.
from maypy.predicates import one_of
VALID_BOOLS = ("y", "n", "yes", "no")
user_input = "y "
assert (
maybe(user_input)
.map(lambda input_: input_.strip())
.filter(one_of(VALID_BOOLS))
.is_present()
)
Conditional action¶
The last method is if_present,
it allows to perform some code, using a Consumer function (Callable[[VALUE], None]),
on the wrapped value if present, otherwise nothing will happen.
Warning
To keep it functional, the function passed should not modify the value but only use it.
Please use chaining of map and get the value instead.