Functional Programming
Functional Programming
Last updated: 2026-05-17 · 30 min read
Functional Programming
Functional Programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. In Korean, it is commonly referred to as functional programming(함수형 프로그래밍) or functional paradigm(함수형 패러다임).
Functional Programming, alongside object-oriented programming, forms one of the two dominant paradigms in the modern software industry, yet its theoretical roots are considerably older. While object-oriented programming emerged from simulation and GUI work in the 1960s, lambda calculus, the foundation of functional programming, originated in mathematical logic in the 1930s. For this reason, functional programming is sometimes called "the old new paradigm."
Object-Oriented:
Object-oriented programming bundles data and the behaviors that manipulate that data together into discrete units called objects.
Functional:
Functional programming constructs programs through the composition of pure functions that transform data.
Core Definitions
Functional programming is a paradigm that places strong emphasis on the following properties.
1.Functions are first-class citizens.
2.Functions are close to mathematical functions (same input → same output).
3.Avoid mutation and side effects.
4.Prefer recursion and higher-order functions over loops.
5.Data is immutable – instead of modifying existing data, new data is created.
Languages that enforce all five of these properties at 100% are rare. The degree of enforcement varies, but the functional mindset has been partially adopted in virtually every modern language.
History
The history of functional programming begins not in industry but in mathematical logic. This starting point is what makes the culture and vocabulary of functional programming distinct from that of object-oriented programming.
Lambda Calculus (1930s)
The lambda calculus, formalized by Alonzo Church between 1932 and 1936, demonstrated that function abstraction and application alone are sufficient to express any computation. The Church-Turing thesis asserts that lambda calculus and Turing machines possess equivalent computational power.
Basic operations of lambda calculus:
-Variable: x
-Abstraction: λx. M (a function that takes x and returns M)
-Application: M N (applies function M to argument N)
These three syntactic constructs alone are sufficient to express natural numbers, Booleans, data structures, recursion, and all computation. Every core concept in modern functional languages is a direct descendant of lambda calculus.
Lisp (1958)
Lisp (LISt Processor), introduced by John McCarthy in 1958, was the first programming language inspired by lambda calculus. McCarthy's 1960 paper "Recursive Functions of Symbolic Expressions and Their Computation by Machine" serves as its foundational reference.1
That said, care is needed when characterizing Lisp as a "functional language." Lisp introduced core elements of the functional mindset (first-class functions, a recursion-centric approach, and homoiconicity), but it freely permitted mutable variables and side effects. In other words, Lisp is the starting point of functional programming, not a purely functional language. This nuance undermines the accuracy of the common claim that Lisp is the "originator of functional programming."
ISWIM and Theoretical Foundations (1960s)
Peter Landin introduced the hypothetical language ISWIM (If you See What I Mean) in his 1966 paper "The Next 700 Programming Languages."2 Although ISWIM was never actually implemented, it exerted a profound influence on the syntax and semantics of subsequent functional languages. The let/where constructs, expression-oriented design, and the offside-rule style of thinking all trace back to this lineage.
Backus's Turing Award Lecture (1977)
In his 1977 Turing Award lecture, "Can Programming Be Liberated from the Von Neumann Style?", John Backus criticized imperative programming for being too tightly bound to Von Neumann architecture and proposed function-level programming instead.3 This lecture is frequently cited as the industrial legitimization of the functional programming movement.
An interesting footnote: Backus was the designer of FORTRAN. The fact that the very person who created the archetypal imperative language went on to identify the limitations of that paradigm gives the lecture even greater weight.
The ML Family (1973)
ML (Meta Language), created by Robin Milner around 1973 for the LCF (Logic for Computable Functions) theorem prover, is the landmark example of combining static type inference with a functional language.4 The Hindley-Milner type system became widely known through the ML family and went on to serve as the foundation for statically typed functional languages.
ML family descendants:
Standard ML (1983)
Caml (1985) / OCaml (1996)
F# (2005) - Microsoft, OCaml-based
Influenced: the type systems of Rust, Swift, and Scala
Miranda and Lazy Evaluation (1985)
Miranda, released by David Turner in 1985, was a commercially supported pure functional language combining lazy evaluation, pure functions, and algebraic data types.5 Trademark issues made Miranda difficult to use as a shared research language in academia, and the need for an open standard to unify the various lazy functional languages became one of the driving motivations behind the creation of Haskell.
Haskell (1990)
Haskell began in 1987 as a committee effort to establish a common standard for lazy-evaluation-based functional languages, and published its first standard (Haskell 1.0) in 1990.6 The name comes from Haskell Curry, a researcher in lambda calculus.
Haskell's defining contributions:
Adopted lazy evaluation as the default
Preserved purity by separating I/O through the IO monad
Introduced type classes (Wadler & Blott, 1989)
Became the standard reference point for functional thinking in both academia and industry
Personal note: there is a joke that "programming in Haskell means no bugs, but also no finished programs." I like that joke.
Concurrency-first functional programming (Erlang, 1986)
Erlang, created in 1986 by Joe Armstrong and colleagues at Ericsson, is a functional language designed for concurrency and reliability in telecommunications systems. Through the actor model, lightweight processes, and the "let it crash" philosophy, it became one of the de facto standards in distributed systems. Elixir (2011) is its successor, layering a modern syntax on top of the Erlang VM.
The resurgence of modern functional programming (2000s onward)
Factors that brought functional programming back into the spotlight in industry:
The arrival of the multicore era, as the concurrency cost of mutable state grew significantly
The emergence of JVM/CLR functional languages such as Scala (2003), Clojure (2007), and F# (2005)
Rust (2010 onward), a systems language with strong functional influences
React + Redux (2015 onward), introducing unidirectional data flow and immutability to the JavaScript frontend
TypeScript (2012 onward), with discriminated unions and type-level expressiveness
Functional Programming is no longer a "specialized paradigm" but has been partially absorbed into every modern language.
Core Concepts
First-class Function
This means functions are treated as equal to any other value.
-Functions can be assigned to variables.
-Functions can be passed as arguments to other functions.
-Functions can be returned as values from other functions.
-Functions can be stored in data structures.First-class functions are a prerequisite for higher-order functions to have meaning.
Pure Function
A function that satisfies both of the following conditions:
-Given the same input, it always returns the same output.
-It does not cause side effects on external state.Examples of side effects: mutating a global variable, writing to the console, writing to a file, network communication, and raising exceptions.
The value of a pure function stems from referential transparency: the property that any expression can be replaced by its resulting value without changing the meaning of the program. This is the foundation of functional reasoning and optimization.
Immutability
The property that once a value is created, it cannot be changed. When a modification is needed, a new value is created instead.
-- Haskell: adding an element to a list creates a new list
xs = [1, 2, 3]
ys = 0 : xs -- ys = [0, 1, 2, 3], xs remains unchanged
The cost of immutability is memory allocation, but persistent data structures and structural sharing bring it to a practical level of efficiency.
Higher-order Function
A function that takes other functions as arguments or returns a function as its result. It is the unit of abstraction in functional programming.
-- map, filter, and fold are the prime examples
map :: (a -> b) -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]
foldr :: (a -> b -> b) -> b -> [a] -> b
Just as polymorphism and inheritance serve as abstraction tools in object-oriented programming, higher-order functions fill that role in functional programming.
Function Composition
An operation that chains two functions together into a single function.
-- Haskell: the `.` operator
double :: Int -> Int
double x = x * 2
negate :: Int -> Int
negate x = -x
doubleNegate :: Int -> Int
doubleNegate = negate . double -- first `double`, then `negate`
Function composition is the unit of modularity in functional programming. If object-oriented programming assembles objects, functional programming composes functions.
Currying and Partial Application
A transformation that expresses a function taking multiple arguments as a sequence of functions each taking one argument. The name derives from Haskell Curry.
-- In Haskell, every function is automatically in curried form
add :: Int -> Int -> Int
add x y = x + y
addOne :: Int -> Int
addOne = add 1 -- Partial Application
Some languages make currying automatic (Haskell, OCaml), while others require it to be done manually (JavaScript, Python).
Lazy Evaluation
An evaluation strategy that defers computation until the value is actually needed. Haskell is the canonical example.
-- Infinite lists can also be defined
naturals = [1..]
take 5 naturals -- [1, 2, 3, 4, 5]
Lazy evaluation offers real benefits in composability and abstraction, but its drawbacks are equally clear: space leaks and difficulty debugging. The fact that lazy evaluation is not unconditionally good is covered in the functional anti-patterns section.
Recursion and Tail Call Optimization
Functional languages use recursion instead of loops. Because naive recursion consumes stack space, tail call optimization (TCO) is important: the compiler transforms a recursive call in tail position into an iterative loop.
-- Non-tail-recursive sum (stack accumulation)
sumBad :: [Int] -> Int
sumBad [] = 0
sumBad (x:xs) = x + sumBad xs
-- Tail-recursive sum (using an accumulator)
sumGood :: Int -> [Int] -> Int
sumGood acc [] = acc
sumGood acc (x:xs) = sumGood (acc + x) xs
Languages like Scheme and Erlang, which treat tail calls as a core part of their language and runtime design, provide stable TCO. OCaml also offers strong practical support for tail recursion. Haskell, however, is a special case: due to lazy evaluation, saying "tail recursion always keeps the stack and heap safe" is incorrect. When an accumulator builds up as deferred thunks, the result can be a space leak where the heap, rather than the stack, overflows, and in such cases strictness controls like seq, bang patterns, and foldl' become necessary.7
Functional Concepts Combined with the Type System
Algebraic Data Types (ADT)
Data is expressed as a combination of product types (AND) and sum types (OR).
-- Product type (similar to struct/record)
data Point = Point Double Double
-- Sum type (tagged union)
data Shape = Circle Point Double
| Rectangle Point Point
| Triangle Point Point Point
The Result type, Option type, trees, and lists are all expressed as algebraic data types.
Pattern Matching
A construct that branches by destructuring algebraic data types.
area :: Shape -> Double
area (Circle _ r) = pi * r * r
area (Rectangle (Point x1 y1) (Point x2 y2)) =
abs (x2 - x1) * abs (y2 - y1)
area (Triangle ...) = ...
The core value is the exhaustiveness check, where the compiler verifies that all cases are handled. If object-oriented polymorphism is runtime dispatch, then functional pattern matching is closer to compile-time dispatch.
Type Class
An ad-hoc polymorphism mechanism introduced by Haskell. Philip Wadler and Stephen Blott proposed it in their 1989 paper How to Make Ad-Hoc Polymorphism Less Ad Hoc.8
class Eq a where
(==) :: a -> a -> Bool
instance Eq Int where
x == y = primIntEq x y
Rust's traits, Scala's implicit/given, and Swift's protocols all occupy the same problem space of "abstracting a set of operations over a type." Rust traits and Scala type class patterns are frequently compared to Haskell type classes in particular. That said, the solutions each language adopts are not entirely equivalent, because their module systems, object systems, and compiler constraints differ.
Parametric Polymorphism
A function that takes a type as a parameter. While similar to C++ templates and Java generics, parametric polymorphism is more deeply integrated in functional languages.
identity :: a -> a
identity x = x
length :: [a] -> Int
A type variable a can be instantiated to any type.
Handling Side Effects
The most challenging area in functional programming. Pure functions alone cannot express I/O, state, or exceptions.
Monad
An abstraction that encodes side effects in the type system to preserve purity. Eugenio Moggi introduced it as a semantics of computation in his 1991 paper Notions of Computation and Monads, and Philip Wadler made it practical in Haskell during the 1990s.910
-- IO Monad: side effects made explicit in the type
main :: IO ()
main = do
name <- getLine -- IO String
putStrLn ("Hello, " ++ name)
That getLine returns IO String means it is "an IO action that produces a string," not "a string itself." This distinction in types is what separates pure functions from side effects.
A common misconception to correct:
Monad = a form borrowed from category theory, but using it does not require knowledge of category theory
A monad is simply a programming tool, like if/then/else, not a mystical concept.
Personal note: the most dangerous moment when encountering monads for the first time is when you say, "Ah, I finally get it." That's usually when you stop understanding them again.
Effect System
A more general approach that expresses side effects themselves as types, rather than using monads. Algebraic effects are an active area of recent research, with languages such as Eff, Koka, and Unison exploring this direction.
Monad: One effect, one type (e.g., IO, State, Reader, etc.)
Effect system: Multiple effects can be freely composed / combined.Functional Programming vs Object-Oriented Programming
The two paradigms decompose the same problem along different axes.
Feature | Object-Oriented | Functional |
|---|---|---|
State | Encapsulated inside objects | Immutable, explicit outside functions |
Polymorphism | Subtyping + parametric polymorphism | Parametric polymorphism + type classes |
Reuse | Inheritance, composition, delegation | Function composition, higher-order functions |
Time / Flow | State changes of objects over time | Transformation flow of values |
Error handling | Exceptions + try/catch | Result / Either, monads |
Modularity | Classes, packages | Modules, functions, types |
Same problem, different decomposition:
"Track the order status"
Object-Oriented: An Order object has a status field, and its state is changed via methods.
Functional: Order is immutable. A state transition is a function that creates a new Order (Order -> Order).
The two approaches are compatible. Most modern languages support both paradigms together (Scala, Rust, C#, TypeScript). The difference is simply which paradigm you adopt as your default way of thinking; it is not an either/or choice.
The Purity Spectrum
Not all functional languages enforce the same level of purity.
[Strong Purity]
-Haskell, Idris, Agda, Elm
-Side effects are separated via types
-Haskell is lazily evaluated by default
-Elm, Idris, Agda have strong purity / type discipline, but their evaluation strategies differ from Haskell
-ML Family (Standard ML, OCaml, F#)
-Mutable references are allowed but explicit
-Strict evaluation by default
-Lisp Family (Clojure, Common Lisp, Scheme, Racket)
-Clojure has immutable data structures by default
-Other Lisps have free-form mutability
-Dynamically typed
-JVM / Native Multi-Paradigm (Scala, Rust, Swift)
-Strong support for functional tools
-Used together with OOP
-Imperative Languages with Functional Influence (C#, JavaScript/TypeScript, Python, Java)
-Lambdas, higher-order functions, comprehensions, etc. have been absorbed
-Core is still OOP / imperative
[Weak Purity]Whether a given language is "functional or not" is not a binary question but a matter of where it sits on a spectrum.
Representative Techniques
map / filter / fold
The three fundamental operations for list transformation.
map (*2) [1,2,3,4] -- [2,4,6,8]
filter (>2) [1,2,3,4] -- [3,4]
foldr (+) 0 [1,2,3,4] -- 10
Operations that would require a for loop, an accumulator, and conditionals in imperative style can be compressed into a single-line expression.
Function Composition and Pipelines
-- Haskell: Right to Left
processData :: [RawData] -> [Result]
processData = map normalize . filter isValid . map parse
// F#: left to right (|> pipe operator)
let processData rawData =
rawData
|> List.map parse
|> List.filter isValid
|> List.map normalize
Point-Free Style
Defined purely through function composition, without explicitly naming the arguments.
-- General Style
sumOfSquares xs = sum (map (^2) xs)
-- point-free
sumOfSquares = sum . map (^2)
It should be used only where it genuinely improves readability; taken too far, it becomes an anti-pattern.
Persistent Data Structures
A data structure that maintains immutability while avoiding full copies on partial updates. Through structural sharing, modifications can be performed in O(log n) or amortized O(1) time.
Representative examples:
- Hash Array Mapped Trie (HAMT)
- Finger Tree
- Persistent Red-Black Tree
- Clojure's PersistentVector / PersistentHashMap
- Haskell's Data.Map (balanced binary tree)Chris Okasaki's 1998 book Purely Functional Data Structures is the standard reference in this field.11
How Immutability Is Actually Implemented
Immutability does not mean "using no memory." In fact, many functional implementations allocate new nodes on the heap while sharing existing nodes. The key point is that existing values are never overwritten in place.
Imperative update:
Overwrite the same location in an array/object.
Functional (persistent) update:
Only create new nodes along the changed path, and share the rest of the substructure with the existing nodes.
Structural Sharing in Lists
A Haskell-style list is conceptually a chain of cons cells.
xs = [1, 2, 3]
ys = 0 : xs
From a memory perspective, it looks roughly like this.
xs -> [1] -> [2] -> [3] -> []
ys -> [0] -> [1] -> [2] -> [3] -> []
^
Sharing xs and tailWhen constructing ys, the entire [1, 2, 3] is not copied. Only a new cons cell [0] is created, and its tail shares the existing list that xs points to. This is why prepending an element with (:) is cheap. Conversely, appending to the end of a list is typically expensive because the existing prefix must be reconstructed.
Structural Sharing in Trees and Maps
Balanced-tree-based maps and sets work similarly. Changing a single key creates only the nodes along the path from the root to that key; all untouched subtrees are shared.
Original tree:
A
/ \
B C
/ \
D E
New tree with only E changed:
A'
/ \
B' C <- shared
/ \
D E' <- D also sharedThis is the essence of persistent data structures. The previous version remains alive, and so does the new version. It is not "copy everything" but rather "path copying plus structural sharing."
Stack vs. Heap Perspective
Function calls still create stack frames. Local variables, return addresses, and temporary computation state can live on the stack. However, values whose lifetimes may extend beyond a call's scope — list nodes, tree nodes, closures, and lazy-evaluation thunks — typically live on the heap.
Stack:
- Function call frames
- Short-lived local bindings
- Temporary state during evaluation
Heap:
- Nodes of data structures such as lists, trees, and records
- Environments captured by closures
- Thunks for lazy evaluation (in Haskell)
- Nodes of shared persistent data structures
In other words, immutability in functional programming does not mean "fast because it only uses the stack." On the contrary, it actively relies on heap allocation and garbage collection, while structural sharing and compiler optimizations keep the cost down.
Personal note: immutability is not a free lunch. The idea is that you are billed not for "copying everything" but only for "the changed path." Personally, I have nothing but a growing tab.
Haskell's Thunks and Space Leaks
Because of lazy evaluation, Haskell may defer computing a value and instead store a thunk on the heap.
sumBad = foldl (+) 0 [1..1000000]
The code above intuitively looks like it keeps adding to an accumulator, but under lazy evaluation, a pile of unevaluated computation chunks can build up like this.
((((0 + 1) + 2) + 3) + ...)
When these chunks accumulate on the heap, the symptom is not a stack overflow but a rapid explosion in heap usage. This is why Haskell performance problems often come down not to "stack or heap" but to "which thunks are forced to evaluate, and when."
import Data.List (foldl')
sumGood = foldl' (+) 0 [1..1000000]
foldl' evaluates the accumulator strictly at each step, reducing thunk accumulation.
Immutability in Practical Languages
In mainstream languages like JavaScript/TypeScript, C#, and Java, "immutable update" does not typically mean the runtime automatically provides persistent data structures. In practice, it usually means either shallow-copying objects or arrays, or using libraries such as Immutable.js, Immer, or Clojure-style persistent collections.
// Shallow-copy-based immutable updates
const next = {
...prev,
status: "paid",
};
This code is an immutable update in the sense that it does not mutate prev, but that does not automatically make the entire deeply nested structure a persistent data structure. Incorrectly sharing nested objects can still cause internal mutation problems.
Immutability implementation approaches:
- Haskell / Clojure: Structural sharing is the default at the language / standard data structure level.
- Scala / F# / OCaml: Immutable and mutable collections are offered as separate choices.
- JS / TS / React: Immutable updates are maintained via convention and libraries.
- Rust: Ownership / borrowing rules control aliasing and mutation.
Functional Programming Examples by Language
Haskell
-- Algebraic data types + pattern matching + recursion
data Tree a = Leaf | Node a (Tree a) (Tree a)
insert :: Ord a => a -> Tree a -> Tree a
insert x Leaf = Node x Leaf Leaf
insert x (Node y left right)
| x < y = Node y (insert x left) right
| x > y = Node y left (insert x right)
| otherwise = Node y left right
OCaml
(* Option type and pattern matching *)
let rec find_first pred = function
| [] -> None
| x :: rest when pred x -> Some x
| _ :: rest -> find_first pred rest
F#
// Pipeline-Oriented
let processOrders orders =
orders
|> List.filter (fun o -> o.Status = Active)
|> List.map (fun o -> { o with Total = computeTotal o })
|> List.sortByDescending (fun o -> o.Total)
Clojure
;; Data Transformation Pipeline
(->> orders
(filter active?)
(map compute-total)
(sort-by :total >))
Scala
// for-comprehension (monad abstraction)
val result = for {
user <- findUser(id)
account <- findAccount(user)
balance <- getBalance(account)
} yield balance
JavaScript / TypeScript
// Higher-Order Functions and Composition
const processOrders = (orders: Order[]): Result[] =>
orders
.filter(o => o.status === "active")
.map(o => ({ ...o, total: computeTotal(o) }))
.sort((a, b) => b.total - a.total);
C#
// LINQ is essentially a functional query language
var result = orders
.Where(o => o.Status == OrderStatus.Active)
.Select(o => o with { Total = ComputeTotal(o) })
.OrderByDescending(o => o.Total)
.ToList();
Rust
// Iterator chaining + closures
let result: Vec<_> = orders
.iter()
.filter(|o| o.status == Status::Active)
.map(|o| Order { total: compute_total(o), ..o.clone() })
.collect();
Concurrency and Distribution
The primary reason functional programming has experienced a resurgence in the multicore era is that immutability reduces the cost of concurrency.
Concurrency costs of mutable state:
- Lock management
- Race conditions
- Deadlocks
- Memory consistency models
Concurrency benefits of immutable state:
- No locks needed (read-only)
- Side effects are explicit and thus easier to reason about
- Naturally pairs with actor model and message passing
Notable examples:
Erlang / Elixir: well known for the actor model, lightweight processes, and high-availability telecommunications systems
Akka (Scala/Java): an actor-based framework for distributed systems
Hadoop / Spark: MapReduce is inherently a functional programming paradigm
React + Redux: unidirectional data flow, immutable state, changes through reducer functions
Elm: enforces pure functional programming on the frontend
Common Misconceptions and Corrections
Misconception 1: "Functional programming has no mutability whatsoever"
Inaccurate. Even purely functional languages have mechanisms for handling side effects (Haskell's IO monad, ST monad). Multi-paradigm functional languages (OCaml, F#, Scala) explicitly allow mutable references. The essence of functional programming is not "no mutation" but rather "immutable by default, mutation made explicit."
Misconception 2: "Functional programming doesn't use objects"
Inaccurate. F#, OCaml, and Scala all have object systems. Haskell also builds object-like abstractions through records and type classes. Functional programming is not a negation of OOP but a paradigm with different priorities.
Misconception 3: "Recursion is always better than iteration"
Inaccurate. In languages that do not guarantee tail call optimization (TCO) (JavaScript, Python), deep recursion causes stack overflows. When clarity is equal, it is better to follow the form the language supports naturally.
Misconception 4: "You need to know category theory to understand monads"
Inaccurate. The intuition of a monad as a box with a consistent interface is sufficient. Category theory is the mathematical background of monads, not a prerequisite for using them.
Personal note: that said, say this out loud in a functional programming community and brace for seizures. Every programming community eventually optimizes for memes and turns dogmatic.
Misconception 5: "Functional programming is not practical in industry"
Inaccurate. Functional languages and functional styles are used in real industrial systems, as shown by WhatsApp's Erlang/FreeBSD infrastructure, Jane Street's OCaml, Meta/Facebook's Haskell-based Sigma, Twitter's Scala, and Klarna's Erlang.1213141516 It is more accurate to say that industry adoption is not absent, but tends to concentrate in specific domains such as high-availability telecommunications, finance, rules engines, and distributed services.
Personal note: I labeled this a misconception, but honestly I'm not sure it actually is one. I learned functional programming because people said it would bump up the leading digit of your salary, and it did change things. Just in the direction of a decrease.
Functional Anti-Patterns
1. Side effects inside map
// Incorrect usage: the purpose of map is to produce a result list
items.map(item => sendEmail(item)); // The result list is discarded
// Correct usage: use forEach for side effects
items.forEach(item => sendEmail(item));
2. Excessive point-free style
-- Code that tells you nothing about what it does
processAll = ((.) . (.)) (flip (.)) (uncurry . curry)
Point-free style should only be used when it improves readability.
Personal note: Point-free code at this level is closer to submitting a cipher than transferring knowledge to your colleagues.
3. Premature monad abstraction
-- Stacking monad transformer layers for simple tasks
type App a = ReaderT Config (StateT World (ExceptT Error IO)) a
A three-level stacked monad is almost always overkill. Start with a single IO or a single Either, then raise the level of abstraction only when necessary.
4. Space leaks caused by lazy evaluation
The classic Haskell pitfall.
-- `foldl` builds a large accumulating thunk due to lazy evaluation
sumBad = foldl (+) 0 [1..1000000]
-- `foldl'` evaluates the accumulator eagerly
sumGood = foldl' (+) 0 [1..1000000]
If you don't know the difference between foldl and foldl', memory usage will explode on large lists. This is the trap that lazy evaluation sets.
5. Using type class hierarchies like OOP class hierarchies
-- Anti-pattern: mimicking deep inheritance hierarchies with type classes
class Animal a where ...
class Animal a => Mammal a where ...
class Mammal a => Dog a where ...
Type classes are not a substitute for OOP inheritance hierarchies. They are a different tool entirely.
6. Accumulating intermediate lists
-- An intermediate list is created at every step
result = sum . map (*2) . filter even . map (+1) $ list
List fusion often optimizes this automatically, but it is safer to explicitly consolidate using streams or folds.
Criticisms of Functional Programming
Learning Curve
Mathematical vocabulary (monad, functor, applicative) and abstract thinking raise the barrier to entry. Developers accustomed to imperative or object-oriented styles need to go through a process of "re-learning."
Complexity of the I/O Model
Composing effects through monad transformers is powerful but carries a steep learning cost. Research into effect systems (algebraic effects) represents an ongoing effort to simplify this.
Memory Allocation Overhead
Immutability causes frequent allocation of new objects. Persistent data structures and compiler optimizations help mitigate this, but it remains a burden in latency-critical systems.
The Readability Debate
Point-free style, monadic do-notation, and type classes are powerful once familiar, but look cryptic to newcomers. A practical criticism is that code review costs can be high.
Why It Matters Now
The renewed attention functional programming has received since the 2010s is not simply a passing trend.
Multi-core era: The cost of concurrent mutable state is growing.
Distributed systems: Immutability and message passing are a natural fit.
Data pipelines: map/reduce is inherently functional.
Front-end state management: One-way flow in React/Redux.
Type system evolution: TypeScript and Rust have absorbed influences from the ML family.
John Hughes's Why Functional Programming Matters is a classic paper that frames the core value of functional programming as the modularity afforded by higher-order functions and lazy evaluation.17 Paul Hudak's survey is a representative overview that traces the history and technical characteristics of functional languages from lambda calculus through Lisp, ISWIM, FP, ML, Miranda, and Haskell.18
Today, "becoming a functional programmer" means less about working exclusively in Haskell and more about knowing how to apply functional thinking regardless of which language you use.
Learning Path
Getting started with functional thinking is more about shifting your mindset than choosing a language.19
1. Start with familiar languages
- mapfilterreduce in JavaScript
- List comprehensions and functools in Python
- LINQ in C#
- Reduce mutability and consciously use function composition
2. Expand to multi-paradigm functional languages
- One of F#, OCaml, or Scala
- Get comfortable with their type systems and pattern matching
3. Take on the challenge of pure functional programming
- Haskell or Elm
- Learn monads, type classes, and lazy evaluation
4. Strengthen theoretical foundations
- Lambda calculus
- Introduction to type theory (Pierce's "Types and Programming Languages")
- Category theory (optional)
Key questions:
-Can this problem be expressed as a flow of data transformations?
-Can this state change be replaced with the creation of a new value?
-Can this side effect be made explicit in the type system?
-Can this loop be expressed as a higher-order function?Summary
Functional Programming is a paradigm rooted in lambda calculus from the 1930s, evolving through Lisp (1958), ML (1973), and Haskell (1990) into the modern era. Its five core principles are as follows.
1. Functions are first-class citizens.
2. Functions are close to mathematical functions (purity).
3. Data is immutable.
4. Side effects are made explicit.
5. The units of abstraction are higher-order functions and function composition.
Functional Programming is not a replacement for object-oriented programming but a complement to it. It is regaining attention in the age of multi-core and distributed systems, and has been partially absorbed into most modern languages. A "functional programmer" is less a user of a specific language and more a developer who knows how to apply functional thinking.
See Also
Programming Paradigms
Design Patterns
Lambda Calculus
Monad
Type Systems
Algebraic Data Type
Result Type
Option Type
Persistent Data Structures
Referential Transparency
Higher-Order Functions
Function Composition
Lazy Evaluation
Tail Call Optimization
Hindley-Milner Type System
Actor Model
References
Hughes, John (1989). "Why Functional Programming Matters". The Computer Journal. 32 (2): 98–107.
Hudak, Paul (1989). "Conception, Evolution, and Application of Functional Programming Languages". ACM Computing Surveys. 21 (3): 359–411.
Moggi, Eugenio (1991). "Notions of computation and monads". Information and Computation. 93 (1): 55–92.
Pierce, Benjamin C. (2002). Types and Programming Languages. MIT Press.
External Links
Official Documentation / Language Sites
Key Papers / Historical Sources
Overview
Category: Programming | Programming Paradigms | Programming Languages | Software Engineering | WIKI
Footnotes
- McCarthy, John (1960). "Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I". Communications of the ACM. 3 (4): 184–195. ↩
- Landin, Peter J. (1966). "The Next 700 Programming Languages". Communications of the ACM. 9 (3): 157–166. ↩
- Backus, John (1978). "Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs". Communications of the ACM. 21 (8): 613–641. doi:10.1145/359576.359579. ↩
- Milner, Robin; Tofte, Mads; Harper, Robert; MacQueen, David (1997). *The Definition of Standard ML (Revised)*. MIT Press. ML's historical background connects to the LCF theorem prover. ↩
- Turner, David. The Miranda Programming Language. Miranda is a lazy/pure functional language with commercial support from Research Software Ltd. and directly influenced Haskell. ↩
- Hudak, Paul; Hughes, John; Peyton Jones, Simon; Wadler, Philip (2007). "A History of Haskell: Being Lazy With Class". HOPL III. ↩
- GHC User's Guide. "Bang patterns and Strict Haskell". GHC notes that lazy evaluation is the default, and thunk elimination and strictness control are critical in performance-sensitive code. ↩
- Wadler, Philip; Blott, Stephen (1989). "How to Make Ad-Hoc Polymorphism Less Ad Hoc". POPL '89: Proceedings of the 16th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages. pp. 60–76. doi:10.1145/75277.75283. ↩
- Wadler, Philip (1995). "Monads for functional programming". Advanced Functional Programming. Lecture Notes in Computer Science. Vol. 925. pp. 24–52. doi:10.1007/3-540-59451-5_2. Moggi's theoretical starting point should also be consulted in "Notions of Computation and Monads". ↩
- Moggi, Eugenio (1991). "Notions of computation and monads90052-4)". Information and Computation. 93 (1): 55–92. doi:10.1016/0890-5401(91)90052-4. ↩
- Okasaki, Chris (1998). *Purely Functional Data Structures*. Cambridge University Press. ISBN 0-521-66350-4. ↩
- Reed, Rick. "That's 'Billion' with a 'B': Scaling to the Next Level at WhatsApp". InfoQ / Erlang Factory SF Bay, 2014. WhatsApp's scaling case study based on Erlang and FreeBSD. ↩
- Jane Street. "Technology at Jane Street". Jane Street writes as many systems as possible in OCaml. ↩
- Meta Engineering. "Fighting spam with Haskell". Facebook/Meta's application of Haskell in the Sigma rule engine. ↩
- Twitter. "Effective Scala". Twitter's internal guide to Scala usage and functional/expression-oriented style. ↩
- Erlang Solutions. "Klarna case study". Klarna's high-availability payment system built on Erlang. ↩
- Hughes, John (1989). "Why Functional Programming Matters". The Computer Journal. 32 (2): 98–107. doi:10.1093/comjnl/32.2.98. Author's public version: Chalmers mirror. ↩
- Hudak, Paul (1989). "Conception, Evolution, and Application of Functional Programming Languages". ACM Computing Surveys. 21 (3): 359–411. doi:10.1145/72551.72554. ↩
- Pierce, Benjamin C. (2002). *Types and Programming Languages*. MIT Press. ISBN 978-0-262-16209-8. ↩