{-❤ LANGUAGE LambdaCase ❤-}
Of all the many, many Haskell language extensions supported by GHC,
LambdaCase
is one of my favorites. It has essentially no downside,
does not conflict with any existing code, and introduces a single,
simple, useful new construct to the language.
LambdaCase
?At a superficial level, LambdaCase
merely introduces one tiny
bit of syntactic sugar: wherever you would have written
\x -> case x of
pattern1 -> value1
pattern2 -> value2
...
you can now write
\case
pattern1 -> value1
pattern2 -> value2
...
That's it!
That may seem a little too cute for its own good. Yet LambdaCase
is one of the most
popular GHC extensions---in the 2017 Haskell survey, it was effectively in a multi-way
tie for the third-most popular extension. And of the top 10 most popular extensions, LambdaCase
is the only one that merely adds syntactic sugar!
What gives? Why is humble LambdaCase
so popular? To answer this question,
let's see how it gets used in practice.
\case
In practice, LambdaCase
gives an ergonomic solution to a small but common problem:
introducing an anonymous function defined by cases.
What do I mean by "defining by cases"? Haskell lets you define functions through pattern matching, using "declaration style":1
eitherToMaybe :: Either a b -> Maybe b
eitherToMaybe (Left _) = Nothing
eitherToMaybe (Right x) = Just x
The alternative is to define a function using "expression style":
eitherToMaybe :: Either a b -> Maybe b
eitherToMaybe e = case e of
Left _ -> Nothing
Right x -> Just x
Stylistically, this has a few advantages. First, we don't need to keep repeating the same function name over and over. This is especially true if the function we are defining also includes several other parameters that are the same for each definition. Second, the indentation helps you to quickly see where the definition begins and ends.
There is one drawback to the expression style that is worth pondering, however.
In declaration style, we did not need to name the parameter; we just pattern-matched
on it directly: eitherToMaybe (Right x) = ...
.
In fact, if we had really wanted to, we could have used @
-patterns to name the argument:
eitherToMaybe e@(Right x) = Just x
But... why? The argument's shape is the important thing, not its name!
In the expression style, on the other hand, we are forced to name the argument in order
to pipe it into the case
statement: eitherToMaybe e = case e of ...
. In fact,
my dumb choice of name e
shows how obnoxious it is to select a good name here.
Maybe input
? x
? theExpressionI'mAboutToDestructure
? I guess there is a reason they say that
naming things is one of the hardest problems in computer science.
On the other hand, \case
offers a simple way to define a function expression-style,
without needing to introduce names for your plumbing. In expression style using LambdaCase
,
the running example becomes:
eitherToMaybe = \case
Left _ -> Nothing
Right x -> Just x
Consider this little snippet of code, that reads input from the user until they stop being indifferent:
data Decision = Yes | No | Meh
deriving Read
main = do
input <- readLn
case input of
Yes -> putStrLn "ok, sure!"
No -> putStrLn "fine, whatever."
Meh -> main
This code is slightly annoying, for the same reason that writing an
"expression style" definition using case .. of
is slightly annoying:
we're introducing a named variable in order to provide the logical plumbing
between readLn
's result and our case
statement. We had to come up with
a name for this plumbing, even though we'd really like to say to the reader
"don't pay too much attention to this variable, I'm just using it to carry
data from over here to over there."
Just as before, we can use \case
to eliminate the plumbing variable entirely:
main = readLn >>= \case
Yes -> putStrLn "ok, sure!"
No -> putStrLn "fine, whatever."
Meh -> main
Now the flow of data becomes clear: read something, and do a case
analysis on
the result.
The reader is not left to wonder if a certain variable is important enough to spend time understanding. The plumbing is hidden behind the wall, where it belongs!
After a while, I noticed that when I was reaching for LambdaCase
, it was
often in a monadic context. That raises an interesting question: how often
in practice is LambdaCase
used only in support of this case-under-a-monadic-value idiom?
Luckily I keep a local copy of Hackage around, to quickly satisfy my curiosity about questions
like this! In my snapshot, I found about 7800 uses of \case
in total. These uses
are spread across in 1600 modules in 800 packages.
Digging around a bit with grep
, we can get an idea of how \case
is used in practice.
The uses seem to be classifiable into a handful of common, easily-identifiable idioms,
described here.
The most-common use of \case
is to define a function by cases, expression style.
This accounts for 4.0k (51%) of the uses in Hackage. If you think about it, this is a little
funny: \case
was added to allow for anonymous functions defined by cases, and what
do we do? Half the time, we immediately give the new function a name! But as we discussed
above, the real benefit here is that we can use expression-style definitions without
introducing a name for our plumbing variable.
Here is an example from hedgehog
:
renderLineDiff :: LineDiff -> String
renderLineDiff = \case
LineSame x ->
" " ++ x
LineRemoved x ->
"- " ++ x
LineAdded x ->
"+ " ++ x
Writing in declaration style would be noisy:
renderLineDiff (LineSame x) = " " ++ x
renderLineDiff (LineRemoved x) = "- " ++ x
renderLineDiff (LineAdded x) = "+ " ++ x
while writing with a case .. of
expression forces us to name the parameter.
We're either going to end up with a redundant name (e.g. lineDiff
) or
a dummy metasyntactic name (e.g. y
or foo
). Neither case really helps the
reader very much.
renderLineDiff lineDiff = case lineDiff of
LineSame x ->
" " ++ x
LineRemoved x ->
"- " ++ x
LineAdded x ->
"+ " ++ x
Can you really say lineDiff
is aiding the reader very significantly?
The next-most-common idiom is using >>= \case
to perform case analysis
on the result of a monadic expression. This idiom appears 2.2k times in Hackage.
In other words, nearly a third of the
real-world uses of \case
are actually using this case-inside-monadic-value
idiom!
The example from the midi-simple
package on Hackage shows a typical use of this
idiom, similar to our example above:
systemCommon :: Parser SystemCommon
systemCommon = peekWord8' >>= \case
0xF1 -> mtcQuarter
0xF2 -> songPosition
0xF3 -> songSelect
0xF6 -> tuneRequest
0xF7 -> eox
_ -> empty
The peekWord8
parser is used to inspect the next byte to parse, and
then the correct parser is run.
It isn't important to name the byte we peeked at. If anything, introducing a name to that byte will just disrupt the simple flow of this function with an irrelevant name!
This example is interesting because the alternatives are either to introduce
do
-notation just for a single binding:
systemCommon :: Parser SystemCommon
systemCommon = do
tag <- peekWord8'
case tag of
0xF1 -> mtcQuarter
0xF2 -> songPosition
0xF3 -> songSelect
0xF6 -> tuneRequest
0xF7 -> eox
_ -> empty
or keep the do
-free code but wrap the case
statement with a lambda
(and parentheses!):
systemCommon :: Parser SystemCommon
systemCommon = peekWord8' >>= (\tag -> case tag of
0xF1 -> mtcQuarter
0xF2 -> songPosition
0xF3 -> songSelect
0xF6 -> tuneRequest
0xF7 -> eox
_ -> empty)
What else is in there?
Another idiom is to traverse some data structure and use a \case
to decide what
to do at each element. This is getting a bit harder to accurately grep, but as a crude
approximation I searched for lines containing both $ \case
and for
, finding
42 instances.
This is from an event loop in the example code for hamilton
:
forM_ (processEvt e) $ \case
SEQuit -> do
killThread t
shutdown vty
exitSuccess
SEZoom s ->
modifyIORef opts $ \o -> o { soZoom = soZoom o * s }
SERate r ->
modifyIORef opts $ \o -> o { soRate = soRate o * r }
SEHist h ->
modifyIORef opts $ \o -> o { soHist = soHist o + h }
with
A similar idiom makes use of a with*
function for acquiring some resource, with
a \case
on the inside to select what to do with that resource.
There are 60 instances of this idiom in Hackage.
Here is an example from hpack
:
instance FromJSON BuildType where
parseJSON = withText "String" $ \case
"Simple" -> return Simple
"Configure" -> return Configure
"Make" -> return Make
"Custom" -> return Custom
_ -> fail "build-type must be one of: Simple, Configure, Make, Custom"
Finally, we find approximately 1k uses of \case
to introduce an anonymous function.
This example from bit-array
is typical:
-- |
-- Convert into a binary notation string.
--
-- >>> toString (BitArray (5 :: Int8))
-- "00000101"
toString :: (FiniteBits a) => BitArray a -> String
toString = fmap (\case True -> '1'; False -> '0') . reverse . toBoolList
Altogether, the idiomatic uses of LambdaCase
on Hackage fall into these categories:
case
function (13%)with
and for
(1%)with the remaining 7% of uses remaining hard to classify via a simple grep
.
It seems that the vast majority of \case
usage is to either get a less-noisy
and plumbing-free definition by cases, or to do case analysis on a monadic value!
Since the case-under-monadic-value idiom is so common, would it make sense to
add it directly to the language?2 The designers of Habit
thought so, adding a form case <- val of { pats }
to the language. In Haskell, it would mean
replacing code like this:
main = readLn >>= \case
Yes -> putStrLn "ok, sure!"
No -> putStrLn "fine, whatever."
Meh -> main
with something like this:
main = case <- readLn of
Yes -> putStrLn "ok, sure!"
No -> putStrLn "fine, whatever."
Meh -> main
or, if we don't mind introducing a new keyword, perhaps:
main = caseM readLn of
Yes -> putStrLn "ok, sure!"
No -> putStrLn "fine, whatever."
Meh -> main
Incidentally, caseM
only appears once on Hackage (in caldims
), in a module that
does not use LambdaCase
. So as far as identifiers go, caseM
may be rare enough that
it is worth re-purposing!
1: The distinction between "declaration style" and "expression style" was elaborated in the terrific retrospective A History of Haskell: Being Lazy with Class.
2: The idea of having special syntax to replace >>= \case
is
not new, of course. In fact, it already appears in the issue tracker that eventually led to LambdaCase
's adoption into GHC. If you have
20 minutes to spare, there are lots of interesting corners of design space explored on that
Trac page!