Introduction to Haskell
What Is Haskell?
Section titled “What Is Haskell?”Haskell is a purely functional, statically typed, lazily evaluated programming language. It was designed by a committee of researchers in the late 1980s to serve as a common language for research in functional programming, and has evolved into a practical language used in industry for systems that demand correctness, concurrency, and abstraction.
Unlike imperative languages where computation proceeds through mutable state and explicit sequencing, Haskell programs describe what to compute rather than how to compute it. Functions in Haskell are mathematical functions: given the same inputs, they always produce the same outputs and have no side effects.
Key characteristics of Haskell:
- Purely functional: No mutable variables, no side effects within function bodies
- Statically typed: Types are checked at compile time with powerful type inference
- Lazily evaluated: Expressions are evaluated only when their results are needed
- Type inference: The compiler deduces most types automatically via the Hindley-Milner algorithm
- Strongly typed: No implicit conversions; type safety is enforced at compile time
- Concurrent: Excellent support for parallel and concurrent programming via lightweight threads
History and Evolution
Section titled “History and Evolution”The Origins (1987—1990)
Section titled “The Origins (1987—1990)”In 1987, a meeting was held at FPCA (Functional Programming Languages and Computer Architecture) in Portland, Oregon, to consolidate the proliferation of lazy functional languages. There were more than a dozen such languages at the time, including Miranda, Lazy ML, Orwell, and Id. The committee aimed to create a single, open standard that could serve as a basis for research and education.
The resulting language was named after Haskell Curry, the mathematician who made foundational contributions to mathematical logic and combinatory logic (and the inspiration behind “currying”). The first version of the Haskell report was published in 1990.
Haskell 98 (1998)
Section titled “Haskell 98 (1998)”Haskell 98 was the first standardized version of the language. It defined a stable, minimal set of features intended for teaching and as a base for future extensions. Key features of Haskell 98:
- Standard libraries including Prelude, List, Char, IO, and Monad
- Type classes with no extensions
- Standard derivation for Eq, Ord, Enum, Bounded, Show, Read
- No multi-parameter type classes
- No functional dependencies
- No rank-N types
Haskell 98 provided a clean, well-defined foundation. Many textbooks and courses still teach Haskell 98 as a starting point before moving to extensions.
Haskell 2010
Section titled “Haskell 2010”The Haskell 2010 standard was the next official revision. It incorporated several widely-used extensions that had become de facto standards. Key additions:
- Foreign Function Interface (FFI): Standardized way to call C functions
- Hierarchical module names: Support for dotted module names like
Data.List - Pattern guards: Guards that use pattern matching
- No C guards: Removal of the C-specific guard extension
- Generalized algebraic data type syntax improvements: Minor syntactic cleanups
Modern Haskell (GHC Extensions)
Section titled “Modern Haskell (GHC Extensions)”The Glasgow Haskell Compiler (GHC) has become the de facto standard compiler and has driven the language”s evolution through extensions that are enabled via pragmas. Key GHC extensions include:
- GADTs (Generalized Algebraic Data Types): More expressive data type definitions
- TypeFamilies: Type-level functions for advanced abstraction
- DataKinds: Promoting data types to the kind level
- RankNTypes: Polymorphic function arguments and results
- TemplateHaskell: Metaprogramming via compile-time code generation
- OverloadedStrings: Automatic conversion of string literals
- ViewPatterns: Pattern matching with arbitrary view functions
Pure Functional Programming
Section titled “Pure Functional Programming”What Does “Pure” Mean?
Section titled “What Does “Pure” Mean?”A pure function has two essential properties:
- Referential transparency: The result of a function depends only on its arguments. The expression
f(x)can always be replaced by its result without changing the program’s behavior. - No side effects: The function does not modify any external state, perform I/O, or interact with the outside world.
-- Pure function: same input always produces same outputsquare :: Int -> Intsquare x = x * x
-- Pure function: depends only on its argumentsfactorial :: Integer -> Integerfactorial 0 = 1factorial n = n * factorial (n - 1)Referential Transparency
Section titled “Referential Transparency”Referential transparency means that an expression can be replaced with its value without changing the program’s meaning. This property enables:
- Equational reasoning: You can reason about programs using algebraic substitutions
- Memoization: Results can be cached since they never change
- Parallelism: Pure computations can run in any order without synchronization
- Testing: Pure functions are easy to test since they have no hidden dependencies
-- Because of referential transparency, these are equivalent:map (*2) [1, 2, 3]-- is the same as[2, 4, 6]
-- And this:let x = square 5in x + x-- is the same as:square 5 + square 5-- is the same as:50Separating Side Effects
Section titled “Separating Side Effects”Haskell does not forbid side effects; it separates them from pure code. Side effects are represented as values in the IO monad, making them explicit in the type system. This means you can always tell from a function’s type signature whether it performs I/O.
-- Pure: no IO in the typelength :: [a] -> Intlength [] = 0length (_:xs) = 1 + length xs
-- Impure: IO is explicit in the typemain :: IO ()main = do putStrLn "Enter your name:" name <- getLine putStrLn ("Hello, " ++ name ++ "!")The type signature main :: IO () immediately tells the reader that this function interacts with the outside world. This explicitness is one of Haskell’s greatest strengths for maintaining large codebases.
Lazy Evaluation
Section titled “Lazy Evaluation”How Lazy Evaluation Works
Section titled “How Lazy Evaluation Works”In Haskell, expressions are not evaluated until their results are needed. This is also called call-by-need evaluation or non-strict evaluation. Compare with strict (eager) evaluation used by most languages:
-- In a strict language, both branches would be evaluated-- In Haskell, only the needed branch is evaluatedif True then expensiveComputationA else expensiveComputationB-- Only expensiveComputationA is evaluated because the condition is TrueThunks and WHNF
Section titled “Thunks and WHNF”A thunk is a deferred computation — an unevaluated expression waiting to be needed. When a thunk is demanded, it is evaluated to Weak Head Normal Form (WHNF), which means the outermost constructor or function application is resolved.
-- This creates a thunk for [1..1000000]-- The list is not materialized in memorylet xs = [1..1000000]
-- This demands only the first element-- Only the thunk for the head is evaluatedhead xs-- => 1
-- This creates a thunk for the sum-- The list elements are only computed as neededlet total = sum xs-- total remains a thunk until its value is demandedBenefits of Laziness
Section titled “Benefits of Laziness”Working with infinite data structures: Lazy evaluation allows you to define and work with infinite lists:
-- Infinite list of natural numbersnats :: [Integer]nats = [0..]
-- Infinite list of Fibonacci numbersfibs :: [Integer]fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- Take first 10 Fibonacci numbers-- Only computes what is neededtake 10 fibs-- => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
-- Check if 55 is a Fibonacci number-- Only computes until it finds or passes 5555 `elem` fibs-- => TrueModular programming: Laziness enables separation of concerns between data generation and data consumption:
-- Generate primesprimes :: [Integer]primes = sieve [2..] where sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
-- Consumer decides how many to take-- The generation and filtering are interleavedsum (take 100 primes)-- => 24133
product (take 5 primes)-- => 2310Composability: Functions can be composed without worrying about intermediate data being materialized:
-- This composes three operations on potentially infinite data-- No intermediate lists are createdresult = sum . takeWhile (< 1000) . filter even $ map (^2) [1..]Pitfalls of Laziness
Section titled “Pitfalls of Laziness”Space leaks: When thunks accumulate faster than they are consumed, memory usage grows unexpectedly:
-- Space leak: the accumulator builds up thunks-- sum [1..1000000] using foldl accumulates unevaluated thunksbadSum = foldl (+) 0 [1..1000000]
-- Fix with strict foldl'goodSum = foldl' (+) 0 [1..1000000]-- foldl' forces evaluation of the accumulator at each stepDebugging difficulty: Lazy evaluation makes reasoning about evaluation order harder. When a program crashes or loops, it can be difficult to determine which thunk caused the problem. Tools like Debug.Trace and GHC’s profiling flags (+RTS -s) help diagnose such issues.
import Debug.Trace
-- Trace shows when an expression is evaluated-- Use only for debugging, not for logicmyFunction x = trace ("Evaluating with x = " ++ show x) (x * 2)Strictness Annotations
Section titled “Strictness Annotations”You can selectively enforce strictness when needed:
-- Bang patterns extension: force evaluation of arguments{-# LANGUAGE BangPatterns #-}
sumStrict :: [Int] -> IntsumStrict = go 0 where go !acc [] = acc go !acc (x:xs) = go (acc + x) xs
-- seq function: evaluates first argument to WHNF before returning second-- Useful for controlling evaluation orderforceSum :: [Int] -> IntforceSum xs = foldl (\acc x -> acc `seq` acc + x) 0 xs
-- Strict data type constructorsdata StrictPair a b = StrictPair !a !b-- Fields are evaluated when the constructor is appliedType Inference
Section titled “Type Inference”Hindley-Milner Type Inference
Section titled “Hindley-Milner Type Inference”Haskell uses the Hindley-Milner type inference algorithm (also known as Algorithm W). This means the compiler can deduce the types of most expressions without explicit annotations. However, top-level function definitions include type signatures as documentation and for clarity.
-- The compiler infers: Int -> Int -> IntdoubleAndAdd x y = (x + x) + y
-- The compiler infers: (a, b) -> (a, b) -> (a, [b])pairProcess p1 p2 = (fst p1, [snd p1, snd p2])
-- The compiler infers: [a] -> IntmyLength [] = 0myLength (_:xs) = 1 + myLength xs
-- The compiler infers: (a -> b) -> [a] -> [b]myMap f [] = []myMap f (x:xs) = f x : myMap f xsType Variables and Polymorphism
Section titled “Type Variables and Polymorphism”When the compiler cannot determine a concrete type, it introduces a type variable (written in lowercase, starting from a). Functions that work for any type are called polymorphic:
-- id works for any type aid :: a -> aid x = x
-- const works for any types a and bconst :: a -> b -> aconst x y = x
-- The type variable 'a' can be instantiated to any concrete type:-- id 42 :: Int -> Int-- id "hello" :: String -> String-- id True :: Bool -> Bool-- id [1, 2, 3] :: [Int] -> [Int]Why Write Type Signatures?
Section titled “Why Write Type Signatures?”Even though the compiler can infer types, there are strong reasons to write them explicitly:
- Documentation: Type signatures communicate the intended interface
- Error messages: Type errors are localized to the signature, not deep in the implementation
- Readability: Readers can understand the function without reading its body
- Safety catch: If the inferred type does not match the written type, the compiler warns you
-- Good: explicit type signature documents intent-- Type error points here, not deep in the function bodyfactorial :: Integer -> Integerfactorial 0 = 1factorial n = n * factorial (n - 1)
-- The compiler infers: [a] -> [a]-- Writing it down makes the API clearreverseList :: [a] -> [a]reverseList = go [] where go acc [] = acc go acc (x:xs) = go (x : acc) xsThe Haskell Ecosystem
Section titled “The Haskell Ecosystem”GHC (Glasgow Haskell Compiler)
Section titled “GHC (Glasgow Haskell Compiler)”GHC is the flagship Haskell compiler, developed at the University of Glasgow since 1992. It compiles Haskell to native code via an intermediate representation called STG (Spineless Tagless G-machine). GHC features:
- Native code generation: Produces optimized machine code for x86, ARM, and other architectures
- LLVM backend: Can use LLVM for code generation, often producing faster code
- Interactive REPL: GHCi for interactive development and testing
- Profiling: Built-in cost-center profiling and heap profiling
- Language extensions: Hundreds of extensions enabled via pragmas
## Compile a Haskell programghc Main.hs -O2 -o myprogram
## Run the interactive REPLghci MyModule.hs
# Compile with optimizations and threadingghc -O2 -threaded -with-rtsopts=-N MyProgram.hsGHCi (Interactive REPL)
Section titled “GHCi (Interactive REPL)”GHCi is the interactive environment for Haskell development. It provides:
-- Load a moduleghci> :load MyModule
-- Evaluate expressionsghci> 2 + 35ghci> map (*2) [1, 2, 3][2, 4, 6]
-- Check typesghci> :type mapmap :: (a -> b) -> [a] -> [b]
-- Get information about a functionghci> :info Maybedata Maybe a = Nothing | Just a
-- List loaded modulesghci> :module
-- Browse module contentsghci> :browse Data.List
-- Reload current moduleghci> :reload
-- Set language extensionsghci> :set -XGADTsghci> :set -XOverloadedStrings
-- Enable multiline modeghci> :set +m
-- Get helpghci> :helpCabal is Haskell’s original build system and package manager. A Cabal project is defined by a .cabal file that specifies dependencies, build options, and metadata:
-- myproject.cabalname: myprojectversion: 0.1.0.0build-type: Simplecabal-version: >=1.10
library exposed-modules: MyLib build-depends: base >=4.14 && <5, containers, text hs-source-dirs: src default-language: Haskell2010
executable myproject main-is: Main.hs build-depends: base >=4.14 && <5, myproject hs-source-dirs: app default-language: Haskell2010# Build the projectcabal build
# Run testscabal test
# Enter a development shell with all dependenciescabal replStack is a cross-platform build tool that provides reproducible builds through a curated package index called Stackage. It uses a stack.yaml file and a package.yaml (hpack format) or .cabal file:
resolver: lts-22.0packages: - .extra-deps: []ghc-options: "$locals'': -Wall -Werror# Build the projectstack build
# Run the executablestack exec myproject
# Run testsstack test
# Start a GHCi session with project dependenciesstack ghci
# Generate haddock documentationstack haddockHackage
Section titled “Hackage”Hackage is Haskell”s central package repository at https://hackage.haskell.org. It hosts thousands of open-source packages. Key resources:
- Package search: Find libraries for specific tasks
- Documentation: Haddock-generated API docs for every package
- Module documentation: Browse module contents and types
- Package metadata: Dependencies, license, maintainer information
Hello World and Compilation
Section titled “Hello World and Compilation”Your First Haskell Program
Section titled “Your First Haskell Program”-- hello.hsmodule Main where
main :: IO ()main = do putStrLn "Hello, World!" putStrLn "What is your name?" name <- getLine putStrLn ("Hello, " ++ name ++ "!")Compiling and Running
Section titled “Compiling and Running”# Compile with GHCghc -O2 hello.hs -o hello./hello
# Run directly with runhaskell (interpreted, no compilation)runhaskell hello.hs
# Compile with Stackstack ghc -- -O2 hello.hs -o hello
# Compile with Cabalcabal build && cabal runUnderstanding the Structure
Section titled “Understanding the Structure”module Main where-- ^ declares this as the Main module
-- 'main' is the entry point when compiled as an executable-- Its type IO () indicates it performs I/O and returns nothing usefulmain :: IO ()main = do -- do notation sequences IO actions putStrLn "Hello, World!" -- prints a string followed by newline name <- getLine -- reads a line from stdin, binds to name -- String concatenation with ++ putStrLn ("Hello, " ++ name ++ "!")GHCi as a Calculator and Playground
Section titled “GHCi as a Calculator and Playground”ghci> -- Arithmeticghci> 2 + 3 * 414ghci> 2^101024ghci> mod 17 52
-- String operationsghci> "Hello" ++ " " ++ "Haskell""Hello Haskell"ghci> length "abc"3
-- List operationsghci> [1..10][1,2,3,4,5,6,7,8,9,10]ghci> take 5 [1, 5, 10..][1,5,10,15,20]
-- Function definitions in GHCighci> let double x = x * 2ghci> double 2142ghci> let greet name = "Hello, " ++ nameghci> greet "Haskell""Hello, Haskell"
-- Multi-line definitionsghci> :set +mghci> let abs nghci> | n < 0 = negate nghci> | otherwise = nghci>ghci> abs (-5)5Basic Project Structure
Section titled “Basic Project Structure”A typical Haskell project organized with Stack:
myproject/ stack.yaml package.yaml src/ Lib.hs Internal/ Helper.hs app/ Main.hs test/ LibTest.hs README.mdThe package.yaml (hpack format) provides a cleaner way to define the Cabal package:
name: myprojectversion: 0.1.0.0synopsis: A sample Haskell project
library: source-dirs: src dependencies: - base >= 4.14 && < 5 - containers - text
executable myproject: main: Main.hs source-dirs: app dependencies: - base - myproject
tests: myproject-test: main: LibTest.hs source-dirs: test dependencies: - base - myproject - HUnitLanguage Extensions and Pragmas
Section titled “Language Extensions and Pragmas”Haskell’s extensibility through GHC pragmas is a distinctive feature. Extensions are enabled at the module level:
{-# LANGUAGE OverloadedStrings #-}{-# LANGUAGE DeriveGeneric #-}{-# LANGUAGE GADTs #-}
module MyModule whereCommonly used extensions:
- OverloadedStrings:
"hello"can be any type that implementsIsString - RecordWildCards:
let { name = n; age = a } in Person {..} - LambdaCase:
\caseinstead of\x -> case x of - TupleSections:
(1,)is equivalent to\y -> (1, y) - DerivingStrategies: Fine-grained control over deriving
- StrictData: All fields in data types are strict by default
Each extension addresses a specific limitation of Haskell 2010. Together, they form the “Modern Haskell” programming style used in production code.
Intuition
Section titled “Intuition”Haskell is a mathematical proof assistant that happens to run programs: In most languages, you write instructions and hope they’re correct. In Haskell, you write specifications — mathematical descriptions of what things are — and the compiler verifies they fit together. Purity means every function is a mathematical function: same input, same output, no surprises. Laziness means you describe what to compute, not when to compute it — like defining an infinite list and trusting the runtime to only build what’s needed.
Why it matters: Haskell’s type system catches entire categories of bugs at compile time that other languages discover in production. The combination of purity and strong typing means that if it compiles, it’s much more likely to be correct.
The key insight: Haskell separates “what to compute” (pure functions) from “how to interact with the world” (IO monad), making side effects explicit in the type system — you can always tell from a function’s type whether it touches the outside world.
- Finance: Standard Chartered, Barclays use Haskell for quantitative analysis
- Blockchain: Cardano (IOHK) is implemented in Haskell for its strong type safety
- Web development: Servant, Yesod, and IHP provide type-safe web frameworks
- Compilers: GHC itself, the Agda proof assistant, and the Idris language
- Systems: Facebook’s spam filtering system (Haxl) uses monad transformers
- Networking: The Discord Elixir gateway uses Haskell for real-time features
The combination of strong typing, purity, and concurrency makes Haskell particularly well-suited for systems where correctness is critical and where concurrent operations are the norm.
Common Mistakes
Section titled “Common Mistakes”- Assuming Haskell is purely academic: Haskell is used in production at major companies (Standard Chartered, Facebook, GitHub). Dismissing it as “not practical” ignores its real-world adoption in finance, infrastructure, and developer tooling.
- Confusing laziness with inefficiency: Laziness does not mean slow. GHC optimises lazy evaluation aggressively, and strict evaluation can actually be slower if it forces unnecessary computation. Profile before assuming laziness is the problem.
- Treating the type system as an obstacle: Haskell’s type system catches bugs at compile time that other languages discover at runtime. Fight the compiler less — read type errors carefully, as they often point directly to the problem.
- Trying to use
IOfor everything: The IO monad is for real-world side effects, not general computation. Keep pure functions separate from IO, and push IO to the edges of your program. This makes testing and reasoning much easier.