Functional Programming Concepts Across Languages
Introduction
Functional programming emphasizes immutability, pure functions, and declarative code over imperative loops and state mutations. This guide covers core concepts—immutability, pure functions, higher-order functions, composition, closures, recursion, and monads—with practical examples in JavaScript, Python, C#, and TypeScript.
Immutability
Why Immutability Matters
Problems with mutable state:
// JavaScript: Mutable state causes bugs
let users = [
{ id: 1, name: "Alice", role: "admin" },
{ id: 2, name: "Bob", role: "user" }
];
function promoteUser(users, userId) {
const user = users.find(u => u.id === userId);
user.role = "admin"; // ❌ Mutates original array
return users;
}
const updatedUsers = promoteUser(users, 2);
console.log(users[1].role); // "admin" - original modified!
Immutable approach:
// ✅ JavaScript: Return new array
function promoteUser(users, userId) {
return users.map(user =>
user.id === userId
? { ...user, role: "admin" } // New object
: user // Keep original
);
}
const updatedUsers = promoteUser(users, 2);
console.log(users[1].role); // "user" - unchanged
console.log(updatedUsers[1].role); // "admin"
Immutable Data Structures
JavaScript/TypeScript:
// Object spread for shallow copy
const original = { name: "Alice", age: 30 };
const updated = { ...original, age: 31 };
// Array spread
const numbers = [1, 2, 3];
const extended = [...numbers, 4, 5];
// Nested immutable update
interface User {
name: string;
address: {
city: string;
zip: string;
};
}
const user: User = {
name: "Alice",
address: { city: "New York", zip: "10001" }
};
// Update nested property immutably
const updatedUser: User = {
...user,
address: {
...user.address,
city: "Boston"
}
};
// Using Immer library for complex updates
import produce from "immer";
const newUser = produce(user, draft => {
draft.address.city = "Boston"; // Looks mutable but creates new object
});
Python:
# Tuples are immutable
coordinates = (40.7128, -74.0060)
# coordinates[0] = 41 # TypeError: tuple doesn't support item assignment
# Immutable data with dataclasses
from dataclasses import dataclass, replace
@dataclass(frozen=True) # frozen=True makes immutable
class User:
name: str
age: int
email: str
user = User(name="Alice", age=30, email="alice@example.com")
# user.age = 31 # FrozenInstanceError
# Create modified copy
updated_user = replace(user, age=31)
print(user.age) # 30 - unchanged
print(updated_user.age) # 31
# Immutable collections
from typing import FrozenSet
allowed_roles: FrozenSet[str] = frozenset(["admin", "user", "guest"])
# allowed_roles.add("superuser") # AttributeError
C#:
// Immutable record types (C# 9+)
public record User(string Name, int Age, string Email);
var user = new User("Alice", 30, "alice@example.com");
// user.Age = 31; // Compile error: init-only property
// Create modified copy with 'with' expression
var updatedUser = user with { Age = 31 };
Console.WriteLine(user.Age); // 30 - unchanged
Console.WriteLine(updatedUser.Age); // 31
// Immutable collections
using System.Collections.Immutable;
var numbers = ImmutableList.Create(1, 2, 3);
var extended = numbers.Add(4); // Returns new list
Console.WriteLine(numbers.Count); // 3
Console.WriteLine(extended.Count); // 4
// ImmutableDictionary
var users = ImmutableDictionary<int, string>.Empty
.Add(1, "Alice")
.Add(2, "Bob");
var updated = users.SetItem(1, "Alice Smith");
Pure Functions
Definition and Benefits
Pure function characteristics:
- Same input always produces same output
- No side effects (doesn't modify external state)
- Doesn't depend on external state
Impure vs Pure:
// ❌ Impure: Depends on external state
let taxRate = 0.08;
function calculateTotalImpure(price) {
return price * (1 + taxRate); // Depends on global variable
}
// ✅ Pure: All inputs as parameters
function calculateTotal(price, taxRate) {
return price * (1 + taxRate);
}
// ❌ Impure: Has side effect
let log = [];
function addAndLogImpure(a, b) {
const result = a + b;
log.push(result); // Side effect: modifies external array
return result;
}
// ✅ Pure: Returns both result and new log
function addAndLog(a, b, log) {
const result = a + b;
return {
result,
log: [...log, result] // Returns new log, doesn't mutate
};
}
Pure Functions Across Languages
Python:
# ❌ Impure: Modifies input
def add_item_impure(items: list, item: str) -> list:
items.append(item) # Mutates input
return items
# ✅ Pure: Returns new list
def add_item(items: list, item: str) -> list:
return [*items, item]
# Pure function for data transformation
def apply_discount(price: float, discount: float) -> float:
"""Calculate discounted price.
Pure function: No side effects, predictable output.
"""
if discount < 0 or discount > 1:
raise ValueError("Discount must be between 0 and 1")
return price * (1 - discount)
# Testable and cacheable due to purity
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
"""Pure recursive function with memoization."""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
C#:
// Pure function with LINQ
public static IEnumerable<int> FilterEvens(IEnumerable<int> numbers)
{
return numbers.Where(n => n % 2 == 0);
}
// Pure function for validation
public record ValidationResult(bool IsValid, string[] Errors);
public static ValidationResult ValidateEmail(string email)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(email))
errors.Add("Email is required");
if (!email.Contains("@"))
errors.Add("Email must contain @");
return new ValidationResult(errors.Count == 0, errors.ToArray());
}
// Usage
var result = ValidateEmail("alice@example.com");
if (!result.IsValid)
{
Console.WriteLine(string.Join(", ", result.Errors));
}
Higher-Order Functions
Functions as First-Class Citizens
Functions as arguments:
// JavaScript: Array methods are higher-order functions
const numbers = [1, 2, 3, 4, 5];
// map takes function as argument
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
// filter takes function as argument
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]
// reduce takes function as argument
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15
// Custom higher-order function
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, i => console.log(`Iteration ${i}`));
// Output:
// Iteration 0
// Iteration 1
// Iteration 2
Functions returning functions:
# Python: Function factory
def create_multiplier(factor):
"""Returns function that multiplies by factor."""
def multiply(x):
return x * factor
return multiply
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Decorator as higher-order function
from functools import wraps
import time
def timing_decorator(func):
"""Measure function execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.2f}s")
return result
return wrapper
@timing_decorator
def slow_function():
time.sleep(1)
return "Done"
slow_function() # Prints: slow_function took 1.00s
C# delegates and lambdas:
// Higher-order function taking delegate
public static List<T> Filter<T>(List<T> items, Func<T, bool> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
result.Add(item);
}
return result;
}
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evens = Filter(numbers, n => n % 2 == 0); // [2, 4]
// Function returning function
public static Func<int, int> CreateAdder(int x)
{
return y => x + y;
}
var add5 = CreateAdder(5);
Console.WriteLine(add5(3)); // 8
Console.WriteLine(add5(10)); // 15
Function Composition
Composing Functions
JavaScript/TypeScript:
// Compose functions right-to-left
const compose = <T>(...fns: Array<(arg: T) => T>) => (x: T): T =>
fns.reduceRight((acc, fn) => fn(acc), x);
// Pipe functions left-to-right
const pipe = <T>(...fns: Array<(arg: T) => T>) => (x: T): T =>
fns.reduce((acc, fn) => fn(acc), x);
// Example: String processing pipeline
const trim = (str: string) => str.trim();
const toLowerCase = (str: string) => str.toLowerCase();
const removeSpaces = (str: string) => str.replace(/\s+/g, "-");
const slugify = pipe(trim, toLowerCase, removeSpaces);
console.log(slugify(" Hello World ")); // "hello-world"
// Compose for data transformation
interface User {
name: string;
age: number;
}
const incrementAge = (user: User): User =>
({ ...user, age: user.age + 1 });
const uppercase Name = (user: User): User =>
({ ...user, name: user.name.toUpperCase() });
const transformUser = compose(uppercaseName, incrementAge);
const user = { name: "alice", age: 30 };
console.log(transformUser(user)); // { name: "ALICE", age: 31 }
Python with functools:
from functools import reduce
from typing import Callable, TypeVar
T = TypeVar('T')
def compose(*fns: Callable[[T], T]) -> Callable[[T], T]:
"""Compose functions right-to-left."""
def composed(x: T) -> T:
return reduce(lambda acc, fn: fn(acc), reversed(fns), x)
return composed
def pipe(*fns: Callable[[T], T]) -> Callable[[T], T]:
"""Compose functions left-to-right."""
def piped(x: T) -> T:
return reduce(lambda acc, fn: fn(acc), fns, x)
return piped
# Example: Data processing pipeline
def remove_duplicates(items: list) -> list:
return list(dict.fromkeys(items))
def sort_items(items: list) -> list:
return sorted(items)
def take_first_three(items: list) -> list:
return items[:3]
process = pipe(remove_duplicates, sort_items, take_first_three)
data = [3, 1, 4, 1, 5, 9, 2, 6, 5]
print(process(data)) # [1, 2, 3]
Map, Filter, Reduce
Universal Patterns
JavaScript:
const products = [
{ name: "Laptop", price: 999, category: "Electronics" },
{ name: "Desk", price: 299, category: "Furniture" },
{ name: "Phone", price: 699, category: "Electronics" },
{ name: "Chair", price: 199, category: "Furniture" }
];
// map: Transform each element
const prices = products.map(p => p.price);
// [999, 299, 699, 199]
// filter: Keep elements matching condition
const electronics = products.filter(p => p.category === "Electronics");
// [{ name: "Laptop", ... }, { name: "Phone", ... }]
// reduce: Aggregate to single value
const total = products.reduce((sum, p) => sum + p.price, 0);
// 2196
// Chaining operations
const expensiveElectronicsTotal = products
.filter(p => p.category === "Electronics")
.filter(p => p.price > 700)
.map(p => p.price)
.reduce((sum, price) => sum + price, 0);
// 1698 (999 + 699)
Python:
products = [
{"name": "Laptop", "price": 999, "category": "Electronics"},
{"name": "Desk", "price": 299, "category": "Furniture"},
{"name": "Phone", "price": 699, "category": "Electronics"},
{"name": "Chair", "price": 199, "category": "Furniture"}
]
# map: Transform each element
prices = list(map(lambda p: p["price"], products))
# [999, 299, 699, 199]
# filter: Keep elements matching condition
electronics = list(filter(lambda p: p["category"] == "Electronics", products))
# reduce: Aggregate
from functools import reduce
total = reduce(lambda sum, p: sum + p["price"], products, 0)
# 2196
# List comprehensions (Pythonic alternative)
prices = [p["price"] for p in products]
electronics = [p for p in products if p["category"] == "Electronics"]
total = sum(p["price"] for p in products)
C# LINQ:
var products = new List<Product>
{
new("Laptop", 999, "Electronics"),
new("Desk", 299, "Furniture"),
new("Phone", 699, "Electronics"),
new("Chair", 199, "Furniture")
};
// Select = map
var prices = products.Select(p => p.Price).ToList();
// [999, 299, 699, 199]
// Where = filter
var electronics = products.Where(p => p.Category == "Electronics").ToList();
// Aggregate = reduce
var total = products.Aggregate(0, (sum, p) => sum + p.Price);
// 2196
// Method chaining (fluent API)
var expensiveElectronicsTotal = products
.Where(p => p.Category == "Electronics")
.Where(p => p.Price > 700)
.Sum(p => p.Price);
// 1698
record Product(string Name, int Price, string Category);
Recursion
Recursive Problem Solving
Factorial:
// JavaScript: Recursive factorial
function factorial(n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive case
}
console.log(factorial(5)); // 120
// Tail-call optimized version
function factorialTCO(n, acc = 1) {
if (n <= 1) return acc;
return factorialTCO(n - 1, n * acc);
}
Tree traversal:
# Python: Recursive tree traversal
from dataclasses import dataclass
from typing import Optional
@dataclass
class TreeNode:
value: int
left: Optional['TreeNode'] = None
right: Optional['TreeNode'] = None
def sum_tree(node: Optional[TreeNode]) -> int:
"""Calculate sum of all node values."""
if node is None:
return 0
return node.value + sum_tree(node.left) + sum_tree(node.right)
def find_max(node: Optional[TreeNode]) -> int:
"""Find maximum value in tree."""
if node is None:
return float('-inf')
left_max = find_max(node.left)
right_max = find_max(node.right)
return max(node.value, left_max, right_max)
# Example tree
tree = TreeNode(10,
TreeNode(5, TreeNode(3), TreeNode(7)),
TreeNode(15, TreeNode(12), TreeNode(20))
)
print(sum_tree(tree)) # 72
print(find_max(tree)) # 20
C# pattern matching:
// Recursive sum with pattern matching
public static int Sum(IEnumerable<int> numbers)
{
return numbers switch
{
[] => 0, // Empty list
[var first, .. var rest] => first + Sum(rest), // Head + recursive tail
_ => throw new ArgumentException()
};
}
// Recursive directory processing
public static long GetDirectorySize(string path)
{
var directory = new DirectoryInfo(path);
var filesSize = directory.GetFiles()
.Sum(file => file.Length);
var subdirectoriesSize = directory.GetDirectories()
.Sum(subdir => GetDirectorySize(subdir.FullName));
return filesSize + subdirectoriesSize;
}
Closures
Lexical Scoping
JavaScript:
function createCounter() {
let count = 0; // Private variable
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(counter.count); // undefined (private)
// Partial application with closures
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Python:
def create_accumulator(initial=0):
"""Closure for accumulating values."""
total = initial
def add(value):
nonlocal total # Access outer variable
total += value
return total
return add
acc = create_accumulator(10)
print(acc(5)) # 15
print(acc(3)) # 18
print(acc(2)) # 20
# Decorator using closure
def memoize(func):
"""Cache function results."""
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def expensive_calculation(n):
print(f"Computing {n}...")
return n ** 2
print(expensive_calculation(5)) # Computes and caches
print(expensive_calculation(5)) # Uses cache (no print)
Monads (Optional/Maybe)
Handling Nullable Values
TypeScript Optional:
// Custom Optional monad
class Optional<T> {
private constructor(private value: T | null) {}
static of<T>(value: T | null): Optional<T> {
return new Optional(value);
}
static empty<T>(): Optional<T> {
return new Optional<T>(null);
}
map<U>(fn: (value: T) => U): Optional<U> {
return this.value !== null
? Optional.of(fn(this.value))
: Optional.empty();
}
flatMap<U>(fn: (value: T) => Optional<U>): Optional<U> {
return this.value !== null
? fn(this.value)
: Optional.empty();
}
getOrElse(defaultValue: T): T {
return this.value !== null ? this.value : defaultValue;
}
isPresent(): boolean {
return this.value !== null;
}
}
// Usage
interface User {
name: string;
email?: string;
}
function getUser(id: number): Optional<User> {
const users: Record<number, User> = {
1: { name: "Alice", email: "alice@example.com" },
2: { name: "Bob" }
};
return Optional.of(users[id] ?? null);
}
const user = getUser(1)
.map(u => u.email)
.map(email => email.toUpperCase())
.getOrElse("NO EMAIL");
console.log(user); // "ALICE@EXAMPLE.COM"
const noEmail = getUser(2)
.map(u => u.email)
.map(email => email.toUpperCase())
.getOrElse("NO EMAIL");
console.log(noEmail); // "NO EMAIL"
C# with null-conditional operator:
// Functional null handling
public record Address(string City, string? Zip);
public record User(string Name, Address? Address);
// Chain safely without null checks
string GetUserZip(User? user) =>
user?.Address?.Zip ?? "Unknown";
// LINQ for optional values
IEnumerable<string> GetAllZipCodes(IEnumerable<User> users) =>
users
.Where(u => u.Address != null)
.Select(u => u.Address!.Zip)
.Where(zip => zip != null)
.Cast<string>();
Best Practices
- Immutability: Prefer immutable data structures, avoid mutations
- Pure Functions: Write functions without side effects for testability
- Composition: Build complex behavior from small, focused functions
- Higher-Order Functions: Use map/filter/reduce over loops
- Recursion: Use for tree/graph traversal, ensure base case
- Closures: Encapsulate private state, create function factories
Key Takeaways
- Immutability prevents bugs from unexpected state changes
- Pure functions are predictable, testable, and cacheable
- Higher-order functions enable abstraction and reusability
- Function composition builds complex logic from simple pieces
- Map/filter/reduce provide declarative data transformation
- Closures enable data privacy and function factories
Next Steps
- Explore Ramda.js or fp-ts for functional JavaScript libraries
- Learn F# or Haskell for pure functional languages
- Study Category Theory for advanced functional concepts
- Master Async Monads (Promise/Task) for functional async code
Additional Resources
Think in transformations, not mutations.