Getting Started with Haskell and Functional Programming
Haskell programs are built from typed functions and immutable values, with input and output kept explicit in IO actions.
Haskell separates ordinary calculations from effects. A function receives values and returns a value. It does not update a variable, print to the terminal, or depend on hidden state unless its type says so.
input → pure function → output
This makes the small parts of a program easy to read in isolation. The type of each function describes the values it accepts and the value it produces.
GHC, GHCi, and Cabal
GHC is the Glasgow Haskell Compiler. GHCi is its interactive environment: it evaluates expressions, loads modules, and helps inspect types while a program is taking shape. Cabal describes and builds multi-file Haskell projects.
GHCup installs and manages the standard toolchain: GHC, GHCi, Cabal, Stack, and the Haskell Language Server. Once it is installed, ghci starts a prompt:
$ ghci
ghci> 2 + 3
5
ghci> :t not
not :: Bool -> Bool
:t asks GHCi for an expression’s type. Bool -> Bool means that not takes a boolean and returns a boolean. The arrow points from input to output.
Values Do Not Change
Haskell binds names to values with =. A binding is not a mutable storage location.
radius = 8
area = pi * radius ^ 2
radius remains 8. A later calculation can create a new value, but it cannot overwrite that binding. This removes a large category of questions about when a value changed and which part of a program changed it.
Local bindings use let:
circleArea r =
let diameter = r * 2
in pi * (diameter / 2) ^ 2
The result of the let block is the expression after in. The indentation is part of Haskell’s syntax: definitions aligned at the same level belong together.
Functions and Types
A function definition places its arguments after its name. Function application uses spaces instead of parentheses and commas.
double x = x * 2
add x y = x + y
double :: Num a => a -> a
add :: Num a => a -> a -> a
double works for numeric types, not only Int. add takes one argument and returns another function that waits for the second argument. This is called currying.
addTen = add 10
result = addTen 5
result is 15. Passing functions around in this form is ordinary Haskell, not a special feature added later.
Lists and Recursion
Lists are linked sequences of values with one element type.
scores = [12, 19, 7, 25]
names = ["Ada", "Grace", "Edsger"]
[] is the empty list. : adds one value to the front of an existing list. Pattern matching separates those two cases:
sumList [] = 0
sumList (first : rest) = first + sumList rest
The first equation handles the base case. The second takes the first value and calls sumList on what remains. No counter or changing accumulator is required; each call returns a value to the previous call.
For everyday transformations, Prelude functions usually replace explicit recursion:
activeNames users =
map name (filter active users)
filter keeps the values meeting a predicate. map transforms every value that remains. The data moves through a pipeline without modifying the original list.
Data Types Make Invalid States Visible
Maybe represents a value that might be absent. It has exactly two forms: Just value and Nothing.
safeHead [] = Nothing
safeHead (first : _) = Just first
The type can be written explicitly:
safeHead :: [a] -> Maybe a
The caller cannot treat the result as an ordinary value without handling both possibilities.
labelFirst items = case safeHead items of
Just item -> "First: " ++ item
Nothing -> "No items"
This is algebraic data in practice. The type names the possible states; pattern matching makes the handling of each state visible.
Effects Live in IO
Printing, reading input, files, randomness, and network requests interact with the world. Haskell represents those operations with IO values rather than letting a pure function perform them silently.
main :: IO ()
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello, " ++ name)
main is an action the runtime performs. putStrLn has an effect and produces no useful value, so its result type is IO (). getLine produces an input action; <- extracts its resulting String inside the do block.
The do block sequences actions. The pure string expression "Hello, " ++ name stays a normal calculation inside it. Effects remain visible at the boundary instead of being mixed into every function.
A Small Complete Program
A file named Main.hs runs with runghc Main.hs. In GHCi, :load Main loads the module and main runs the action.
module Main where
isEven n = n `mod` 2 == 0
evenSquares numbers =
map square (filter isEven numbers)
where
square n = n * n
main :: IO ()
main = do
let numbers = [1..10]
print (evenSquares numbers)
The pure part is evenSquares: it filters the even numbers, squares them, and returns a new list. main supplies the input list and prints the result. Running the program produces [4,16,36,64,100].
The next useful pieces are tuples, custom data declarations, Either for errors, and type classes. The central structure remains the same: model values with types, transform them with functions, and keep effects explicit.