Haskell gives you many ways to perform binding and destructuring.
Here I'll discuss:
where
bindingslet
bindings- pattern guards
ViewPatterns
These are pretty similar and it can be hard to pick which one to use. Here's how I choose.
Three relevant differences:
- location
- scope
- value sensitivity
order: after the function definition scope: same as the top of the function definition value sensitivity: no
Super when: you have local logic that is called multiple times local to your function.
foo x = bar * (bar + 1)
where
bar = x * 3
This abstraction keeps your top-level flow tidy.
order: above usage in a block scope: has access to preceding bindings in block value sensitivity: no
Super when: you want to close over a variable in a do block.
foo = do
c <- getC
let bar x = …
pure $ bar 1 + bar 2
A where
statement would require an extra parameter
foo x = do
c <- getC
pure $ bar 1 c + bar 2 c
where
bar x c' = …
scope: all parameters order: at function definition value sensitivity: yes
Super when: you have boolean logic that is applied to multiple parameters.
foo x y
| x < y = LessThan
| x > y = GreaterThan
| otherwise = EqualTo
order: at parameter position scope: only has access to that parameter value sensitivity: no
Super when: you want to pattern match the output of a function over a parameter.
foo (validate -> Just x) = Correct x
foo _ = Invalid
Especially good if you don't want to call validate
at every call site explicitly.
It is tidier than the corresponding pattern guard and shares top-level exhaustiveness checking:
foo x
| Just x <- validate = Correct x
| otherwise = Invalid
The corresponding case statement requires an additional binding:
foo x =
case validate x of
Just x' -> Correct x'
_ -> Nothing
They also support nesting:
foo (validate -> Just (validate -> Just x)) = SoValidated
Default to where
, use let
to close over sequential scope, use pattern guards for value filtering over parameters, and use viewpatterns to immediately transform and destructure a parameter.