All these lambdas seem excessive. "Maybe"-like pattern in Python can look like (None is falsy and custom objects are true):
# note: `None and f()` returns None without calling f
balance = user and user.get_balance()
credit = balance and balance.credit_amount()
discount_program = choose_discount(credit) if credit and credit > 0 else None
Nullable (Optional[Foo] / Union[Foo, None]) and Maybe[Foo] are similar but not quite the same thing. The difference is quite subtle.
Optional[Foo] is same as Foo | None, meaning you can operate on foo with Foo's methods, except that if it's None, you get NoneType errors.
Maybe[Foo] is an actual container. You have to map over or unwrap it to operate on Foo.
The big difference is ergonomics/opinion. You can't actually map over Optional, so every time you wanna use it, you have to manually check if foo is not None. Whereas Maybe, you can operate generically over it. Some folks think "is not None" is better. Personally I hate NoneType errors in prod and find that much more painful than a bit of indirection.
It looks like a distinction without a difference. We can consider `None and f()` pattern as explicit syntax that unwraps objects as necessary without infecting other code.
Background: an object that either None or true in a boolean context (true unless overriden for custom objects).
Given such object, we can consider it in a virtual Maybe container/box.
When we want to use it, we have to unwrap it using `obj and obj.method()` syntax.
Then `obj.method()` is ordinary "unwrapped" call.
Just to remind you. Here's how "ergonomic" Maybe variant from the article look like:
# Type hint here is optional, it only helps the reader here:
discount_program: Maybe['DiscountProgram'] = Maybe.from_optional(
user,
).bind_optional( # This won't be called if `user is None`
lambda real_user: real_user.get_balance(),
).bind_optional( # This won't be called if `real_user.get_balance()` is None
lambda balance: balance.credit_amount(),
).bind_optional( # And so on!
lambda credit: choose_discount(credit) if credit > 0 else None,
)
The dry-python lambda soup is awful, I totally agree there. That's just there mostly for demonstration purposes though. Generally you'd actually use the flow construct (monads) to compose methods.
But `obj and obj.method()` really is not the same thing as `obj.map(method)`. "virtual Maybe container/box" is a nice idea, and does actually type-check with mypy (mostly), but you cannot actually compose it with other functions. The problem is, each time you do `obj and obj.method()`, you end up union-ing type(obj) and type(obj.method).
main.py:40: note: Revealed type is "Union[__main__.User, None]"
main.py:41: note: Revealed type is "Union[__main__.User, None, builtins.float]"
main.py:42: note: Revealed type is "Union[__main__.User, None, builtins.float]"
main.py:43: note: Revealed type is "Union[__main__.User, None, builtins.float, builtins.str]"
True Maybe types are more precise. Maybe if the Mypy engine could be retooled to recognise the `obj and obj.method()` idiom as tantamount to `obj.map(method)`, this could be avoided.
But that's likely a me problem, I assume that for people more familiar with monads and all that jazz, it's easier to understand.