Skip to content

Instantly share code, notes, and snippets.

@matthew-piziak
Created June 10, 2024 23:23
Show Gist options
  • Save matthew-piziak/1fad2d8861d0d5486132949dce18375a to your computer and use it in GitHub Desktop.
Save matthew-piziak/1fad2d8861d0d5486132949dce18375a to your computer and use it in GitHub Desktop.
Choosing a Haskell Binding

Haskell gives you many ways to perform binding and destructuring.

Here I'll discuss:

  • where bindings
  • let 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

Where

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.

Let

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' = …

Pattern Guards

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

ViewPatterns

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

Summary

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment