Part 5: Haskell
In this final part, we explore Haskell, a purely functional programming language that represents a paradigm shift
from the imperative languages covered so far. Named after the logician Haskell Curry, Haskell was created in
1990 by an academic committee aiming to unify functional programming research into a single language. Haskell's
laziness, strong static type system, purity, and mathematical elegance make it unlike any mainstream language.
Learning Haskell will make you a better programmer in all languages by teaching you to think in terms of
transformations and composition rather than steps and mutation.
5.1 Installing Haskell
The recommended way to install Haskell is GHCup, which manages GHC (the Glasgow Haskell Compiler), cabal
(a build tool), and stack (an alternative build tool). GHC is the flagship Haskell compiler. GHCi is the interactive
REPL. Haskell source files use the .hs extension. Haskell has a REPL for experimentation, which is invaluable for
learning.
# Install GHCup (Linux/macOS)
$ curl --proto '=https' --tlsv1.2 -sSf [Link] | sh
# Verify tools
$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.6.3
$ ghci --version
GHCi, version 9.6.3
$ cabal --version
cabal-install version [Link]
# Run the REPL
$ ghci
GHCi, version 9.6.3: [Link]
Prelude> 2 + 2
4
Prelude> putStrLn "Hello, Haskell!"
Hello, Haskell!
Prelude> :quit
# Compile and run a program
$ ghc [Link] -o hello
$ ./hello
# Run a script without compiling (runghc)
$ runghc [Link]
5.2 Hello World
Haskell syntax is minimal and declaration-oriented. Unlike most languages, indentation matters (layout rule). There
are no semicolons or braces. The main function is the entry point. putStrLn prints a string followed by a newline.
The do notation sequences I/O actions.
-- [Link]
Page 1
Part 5: Haskell
main :: IO ()
main = putStrLn "Hello, World!"
-- With do notation (for multiple actions)
main2 :: IO ()
main2 = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
-- Functions are defined by pattern matching and equations
double :: Int -> Int
double x = x * 2
-- Multiple equations (evaluated top to bottom)
fizzbuzz :: Int -> String
fizzbuzz n
| n `mod` 15 == 0 = "FizzBuzz"
| n `mod` 3 == 0 = "Fizz"
| n `mod` 5 == 0 = "Buzz"
| otherwise = show n
-- Run: ghc [Link] -o hello && ./hello
5.3 Types and Type Signatures
Haskell has a strong, static type system with full type inference. Type signatures use :: (read as has type).
Function types use -> (read as maps to). A function Int -> Int -> Int takes two Ints and returns an Int. Haskell is
statically typed, so type errors are caught at compile time. The type system is the heart of Haskell's safety and
expressiveness.
-- Type signatures (optional but recommended)
add :: Int -> Int -> Int
add x y = x + y
-- Haskell infers types automatically
multiply x y = x * y -- inferred: Num a => a -> a -> a
-- Type variables (polymorphism)
identity :: a -> a -- works for any type
identity x = x
-- Typeclass constraints
-- Num is a typeclass (like an interface)
square :: Num a => a -> a -- a must be a Numsquare x = x * x
-- Common basic types
age :: Int -- fixed-size integer
big :: Integer -- arbitrary-precision integer
pi_approx :: Double -- double-precision float
Page 2
Part 5: Haskell
name :: String -- [Char] (list of characters)
letter :: Char -- single character
flag :: Bool -- True or False
-- Checking types in GHCi
-- :t 'a' => 'a' :: Char
-- :t True => True :: Bool
-- :t "hello" => "hello" :: String
-- :t length => length :: [a] -> Int
-- :t (+) => (+) :: Num a => a -> a -> a
-- Type aliases
type Point = (Double, Double)
origin :: Point
origin = (0.0, 0.0)
-- Defining new types with data
data Color = Red | Green | Blue -- sum type (enum-like)
data Shape
= Circle Double -- one constructor with a Double
| Rectangle Double Double -- two Doubles (width, height)
| Triangle Double Double Double
-- Record syntax
data Person = Person
{ personName :: String
, personAge :: Int
} deriving (Show, Eq)
alice :: Person
alice = Person { personName = "Alice", personAge = 30 }
-- Access: personName alice => "Alice"
5.4 Functions and Pattern Matching
Haskell functions are pure: given the same input, they always return the same output, with no side effects. Pattern
matching decomposes data structures and is the primary way to make decisions in Haskell. Guards (|) provide
conditional branching. Case expressions provide pattern matching inline. Functions are curried by default: a
function taking two arguments is actually a function taking one argument that returns a function taking the second.
-- Pattern matching on function arguments
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
-- Pattern matching on tuples
addPair :: (Int, Int) -> Int
addPair (x, y) = x + y
-- Pattern matching on lists
Page 3
Part 5: Haskell
myLength :: [a] -> Int
myLength [] = 0
myLength (_:xs) = 1 + myLength xs
-- Guards (conditional branching)
grade :: Int -> String
grade n
| n >= 90 = "A"
| n >= 80 = "B"
| n >= 70 = "C"
| otherwise = "F"
-- Where clauses (local bindings)
quadratic :: Double -> Double -> Double -> Double -> Double
quadratic a b c x = a * x^2 + b * x + c
where
-- Local definitions
_discriminant = b^2 - 4*a*c
-- Let expressions (local bindings, expression-level)
area :: Double -> Double
area r = let pi_val = 3.14159 in pi_val * r^2
-- Case expressions
describe :: [a] -> String
describe xs = case xs of
[] -> "empty"
[_] -> "one element"
[_,_] -> "two elements"
_ -> "many elements"
-- Currying and partial application
add :: Int -> Int -> Int
add x y = x + y
add5 :: Int -> Int
add5 = add 5 -- partial application: add5 3 = 8
-- Sectioning (partial application of operators)
multiplyBy3 :: Int -> Int
multiplyBy3 = (3 *) -- multiplyBy3 4 = 12
divideBy2 :: Int -> Int
divideBy2 = (`div` 2) -- divideBy2 10 = 5
-- Function composition with .
-- (f . g) x = f (g x)
addOneThenDouble :: Int -> Int
addOneThenDouble = (* 2) . (+ 1) -- (add 1 then multiply by 2)
-- addOneThenDouble 3 = (*2) ((+1) 3) = (*2) 4 = 8
Page 4
Part 5: Haskell
5.5 Lists and List Comprehensions
Lists in Haskell are linked lists (singly linked). They are homogeneous (all elements same type). List
comprehensions provide a concise, mathematical syntax for building lists, similar to set-builder notation. The cons
operator (:) prepends an element. The ++ operator concatenates lists. Because Haskell is lazy, lists can be infinite.
-- Creating lists
nums = [1, 2, 3, 4, 5]
evens = [2, 4..20] -- range: [2,4,6,...,20]
alphabet = ['a'..'z'] -- "abcdefghijklmnopqrstuvwxyz"
ones = repeat 1 -- infinite list of 1s
-- List operations
head [1,2,3] -- 1 (first element)
tail [1,2,3] -- [2,3] (all but first)
last [1,2,3] -- 3
init [1,2,3] -- [1,2] (all but last)
length [1,2,3] -- 3
null [] -- True (is empty?)
reverse [1,2,3] -- [3,2,1]
take 3 [1..] -- [1,2,3] (take from infinite)
drop 2 [1..5] -- [3,4,5]
elementAt = [1,2,3] !! 1 -- 2 (0-indexed)
-- Concatenation
[1,2] ++ [3,4] -- [1,2,3,4]
0 : [1,2,3] -- [0,1,2,3] (cons)
-- List comprehensions (set-builder notation)
-- [expression | generator, qualifier, ...]
squares = [x^2 | x <- [1..5]] -- [1,4,9,16,25]
evensUpTo20 = [x | x <- [1..20], even x] -- [2,4,...,20]
-- Multiple generators (cartesian product)
pairs = [(x,y) | x <- [1,2], y <- [3,4]] -- [(1,3),(1,4),(2,3),(2,4)]
-- With conditions
fizzbuzzList = [if n `mod` 15 == 0 then "FizzBuzz"
else if n `mod` 3 == 0 then "Fizz"
else if n `mod` 5 == 0 then "Buzz"
else show n | n <- [1..20]]
-- Infinite list with take (laziness)
first10Squares = take 10 [x^2 | x <- [1..]]
-- [1,4,9,16,25,36,49,64,81,100]
-- Strings are lists of characters
-- "hello" is syntactic sugar for ['h','e','l','l','o']
upperString :: String -> String
upperString s = [toUpper c | c <- s]
Page 5
Part 5: Haskell
where toUpper c = if c >= 'a' && c <= 'z'
then toEnum (fromEnum c - 32)
else c
-- zip (combine two lists into pairs)
zipped = zip [1,2,3] ['a','b','c'] -- [(1,'a'),(2,'b'),(3,'c')]
-- map and filter (higher-order functions)
doubled = map (*2) [1..5] -- [2,4,6,8,10]
evensOnly = filter even [1..10] -- [2,4,6,8,10]
-- fold (reduce)
sumOf = foldr (+) 0 [1..5] -- 15 (right fold)
productOf = foldl (*) 1 [1..5] -- 120 (left fold)
5.6 Higher-Order Functions
Haskell treats functions as first-class values. Higher-order functions take functions as arguments or return
functions. The most important are map (apply a function to each element), filter (select elements), foldr/foldl
(reduce), and zipWith (combine two lists element-wise). Function composition with the . operator lets you build
complex functions from simple ones.
-- map: apply a function to each element
squaresList = map (^2) [1..5] -- [1,4,9,16,25]
-- filter: keep elements satisfying a predicate
pos = filter (> 0) [-2, -1, 0, 1, 2] -- [1,2]
-- foldr (right fold) and foldl (left fold)
sumList = foldr (+) 0 [1..5] -- 15
productList = foldl (*) 1 [1..5] -- 120
-- foldr structure: f 1 (f 2 (f 3 (f 4 (f 5 0))))
-- foldl structure: f (f (f (f (f 0 1) 2) 3) 4) 5
-- zipWith: combine two lists with a function
sums = zipWith (+) [1,2,3] [10,20,30] -- [11,22,33]
products = zipWith (*) [1,2,3] [4,5,6] -- [4,10,18]
-- Function composition (.)
-- (f . g) x = f (g x)
-- Useful for chaining transformations
process :: [Int] -> [Int]
process = map (+1) . filter even . take 10
-- Take 10, filter evens, then add 1 to each
-- flip: swap argument order of a function
myDiv :: Int -> Int -> Int
myDiv = div -- div 10 2 = 5
flippedDiv = flip div -- flippedDiv 2 10 = 5
Page 6
Part 5: Haskell
-- curry and uncurry
-- curry turns a tuple-taking function into a curried one
addTuple :: (Int, Int) -> Int
addTuple (x, y) = x + y
addCurried :: Int -> Int -> Int
addCurried = curry addTuple
-- uncurry goes the other way
addTupled :: (Int, Int) -> Int
addTupled = uncurry (+)
-- $ operator (lowest precedence, applies function)
-- Instead of: putStrLn (show (2 + 2))
-- You can write: putStrLn $ show $ 2 + 2
result = putStrLn $ show $ 2 + 2
-- Lambda (anonymous functions)
increment = map (\x -> x + 1) [1,2,3] -- [2,3,4]
-- Point-free style (no explicit arguments)
evenCount :: [Int] -> Int
evenCount = length . filter even
-- Instead of: evenCount xs = length (filter even xs)
-- Application: quicksort (classic Haskell elegance)
qsort :: [Int] -> [Int]
qsort [] = []
qsort (p:xs) =
qsort smaller ++ [p] ++ qsort larger
where
smaller = [x | x <- xs, x < p]
larger = [x | x <- xs, x >= p]
-- qsort [3,1,4,1,5,9,2,6] => [1,1,2,3,4,5,6,9]
5.7 Typeclasses
Typeclasses are Haskell's interface mechanism. They define a set of functions that types can implement. The Eq
typeclass provides == and /=. Ord provides ordering (compare, <, >). Show provides string representation (show).
Read parses from strings. Num, Fractional, Integral are numeric typeclasses. Haskell can automatically derive Eq,
Ord, Show, Read, and others with the deriving clause.
-- Defining a typeclass
class MyShow a where
myShow :: a -> String
-- Instance for Int
instance MyShow Int where
myShow n = "Int: " ++ show n
Page 7
Part 5: Haskell
-- Instance for a custom type
data Temperature = Celsius Double | Fahrenheit Double
instance MyShow Temperature where
myShow (Celsius c) = show c ++ " C"
myShow (Fahrenheit f) = show f ++ " F"
-- Using typeclass constraints
prettyPrint :: MyShow a => a -> IO ()
prettyPrint x = putStrLn (myShow x)
-- Deriving common typeclasses
data Point2D = Point2D Double Double
deriving (Show, Eq, Ord)
-- Eq: == and /=
Point2D 1 2 == Point2D 1 2 -- True
-- Ord: compare, <, >, <=, >=
Point2D 1 2 < Point2D 3 4 -- True
-- Show: show (string representation)
show (Point2D 1 2) -- "Point2D 1.0 2.0"
-- Standard typeclass hierarchy (simplified):
-- Eq => ==, /=
-- Ord => compare, <, > (implies Eq)
-- Show => show
-- Read => read
-- Num => +, -, *, fromInteger (Int, Integer, Double...)
-- Enum => succ, pred, ranges [1..10]
-- Bounded => minBound, maxBound
-- Typeclass default methods and extensions
class Drawable a where
draw :: a -> String
area :: a -> Double -- no default, must implement
name :: a -> String -- has a default
name _ = "Unknown Shape"
data Circle = Circle Double
instance Drawable Circle where
draw (Circle r) = "Circle of radius " ++ show r
area (Circle r) = pi * r^2
-- name uses default: "Unknown Shape"
-- Multiple typeclass constraints
showArea :: (Drawable a, Show a) => a -> String
showArea s = draw s ++ " has area " ++ show (area s)
Page 8
Part 5: Haskell
5.8 Monads and I/O
Monads are Haskell's way to handle side effects in a pure language. IO is a monad. A value of type IO String
represents an I/O action that, when run, produces a String. The do notation is syntactic sugar for monadic
operations. The bind operator (>>=) chains monadic actions. The return function wraps a pure value in a monad.
Monads are fundamental to Haskell: Maybe (for optional values), List (for nondeterminism), IO (for input/output),
and many more are all monads.
-- IO is a monad: IO actions produce values when executed
main :: IO ()
main = do
putStrLn "Enter your name:" -- IO () (no value)
name <- getLine -- IO String, bind to name
putStrLn ("Hello, " ++ name)
-- do notation is sugar for bind (>>=)
main' :: IO ()
main' =
putStrLn "Enter your name:" >>
getLine >>= \name ->
putStrLn ("Hello, " ++ name)
-- return wraps a pure value in a monad
greet :: IO String
greet = do
putStrLn "What is your name?"
name <- getLine
return ("Hello, " ++ name)
-- Maybe monad (optional values, no null)
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv x y = Just (x `div` y)
-- Chain Maybe computations with do
calculate :: Maybe Int
calculate = do
a <- safeDiv 100 2 -- Just 50
b <- safeDiv a 0 -- Nothing (short-circuits!)
return (b + 1) -- never reached
-- Result: Nothing
-- >>= with Maybe: stops on Nothing
-- Just 50 >>= (\a -> safeDiv a 0) => Nothing
-- List monad (nondeterminism / multiple results)
-- [1,2] >>= (\x -> [x, x*10]) => [1,10,2,20]
-- Generic monad operations
-- return :: a -> m a (wrap value in monad)
Page 9
Part 5: Haskell
-- >>= :: m a -> (a -> m b) -> m b (bind/chain)
-- >> :: m a -> m b -> m b (sequence, ignore result)
-- fmap (functor: map over monad)
-- fmap (+1) (Just 5) => Just 6
-- fmap (+1) Nothing => Nothing
-- The <- in do notation extracts the value from the monad
-- Each line in a do block is a monadic action
-- A simple IO program: read a file and count lines
import [Link]
countLines :: FilePath -> IO Int
countLines path = do
contents <- readFile path
return (length (lines contents))
-- IO actions are values; they only execute when run by main
5.9 Laziness and Infinite Data Structures
Haskell is lazy by default: expressions are not evaluated until their results are needed. This enables powerful
techniques like infinite data structures. You can define an infinite list and take only the part you need. Laziness can
also cause space leaks if you are not careful, but it enables elegant, compositional code that would be impossible
in strict languages.
-- Infinite lists (only computed as needed)
naturals = [1..] -- infinite: [1,2,3,4,...]
ones = repeat 1 -- infinite: [1,1,1,1,...
cycle123 = cycle [1,2,3] -- infinite: [1,2,3,1,2,3,...
-- Take from infinite lists (works due to laziness)
take 5 naturals -- [1,2,3,4,5]
take 3 (repeat 'a') -- "aaa"
-- Fibonacci (infinite, lazy)
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- Take first 10: take 10 fibs
-- [0,1,1,2,3,5,8,13,21,34]
-- Primes (sieve of Eratosthenes, lazy)
primes :: [Integer]
primes = sieve [2..]
where
sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
-- take 10 primes => [2,3,5,7,11,13,17,19,23,29]
Page 10
Part 5: Haskell
-- Laziness means you define data, you do not build it eagerly
-- This allows elegant separation of generation and consumption
-- Infinite tree (each node has infinite children)
data Tree a = Node a [Tree a]
tree :: Tree Int
tree = Node 0 (map child [1..])
where child n = Node n (map grandchild [1..])
grandchild n = Node (n * 100) []
-- seq forces evaluation (rare, for performance)
-- bang patterns (!) force strictness
-- strictness annotations: data T = T !Int
-- Laziness gotcha: space leaks
-- sum [1..n] can use O(n) space if not optimized
-- Use foldl' (strict fold) for accumulation:
import [Link] (foldl')
strictSum = foldl' (+) 0 [1..1000000] -- constant space
5.10 Comparison and Final Thoughts
We have now covered five diverse languages. Assembly is the lowest level, giving direct hardware control.
JavaScript dominates web and increasingly server-side development with its async event loop. PHP is the
workhorse of server-side web, powering most of the internet's content management systems. Rust provides
C-level performance with compile-time memory safety guarantees through its ownership system. Haskell
represents the pure functional paradigm, where laziness, strong types, and monads enable a radically different
approach to programming.
Language | Paradigm | Typing | Memory | Use Case
-----------+-------------+-----------+---------------+-------------------
Assembly | Imperative | None | Manual | Systems, kernels
JavaScript | Multi | Dynamic | GC | Web, servers
PHP | Imper/OOP | Dynamic* | GC | Web servers
Rust | Imper/OOP | Static | Ownership | Systems, safe
Haskell | Functional | Static | GC (lazy) | Research, finance
* PHP has optional type declarations since PHP 7
Assembly: ultimate control, steepest learning curve
JavaScript: ubiquitous, flexible, async-first
PHP: easy deployment, web-focused, huge ecosystem
Rust: safety + speed, modern systems programming
Haskell: pure FP, mathematical, mind-expanding
Key insights from each:
- Assembly: understand the hardware your code runs on
- JavaScript: functions are values, async is fundamental
- PHP: web deployment can be simple and practical
Page 11
Part 5: Haskell
- Rust: compile-time guarantees prevent runtime bugs
- Haskell: think in transformations, not mutations
Learning multiple paradigms (imperative, OOP, functional)
makes you a more versatile and thoughtful programmer.
Each language embodies a philosophy:
Assembly: control
JavaScript: flexibility
PHP: pragmatism
Rust: safety
Haskell: elegance
Thank you for completing this five-part tutorial series
covering Assembly, JavaScript, PHP, Rust, and Haskell.
Happy learning and happy coding!
5.11 Where to Go Next
For each language, here are recommended next steps. Assembly: read The Art of Assembly Language by Randall
Hyde, study reverse engineering tools like Ghidra, and try writing a simple OS bootloader. JavaScript: build a
full-stack web app with a framework like React or Vue, learn TypeScript for static types, and explore [Link] for
backend. PHP: learn Laravel or Symfony for modern web development, study the PSR standards, and build a
REST API. Rust: read The Rust Programming Language (the book), build a CLI tool or web server with
Actix/Axum, and contribute to the Rust ecosystem. Haskell: read Learn You a Haskell for Great Good, study Real
World Haskell, and try a project with a web framework like Servant or Yesod. The most important thing is to build
real projects in each language, as theory alone is not enough to master any language.
Page 12