A let binding creates a local definition by associating an identifier with an expression or function definition. The bound name represents immutable data, not a mutable variable. In an expression, in closes the binding and marks the expression where the name is available. section 3.4.1
Reuse an intermediate result
Inside do notation, bind a computed value and use it in the next action: main = do; line <- getLine; let upperCaseLine = map toUpper line; putStrLn upperCaseLine. The same idea works in ordinary expressions: let x = 1 + 1 in x + 1 :: Int evaluates to 3. section 3.4.1
Use let bindings to:
- Name repeated or complicated expressions, making nested code easier to read. section 3.4.1
- Define local functions whose arguments can include values from an enclosing function. section 3.4.1
- Hide auxiliary recursion parameters such as a line counter from the public function interface. section 4.1.2
- Break a larger transformation into named declarative steps that can reference one another. section 4.3.4
Listing 4.2 Function structure with a locally defined function
numberAllLines :: [String] -> NumberedLines
numberAllLines lines =
let go :: Int -> [String] -> NumberedLines #1
go counter lines = ...
in go 1 lines #2
A local go function accepts the hidden counter, while the outer function starts recursion with go 1 lines. section 4.1.2
Compose a readable transformation
prettyNumberedLines binds numbers and text, converts Maybe Int values to strings, finds the maximum width, pads each number, and combines the results. Each name exposes one computation while the final expression assembles the output. section 4.3.4

