Skip to content
let, where, and the $ operator
step 1/7

Reading — step 1 of 7

Learn

~3 min readCustom Types

Three small but pervasive bits of Haskell syntax that polish your code: let, where, and $. LYAH chapter 5 covers these as foundational.

let — local bindings

areaCircle :: Double -> Double
areaCircle r =
    let pi' = 3.14159
        squared = r * r
    in pi' * squared
  • let X in Y — define bindings X, use them in Y
  • Multiple bindings indented to the same column
  • in is required for let-as-expression

In do blocks (next lesson on IO), let doesn't need in:

main = do
    let name = "Ada"
    let age = 36
    putStrLn (name ++ " is " ++ show age)

where — bottom-up bindings

areaCircle :: Double -> Double
areaCircle r = pi' * squared
  where
    pi'     = 3.14159
    squared = r * r

Same meaning, different style:

  • let — top-down: define then use
  • where — bottom-up: use then define

Idiomatic Haskell prefers where for definition-style code. The main expression reads at the top; helpers below.

bmi :: Double -> Double -> String
bmi weight height
    | bmi < 18.5 = "underweight"
    | bmi < 25.0 = "normal"
    | bmi < 30.0 = "overweight"
    | otherwise  = "obese"
  where
    bmi = weight / (height * height)

Where clauses can be shared across guards. let bindings can't be (each let is per-equation).

$ — function application without parens

The dollar sign is just regular function application — but with the LOWEST precedence:

putStrLn (show (1 + 2))     -- noisy parens
putStrLn $ show $ 1 + 2     -- cleaner

$ is right-associative, low-precedence — basically: "evaluate everything to my right as one big expression, then apply the function on the left."

Without $:

f (g (h x))

With $:

f $ g $ h x

Slightly cleaner for chains of single-arg applications.

. — function composition

f . g = \x -> f (g x)

-- Use:
length . filter even . map (* 2) $ [1, 2, 3, 4, 5]
-- map *2: [2,4,6,8,10]; filter even: [2,4,6,8,10]; length: 5

. is composition — (f . g) x = f (g x). Builds pipelines from right to left.

Compare to $:

  • $ — apply function to argument
  • . — compose functions

Common idiom:

result = function . step1 . step2 . step3 $ input

Reads: "input through step3, then step2, then step1, then function." The . sequence is the pipeline; $ applies it to the input.

let vs where — choosing

  • let — when the binding is local to ONE expression, especially in do blocks and lambdas
  • where — when the binding belongs to a top-level function, especially with guards
-- let — fine for short expressions:
f x = let y = expensive x in y * y + y

-- where — better for multi-line functions:
f x = result
  where
    intermediate = step1 x
    moreWork     = step2 intermediate
    result       = combine moreWork

Common mistakes

  • Forgetting in after a let expression (outside of do) — syntax error.
  • Indenting let/where bindings inconsistently — Haskell is whitespace-sensitive; bindings must align.
  • Confusing $ with .$ applies; . composes. f $ x is f x. f . x only works if x is also a function.
  • $ chains that are hard to readf $ g $ h $ i $ x works but parens or . are sometimes clearer.
  • Using where inside a lambda — not allowed. Use let or extract a top-level function.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…