Another option is Exceptions. The function either does what it's supposed to, or freaks out.
You can remove the null checks and the software will raise a null pointer exception. In the first example, could raise a NotLoggedInException.
It's still a maybe function, but you have a mechanism for expressing the why-notness of the function run, as opposed to returning a generic null.
As an aside, I prefer the "Unless" model of thinking vs the "Maybe" model of thinking. It's biased towards success. It presumes that the function is most likely to do something unless a precheck fails. filterBestFriendsUnless vs maybeFilterBestFriends. getUserUnless vs maybeGetUser. If we go this far down the rabbit hole, we can assume there's always an "unless". Programs run out of memory, stacks have limited depth. There are maybe conditions for which we can not account.
I think that's true for checked exceptions; in Typescript, I'd rather see that a function may return a null, rather than get surprised by a possible exception that's not telegraphed.
I think that's my biggest problem with exceptions. I have to rely on the doc comments to figure out whether a method can throw exceptions and which and when. And who knows if that covers all the possible exceptions from all the code that method relies on. It entirely sidesteps the type system and means I can't rely on the input/output types when using a method.
> I have to rely on the doc comments to figure out whether a method can throw exceptions
But you still have to rely on the docs to tell if a function can abort execution (say, by calling std::optional<T>::value() when there's no value). And an unhandled exception would abort just the same. Where do you see there being a difference?
> and which and when.
Maybe types don't tell you that either, you still need documentation for that.
Even worse, Maybe types cannot tell you that unless they're leaf-ish functions. Because they may call opaque functions (such as your own callbacks) for which they have no such knowledge to begin with. Thus they have to support propagating some type-erased error type... which is exactly what exceptions do.
Well it uses exceptions in the case your generator is at the end, not usually at the end of a for loop because a for loop by definition iterates over a list until the list is finished.
The exception actually occurs when you call next() on a generator which cannot return any more values, or is finished, in which case `StopIteration` is usually raised.
All Python iterators raise StopIteration at the end of iteration. For-loops always use the iterator protocol. Neither generators nor lists are special in this regard.
>>> next(iter([]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> next(iter((lambda: (yield 5) if False else None)()))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Semantically, Python separates Exceptions and Errors. The mechanism for throwing and catching them is the same.
Here's a quick description from somewhere on the interwebs: An error is an issue in a program that prevents the program from completing its task. In comparison, an exception is a condition that interrupts the normal flow of the program.
Then there is StopIteration, which does not fit well into either of the two above. It's a wart I've learned to treat as a beauty mark.
Well, by your definition of Exceptions and Errors, errors are something that Python, the formal language as understood by the computer, doesn't know anything about. It's a concept for humans that they can use when analysing programs. And different analyses might come to different conclusions. [0]
You could signal errors via eg returning None or False or throwing an exception. But not throwing an exception could also be an error. (Eg if your for-loop never ends, that might be an error. And that would be synonymous with StopIteration never being thrown.)
[0] Eg for a program like 'cat' it would normally be considered an error, when the file being read doesn't exist. But perhaps in my particular usage, that's expected to occur quite often, and is a normal part of my operation. You can translate this example to Python: FileNotFound might be an error, or a normal condition.
In this case, it's not being used for basic control flow. It's a prerequisite of the function that the user is logged in - and you violated that so it's an error. Returning null masks the reason why that happened.
As others already said: you shouldn't even be able to call this function when your pre-requisites for calling it are violated, ideally. You can achieve that by putting this function inside some sort of object which can't be created without a logged in user. If you don't have that, you can't ask for user information.
So what I meant is that you see the exact same maybe pattern in many projects but instead of returning null there is a guard that throws an exception. I agree with the solution.
Not exactly. "getUser" not having a user to get is not exceptional unless you only have logged in users. If you have logged-out users, then "getUser" should gracefully handle the case of an unknown, logged-out user (either returning null or some other sentinel value).
What the exceptional case is depends on the what the pre- and post-conditions of the function are. If a function assumes that the user is logged in then the user not being logged in is indeed exception. Not to say that it is good design though. That function is quite fragile like this. If it must assume that a user is logged in, then it could easily require a user to be given as an argument which will remove the whole possibility.
An alternative I've used or seen used in Java is to put @Nullable on the function. The caller knows the result could be null, and must check for it. Linters/Static analysis can verify when you haven't checked it as well.
There's an urge to return Optional<Object> but now you must check Optional.isPresent AND object != null.
In my experience, the "best" code (defining "best" as some abstract melange of "easy to reason about", "easy to modify", "easy to compose", and "easy to test") ends up following the characteristics outlined by the sum of these three essays — strictly and rigorously elevating exceptions/failures/nulls to first-class types and then pushing them as high in the stack as possible so callers _must_ deal with them.
What constitues the "best" code depends on the incidental complexity of the problems you're trying to solve. Great code is when you have just enough of all those things, but have too much or too little and the code is worse.
You're right, of course — there are parts of my codebase that flagrantly disregard these rules, and did so for good reasons that I don't regret.
But I've found that while "everything is relative and should be situated in the context of the problem you're trying to solve" is a useful truism, it makes for poor praxis. It's hard to improve existing code or develop newer engineers without _some_ set of compasses and heuristics for what "good code" is, and once you develop that set the patterns and strategies for implementing "good code" naturally follows.
I agree, I'd imagine as a senior you have a good sense of what counts as good enough. Unfortunately, there are too many would-be seniors justifying horrendous amounts of accidental complexity as "good practice".
The author has conflated two concepts into "maybe function". Parsing is "maybe" in the sense that the parser will either return your object or fail. But it doesn't have to do any hidden, surprising behaviour like the "if (!loggedIn) {" line in the article.
I hold these two essays similarly high in influence for myself. The pipeline/railway oriented programming really made it click about how to use first-class types to deal with error cases elegantly.
Unfortunately, a lot of languages make it difficult to have the compiler enforce exhaustiveness.
If you just have 1 step then there is no advantage.
If you have multiple steps then the advantage is that you never have to unpack "in the middle" and you don't have to care - and the compiler has your back.
Classical example: show the street number of the user or show <None> if there is no street number. There can be multiple things missing on the way and multiple transformations might happen on the way. E.g. the user might not even have an adress saved alltogether.
In that case, you only have to "check whether those Maybes contain values or not" once at the very end.
I dont think there is any advantage when the language lacks syntax level support for Monads. E.g. in Haskell (which does) it would look roughly like:
getUser :: Maybe User
getUser = ...
getFriends :: Maybe [User]
getFriends = do
user <- getUser
let friends = getFriendsForUser user
return friends
bestFriends :: Maybe [User]
bestFriends = do
friends <- getFriends
let besties = filter isBestFriend friends
return besties
render :: IO ()
render = do
let bffs = getBestFriends
case bffs of
Just besties -> renderBestFriends besties
Nothing -> renderNoFriends
The Maybe monad itself contains the equivalent of runSafely from the article, and the syntax propagates the failure case transparently from getUser down to the choice of render function used. All without either the hassle of handling null cases, or the danger that you might forget to handle them and the code crash. Without syntax level support, its not obviously an improvement to me
You do have another solution that can lower the amount of conditions: Null Objects. These don’t fit every use cases, but they can allow you to express what’s missing, or not defined, or empty, and avoid nil pointers dereference or conditions to check the state.
As Sandy Metz is used to say « Nothing is Something »[0]
> Here’s a specific example, it’s a “maybe” function as it only returns the friends of a user, if the user is logged in. Basically it introduces a possible return null.
function maybeGetUser(): User | null {
if (!loggedIn) {
return null;
}
return fetchUser();
}
I believe this is an error. The code sample I took from the article is about getting a user, not the user's friends.
Since that function will return a list, an empty List might work.
Program logic fundamentally has to contend with different conditions.
Sometimes the user will be logged in and have friends, sometimes they won't.
The "maybe" style has the inconsistency embedded in the type system; it's
impossible to have an invocation to getFriends and then not handle the
resulting possibility of not being logged in.
Shifting it up to the caller just means that you're going to have to remember
to ensure the user is logged in before calling getFriends otherwise you'll get
some kind of error, which might give you more control, but now there's no
guarantee in the type system that you've handled the case where the user isn't
logged in.
Writing ifs everywhere to handle failure conditions might be a bit of a pain,
but that's more of a failing of the language than the style.
I agree with this. I prefer explicit optional/null return types, otherwise the possibility of an error still exists but is now hidden to the caller. Langs with syntax for concise optional chaining and such fix this problem imo
Note that Typescript - the language of the examples - does have proper null through it's type system. A function that returns `T | null` will require callers check for null before using T.
The proliferation of conditional "maybe" functions is a sign that your call graph is contrived and unnatural. You shouldn't be checking "userLoggedIn == true" in each and every accessor function. Ideally, such checks should bubble up towards the top of the call stack, and be performed once in an event loop iteration. The calling code should make sure that some basic prerequisites are met.
The kicker here is that the author implemented a functor and called it a monad. So of course readers are going to think "the monad approach" is confusing and stay away.
I mean even if you implement a more standard Monad interface plenty of functional programmers still find working with Monads to be ugly. It's really not a solved area.
I’ll accept that. But do notation is the closest thing to sensible we have, whereas most of these articles are just constantly trying to chip away at part of the problem in the hope that they’ll be able to make the whole mountain disappear one stone at a time. And to date, I don’t find the evidence promising that they can.
I’m not wedded to stuff like monadic state, I think that might be a bridge too far for regular programming (and besides which, it doesn’t really generalise anyway) but that still leaves a large family of issues that we’re all aware of but trying to dodge.
This is a pattern you cannot always avoid, due to react, but i don't think it should be normalized.
Two remarks:
• the render function is omitted, this pattern as a huge impact on application behaviors, if not for display-as-you-load issues, on DOM hidden state (things like focus, animations, etc…) for web apps.
• App's do have a global state, with self-consistency, scattering it in a mixed match of loading cache and self contained components just make it hard to work with. I think it's better to have a centralized upper level parent component that manage the transitional initialization states and consistency, not necessarily for the whole app, but at least for the whole displayed UI content.
I can't wait for more languages to adopt the "?" operator [1] like the Rust one. It's just syntactic sugar for "if expr null return null" but makes it far easier to write code in a more monadic style.
I can't wait for more languages to simply not include null at all. It makes trying to spot them in static analysis and runtime checks much easier, because you no longer need either.
In the context of the parent's example, Rust doesn't include null at all, it just has a standard Option type with a None variant. While the other mentioned languages (JS and Go) do have null, they don't necessarily need to remove it to start getting the benefits, they just need to provide standardized alternatives and get the community to follow along (and if, say, a fancy new ? operator only worked on these new types and didn't work on null in general, that would be a strong carrot).
And what if `fetchUser` hits an error? At the very least pop your de-maybeifier after the async call. Or use something language standardised (like a promise in JS where you can just throw).
I'm all for a perfy shortcut / early return but this maybe just seems like an abstraction on a non-issue.
Can’t disagree more. Solution 1 just presents risk that some calls getUser() without doing the log in check. Then what happens?
It is false that getUser being a “maybe function” forces the other functions like getFriends to be maybe functions. Don’t let them take null in their arguments. Force the caller to deal with the null when it is returned by getUser.
I would argue that the vast majority of functions in real world software are maybe functions in that they can fail. You need to be able to deal with failure. Not only can the user not be logged in, there can be a network issue, etc that makes even downstream functions fail.
Also, you have to deal with developer mistakes and what happens when they call incorrectly. This can be something as simple as getting the first element of a collection. What happens when the collection is empty? You can adopt the C++ approach of “undefined behavior” but it turns out to be dangerous.
Monads provide a nice disciplined way to dealing with this and composing together functions that can potentially fail.
Thankfully, newer languages such as providing support for monads and older languages are evolving features/libraries for monadic error handling.
If enumerating every possible failure mode of a function is impossible, then that would underscore the importance of failing fast and dynamically restarting components in order to provide robustness in the face of unforeseeable errors.
In C# the closest analogy I can think of is the "Try" pattern. For example, you have int.Parse(string) which returns int, and int.TryParse(string, out int) which returns bool. The fact that the returned value is the validation is a strong incentive to do something with it.
You can address this with explicit parameterization instead of global state. That way the missing data is an obvious type error rather than a surprise in the middle of a running function.
This is a great example of the issue with using monads and monad-like patterns in languages that don't have proper support via language constructs for these.
In rust for example, this is trivially handled with the questionmark postfix operator — which is just sugar for match — whereas in languages like JS and Java, stacking Optionals and so on can be rather painful as all this sugar is done manually.
What seems that the fundamental problem is that the functions depend on global state that is not explicitly passed in (is the user logged in?). Maybe an explicit session parameter could work better here. You only have a session when the user is logged in, so you can't even pass anything to the functions otherwise. It can of course be passed further recursively.
By the way, I have never understood the practice of using a verb in the name of a (pure) function; naming the function after its result using a noun or adjective phrase makes much more sense.
102 comments
[ 6.6 ms ] story [ 162 ms ] threadhttps://pkg.go.dev/text/template#Must
You can remove the null checks and the software will raise a null pointer exception. In the first example, could raise a NotLoggedInException.
It's still a maybe function, but you have a mechanism for expressing the why-notness of the function run, as opposed to returning a generic null.
As an aside, I prefer the "Unless" model of thinking vs the "Maybe" model of thinking. It's biased towards success. It presumes that the function is most likely to do something unless a precheck fails. filterBestFriendsUnless vs maybeFilterBestFriends. getUserUnless vs maybeGetUser. If we go this far down the rabbit hole, we can assume there's always an "unless". Programs run out of memory, stacks have limited depth. There are maybe conditions for which we can not account.
But you still have to rely on the docs to tell if a function can abort execution (say, by calling std::optional<T>::value() when there's no value). And an unhandled exception would abort just the same. Where do you see there being a difference?
> and which and when.
Maybe types don't tell you that either, you still need documentation for that.
Even worse, Maybe types cannot tell you that unless they're leaf-ish functions. Because they may call opaque functions (such as your own callbacks) for which they have no such knowledge to begin with. Thus they have to support propagating some type-erased error type... which is exactly what exceptions do.
So, again: how is the situation different?
The exception actually occurs when you call next() on a generator which cannot return any more values, or is finished, in which case `StopIteration` is usually raised.
Here's a quick description from somewhere on the interwebs: An error is an issue in a program that prevents the program from completing its task. In comparison, an exception is a condition that interrupts the normal flow of the program.
Then there is StopIteration, which does not fit well into either of the two above. It's a wart I've learned to treat as a beauty mark.
You could signal errors via eg returning None or False or throwing an exception. But not throwing an exception could also be an error. (Eg if your for-loop never ends, that might be an error. And that would be synonymous with StopIteration never being thrown.)
[0] Eg for a program like 'cat' it would normally be considered an error, when the file being read doesn't exist. But perhaps in my particular usage, that's expected to occur quite often, and is a normal part of my operation. You can translate this example to Python: FileNotFound might be an error, or a normal condition.
There's an urge to return Optional<Object> but now you must check Optional.isPresent AND object != null.
1. Parse, don't validate (https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...)
2. Pipeline-oriented programming (https://fsharpforfunandprofit.com/pipeline/)
In my experience, the "best" code (defining "best" as some abstract melange of "easy to reason about", "easy to modify", "easy to compose", and "easy to test") ends up following the characteristics outlined by the sum of these three essays — strictly and rigorously elevating exceptions/failures/nulls to first-class types and then pushing them as high in the stack as possible so callers _must_ deal with them.
But I've found that while "everything is relative and should be situated in the context of the problem you're trying to solve" is a useful truism, it makes for poor praxis. It's hard to improve existing code or develop newer engineers without _some_ set of compasses and heuristics for what "good code" is, and once you develop that set the patterns and strategies for implementing "good code" naturally follows.
Parsing is determining whether you should do it or not- it's about setting up a boundary from which you never attempt something that would be a maybe.
Unfortunately, a lot of languages make it difficult to have the compiler enforce exhaustiveness.
Solution3 for their example:
If you have multiple steps then the advantage is that you never have to unpack "in the middle" and you don't have to care - and the compiler has your back.
Classical example: show the street number of the user or show <None> if there is no street number. There can be multiple things missing on the way and multiple transformations might happen on the way. E.g. the user might not even have an adress saved alltogether.
In that case, you only have to "check whether those Maybes contain values or not" once at the very end.
As Sandy Metz is used to say « Nothing is Something »[0]
0: https://youtu.be/OMPfEXIlTVE?si=qmizH1OvqV7eLKNK
> This is highly related to the "Null Object Pattern", but I thought I would explain it from the perspective of functions.
Since that function will return a list, an empty List might work.
The "maybe" style has the inconsistency embedded in the type system; it's impossible to have an invocation to getFriends and then not handle the resulting possibility of not being logged in.
Shifting it up to the caller just means that you're going to have to remember to ensure the user is logged in before calling getFriends otherwise you'll get some kind of error, which might give you more control, but now there's no guarantee in the type system that you've handled the case where the user isn't logged in.
Writing ifs everywhere to handle failure conditions might be a bit of a pain, but that's more of a failing of the language than the style.
* Monads naturally arise out of many problems in programming.
* But I don't want my language to support monads.
* So here's something you can do to stay in denial about how much you need monads.
At least this example only involves writing hard-to-analyse code and doesn't lead to you trying to invent green threads.
I’m not wedded to stuff like monadic state, I think that might be a bridge too far for regular programming (and besides which, it doesn’t really generalise anyway) but that still leaves a large family of issues that we’re all aware of but trying to dodge.
Two remarks:
• the render function is omitted, this pattern as a huge impact on application behaviors, if not for display-as-you-load issues, on DOM hidden state (things like focus, animations, etc…) for web apps.
• App's do have a global state, with self-consistency, scattering it in a mixed match of loading cache and self contained components just make it hard to work with. I think it's better to have a centralized upper level parent component that manage the transitional initialization states and consistency, not necessarily for the whole app, but at least for the whole displayed UI content.
(mostly waiting for this in JS and Go)
[1]: https://doc.rust-lang.org/reference/expressions/operator-exp...
Given these two function signatures:
This code won't compile, because `getUser()` cannot return a `None`: But this code will compile: (rust playground link: https://play.rust-lang.org/?version=stable&mode=debug&editio...)I'm all for a perfy shortcut / early return but this maybe just seems like an abstraction on a non-issue.
It is false that getUser being a “maybe function” forces the other functions like getFriends to be maybe functions. Don’t let them take null in their arguments. Force the caller to deal with the null when it is returned by getUser.
Also, you have to deal with developer mistakes and what happens when they call incorrectly. This can be something as simple as getting the first element of a collection. What happens when the collection is empty? You can adopt the C++ approach of “undefined behavior” but it turns out to be dangerous.
Monads provide a nice disciplined way to dealing with this and composing together functions that can potentially fail.
Thankfully, newer languages such as providing support for monads and older languages are evolving features/libraries for monadic error handling.
There is only one safe(ish) way to deal with programmer errors: crash. Hopefully loudly and early enough so it gets discovered in testing.
Predicting every possible failure reason for a function is impossible. Every function is a maybe function.
In rust for example, this is trivially handled with the questionmark postfix operator — which is just sugar for match — whereas in languages like JS and Java, stacking Optionals and so on can be rather painful as all this sugar is done manually.
But it did do something, it checked if the user logged was logged in first.
Procedures should do something. Functions should return something.
https://en.wikipedia.org/wiki/Command%E2%80%93query_separati...
By the way, I have never understood the practice of using a verb in the name of a (pure) function; naming the function after its result using a noun or adjective phrase makes much more sense.