← LOGBOOK LOG-445
COMPLETE · SOFTWARE ·
HASKELLFUNCTIONAL-PROGRAMMINGGHCTYPESRECURSION

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.

Sources