Non-Obvious Haskell Idiom: Guard-Sequence
Reading production Haskell code, we sometimes stumble over idioms that look confusing at first, but which show up frequently enough that they are worth learning. This is one of those examples, where we optionally return something with guard-sequence.
The guard–sequence idiom comes in three common shapes:
guard condition *> action
performs the side-effect object action if the condition is true, and otherwise fails.guard condition $> value
returns the value if the condition is true, otherwise returns a failure.1 Most often, this meansJust value
on success andNothing
on failure. But it could also be a plain value on success and an exception on failure. The compiler knows what to pick based on the type expected.value <$ guard condition
also returns the value if the condition is true, but with emphasis on the value rather than the condition.
Explanation
Sometimes we have a boolean check that decides whether the return value is a
failure or success. Some people create a function called ensure
to do this,
and it might be defined as
ensure :: Bool -> a -> Maybe a ensure p x = if p then Just x else Nothing
Here is an example of how it could be used to check if a person is of age, and
if so, return a ticket to an event for them. If they are a minor, it returns
Nothing
.
ensure (age >= 18) (ticket_for person)
I used to be surprised that the ensure
function did not exist in the standard
libraries, but there’s a good reason for this: we can phrase the same thing
using combinations of existing operators. The guard
function (available in
Control.Monad) combined with the functor-replace $>
operator (from
Data.Functor) allows us to write
guard (age >= 18) $> ticket_for person
This is a tiny bit longer to write, but uses only standard functions and
operators, which means it is more likely to be readable by someone else without
having to first look up what ensure
means in this specific context.
The way it works is that
guard
returnsNothing
if the condition is not met, andJust ()
if the condition is met; then- the
$>
operator is a no-op onNothing
values, but replaces whatever is inside aJust
value on the left with the value on the right.
This means in this case it acts a little like the &&
operator in JavaScript.
The JavaScript equivalent would be
age >= 18 && ticket_for(person)
The difference is, of course, that the Haskell version is more principled and doesn’t come with the confusing edge cases the JavaScript version does.
The Haskell version is also more clear in the presence of multiple conditions. We might write
guard (age >= 18 && age <= 24 || age >= 65) $> ticket_discount
where it is clear that the logical operators &&
and ||
work on the
condition, whereas the rest is about translating the boolean to failure or
success. In JavaScript, this code would be
(age >= 18 && age <= 24 || age >= 65) && ticket_discount
where the &&
operator is used both to combine conditions and short-circuit to
handle failure.
The Haskell version can also be flipped around to indicate emphasis on the discount rather than the condition:
ticket_discount <$ guard (age >= 18 && age <= 24 || age >= 65)
Here, the <$ guard
combination reads as a sort of if
.
Another reason the standard phrasing is superior to an ensure
function is that
it is more flexible. In these cases, we have returned a constant pure value, but
if we switch the functor-replace operator $>
with the applicative sequence
operator *>
the right-hand side can be something effectful instead, like a
parser. For example, a Swedish driver’s licence has a letter combination
indicating what types of vehicles a person is allowed to drive. Maybe we have
records where this field is completely absent for minors. We can then
conditionally parse that field based on age, with something like
guard (age >= 18) *> munch1 isLetter
This parser will fail for any age less than 18 (regardless of whether or not they have a licence), and parse the type of licence in all other cases (failing as usual if none exists).
In this case, the return value is no longer a Maybe String
but a Parser
String
, whose parsing automatically fails on underage records. This is because
the guard
and sequence operators are generic and work with any type that
supports some sort of failure, not just Maybe
.