Functional Programming Concepts Across Languages

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:

  1. Same input always produces same output
  2. No side effects (doesn't modify external state)
  3. 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

  1. Immutability: Prefer immutable data structures, avoid mutations
  2. Pure Functions: Write functions without side effects for testability
  3. Composition: Build complex behavior from small, focused functions
  4. Higher-Order Functions: Use map/filter/reduce over loops
  5. Recursion: Use for tree/graph traversal, ensure base case
  6. 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.