Skip to content

OOP VS FP

Comparison Between Functional Programming (FP) and Object-Oriented Programming (OOP)

Functional Programming (FP)

  1. Focus: FP emphasizes the use of pure functions, immutable data, and a declarative style of programming.
  2. State Management: FP encourages the use of stateless, pure functions that transform input data into output data without modifying the original data.
  3. Composition: FP promotes the composition of small, reusable functions to build complex programs.
  4. Abstraction: FP focuses on abstraction through higher-order functions and function composition.
  5. Paradigm: FP is a paradigm that treats computation as the evaluation of mathematical functions.

Object-Oriented Programming (OOP)

  1. Focus: OOP focuses on the creation of objects, which are instances of classes, and the interactions between these objects.
  2. State Management: OOP allows for the management of state through the use of instance variables and methods within objects.
  3. Encapsulation: OOP promotes the encapsulation of data and behavior within objects, hiding implementation details from the outside world.
  4. Inheritance: OOP supports the concept of inheritance, where new classes can be derived from existing classes, inheriting their properties and behaviors.
  5. Paradigm: OOP is a paradigm that treats computation as the manipulation of objects.

Similarities

  1. Abstraction: Both FP and OOP promote the use of abstraction to manage complexity and provide reusable components.
  2. Modularity: Both paradigms encourage the decomposition of programs into smaller, modular units, whether they are functions or objects.
  3. Code Reuse: Both FP and OOP aim to facilitate code reuse, either through function composition or inheritance.

Differences

  1. State Management: FP favors immutable state and stateless functions, while OOP allows for the management of mutable state within objects.
  2. Composition vs. Inheritance: FP promotes function composition, while OOP emphasizes the use of inheritance to extend and reuse code.
  3. Paradigm: FP is based on the mathematical concept of functions, while OOP is based on the concept of objects and their interactions.
  4. Testability: FP’s emphasis on pure functions and immutable data can make it easier to write testable and predictable code, compared to the potential side effects of mutable state in OOP.
  5. Concurrency: FP’s focus on immutable data and stateless functions can make it easier to reason about and manage concurrent execution, compared to the potential challenges of shared mutable state in OOP.

Overview Summary

Aspect Object-Oriented Programming (OOP) Functional Programming (FP)
Philosophy “Tell the object to do something.” “Pass data through a pipe.”
Primary Concept Objects (data + behavior) Pure Functions (input -> output)
Logic Placement Inside classes (methods). Outside data (standalone functions).
State Management Emphasizes mutable state within objects Emphasizes immutable data, avoids state changes
Data Handling Methods modify self.data. Functions return new versions of data.
Code Structure Clear structure, programs broken into objects Concise, uses function composition and higher-order functions
Predictability Can be complex to manage state in large systems High predictability due to no side effects
Reusability Achieved through inheritance and interfaces Achieved through generic, pure functions

In essence, OOP is well-suited for modeling real-world entities and managing complex, stateful systems, while FP excels in situations requiring high predictability, testability, and parallel processing by minimizing side effects and shared state.

Fundamental Code Examples Contrasted

Data Transformation (Processing a List)

Goal: Take a list of prices, apply a 10% tax to each, and keep only the ones over $20.

Object-Oriented Approach
  • Uses an object to hold the logic and state of the “Tax Processor.”
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class TaxProcessor:
    def __init__(self, tax_rate):
        self.tax_rate = tax_rate

    def process_prices(self, prices):
        results = []
        for price in prices:
            taxed = price * (1 + self.tax_rate)
            if taxed > 20:
                results.append(taxed)
        return results

# Usage
processor = TaxProcessor(0.10)
final_prices = processor.process_prices([10, 25, 15, 30])
Functional Approach
  • Uses “Pure Functions” and list transformations.
1
2
3
4
5
6
7
8
prices = [10, 25, 15, 30]
TAX_RATE = 0.10

# A pure function: same input always yields same output
apply_tax = lambda p: p * (1 + TAX_RATE)

# Composing logic into a single pipeline
final_prices = list(filter(lambda p: p > 20, map(apply_tax, prices)))

Maintaining a Counter

Goal: Create a counter that can increment and tell you its current value.

Object-Oriented Approach
  • The state is “encapsulated” inside the object.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count

my_counter = Counter()
print(my_counter.increment()) # 1
print(my_counter.increment()) # 2
Functional Approach
  • State is “external.” Functions return a new value rather than changing the old one.
1
2
3
4
5
6
7
def increment(count):
    return count + 1

# The "state" is managed by the caller, not the function
count = 0
count = increment(count) # 1
count = increment(count) # 2

Reading and Formatting Data

Goal: Take a “User” dictionary and create a formatted string for a profile.

Object-Oriented Approach
  • The data and the formatting logic live together.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class User:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    def get_full_name(self):
        return f"{self.first} {self.last}"

user = User("Jane", "Doe")
print(user.get_full_name())
Functional Approach
  • Data is a simple structure; functions are “stateless” tools used to act on that structure.
1
2
3
4
5
6
user_data = {"first": "Jane", "last": "Doe"}

def format_name(data):
    return f"{data['first']} {data['last']}"

print(format_name(user_data))

Conditional Logic

Goal: Model a real world on/off switch.

Object-Oriented Approach
  • Switch state and behavior encapsulated in Swtich object. You don’t need to know its status to trigger a change.
1
2
3
4
5
6
7
8
9
class Switch:
    def __init__(self, s):
        self.s = s

    def toggle(self):
        self.s = not self.s

sw = Switch(True);
sw.toggle()`
Functional Approach
  • Predictable transformation of data. State is external, the function is a pure transformer with no knowledge of the switch.
1
2
3
4
toggle = lambda s: not s

is_on = True
is_on = toggle(is_on)

Design Pattern Code Examples Contrasted

I’ll compare how OOP design patterns translate to functional programming, showing both approaches in Python. The key insight: many OOP patterns exist to work around limitations in OOP languages, while FP often solves the same problems more directly.

Singleton

Problem: Ensure only one instance exists.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class DatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.connection = "Connected to DB"
        return cls._instance

db1 = DatabaseConnection()
db2 = DatabaseConnection()
assert db1 is db2  # Same instance
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Use a module-level variable or closure
_connection = None

def get_database_connection():
    global _connection
    if _connection is None:
        _connection = {"connection": "Connected to DB"}
    return _connection

db1 = get_database_connection()
db2 = get_database_connection()
assert db1 is db2

# Even simpler - modules are singletons in Python
# So pretend a module is database.py
connection = "Connected to DB"
  • Note: Python modules are naturally singletons, and if we need lazy initialization, a simple function with a closure suffices.

Strategy

Problem: Select algorithm at runtime.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount): pass

class CreditCard(PaymentStrategy):
    def pay(self, amount):
        return f"Paid ${amount} with credit card"

class PayPal(PaymentStrategy):
    def pay(self, amount):
        return f"Paid ${amount} with PayPal"

class ShoppingCart:
    def __init__(self, strategy: PaymentStrategy):
        self.strategy = strategy

    def checkout(self, amount):
        return self.strategy.pay(amount)

cart = ShoppingCart(CreditCard())
print(cart.checkout(100))
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Functions are first-class citizens - pass them directly.

def credit_card_payment(amount):
    return f"Paid ${amount} with credit card"

def paypal_payment(amount):
    return f"Paid ${amount} with PayPal"

def checkout(amount, payment_method):
    return payment_method(amount)

print(checkout(100, credit_card_payment))
print(checkout(100, paypal_payment))
  • OOP needs a class hierarchy for what FP does with functions. Functions themselves are strategies.

Command

Problem: Encapsulate requests as objects.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self): pass

    @abstractmethod
    def undo(self): pass

class LightOnCommand(Command):
    def __init__(self, light):
        self.light = light

    def execute(self):
        self.light.turn_on()

    def undo(self):
        self.light.turn_off()

class Light:
    def turn_on(self):
        print("Light is on")

    def turn_off(self):
        print("Light is off")

light = Light()
command = LightOnCommand(light)
command.execute()
command.undo()
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Commands are functions/closures

def create_light_commands(light_state):
    def turn_on():
        light_state['on'] = True
        print("Light is on")
        return turn_off  # Return undo function

    def turn_off():
        light_state['on'] = False
        print("Light is off")
        return turn_on  # Return undo function

    return turn_on, turn_off

light = {'on': False}
turn_on, turn_off = create_light_commands(light)

undo = turn_on()  # Execute, get undo
undo()            # Undo

Alternatively with partial application

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from functools import partial

def set_light(state, status):
    state['on'] = status
    print(f"Light is {'on' if status else 'off'}")

light = {'on': False}

turn_on = partial(set_light, light, True)
turn_off = partial(set_light, light, False)

turn_on()
turn_off()
  • Commands in OOP can be viewed as encapsulated, deferred function calls.
  • FP uses actual functions, closures, or partial application.

Observer

Problem: Notify MANY dependents about ONE state change.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from abc import ABC, abstractmethod

class Subject:
    def __init__(self):
        self._observers = []

    def register(self, observer):
        self._observers.append(observer)

    def unregister(self, observer):
        self._observers.remove(observer)

    def notify(self, data):
        for observer in self._observers:
            observer.update(data)

class Observer(ABC):
    @abstractmethod
    def update(self, data): pass

class EmailObserver(Observer):
    def update(self, data):
        print(f"Email: {data}")

class SMSObserver(Observer):
    def update(self, data):
        print(f"SMS: {data}")

subject = Subject()
emailObs = EmailObserver()
subject.register(emailObs)
subject.register(SMSObserver())
subject.notify("New message!")
subject.unregister(emailObs)
subject.notify("Another message!")
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# FP treats observers as a list of functions

def create_observable():
    observers = []

    def subscribe(fn):
        observers.append(fn)
        return lambda: observers.remove(fn)  # Return unsubscribe

    def notify(data):
        for observer in observers:
            observer(data)

    return subscribe, notify

subscribe, notify = create_observable()

# Observers are just functions
def email_handler(data):
    print(f"Email: {data}")

def sms_handler(data):
    print(f"SMS: {data}")

unsubscribe_email = subscribe(email_handler)
subscribe(sms_handler)

notify("New message!")
unsubscribe_email()
notify("Another message!")
  • Note: Observers in FP are callback functions. No interface necessary since functions are objects.

Decorator

Problem: Add behavior to objects dynamically.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from abc import ABC, abstractmethod

class Coffee(ABC):
    @abstractmethod
    def cost(self): pass

    @abstractmethod
    def description(self): pass

class SimpleCoffee(Coffee):
    def cost(self):
        return 5

    def description(self):
        return "Simple coffee"

class CoffeeDecorator(Coffee):
    def __init__(self, coffee: Coffee):
        self._coffee = coffee

class Milk(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 1

    def description(self):
        return self._coffee.description() + ", milk"

class Sugar(CoffeeDecorator):
    def cost(self):
        return self._coffee.cost() + 0.5

    def description(self):
        return self._coffee.description() + ", sugar"

coffee = SimpleCoffee()
coffee = Milk(Sugar(coffee))
print(f"{coffee.description()}: ${coffee.cost()}"
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Use Python decorators or function composition

def base_coffee():
    return {"description": "Simple coffee", "cost": 5}

def with_milk(coffee_fn):
    def decorated():
        coffee = coffee_fn()
        return {
            "description": coffee["description"] + ", milk",
            "cost": coffee["cost"] + 1
        }
    return decorated

def with_sugar(coffee_fn):
    def decorated():
        coffee = coffee_fn()
        return {
            "description": coffee["description"] + ", sugar",
            "cost": coffee["cost"] + 0.5
        }
    return decorated

@with_milk
@with_sugar
def my_coffee():
    return base_coffee()

coffee = my_coffee()
print(f"{coffee['description']}: ${coffee['cost']}")

Or with function composition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def base_coffee():
    return {"description": "Simple coffee", "cost": 5}

def compose(*functions):
    def inner(arg):
        result = arg
        for fn in reversed(functions):
            result = fn(result)
        return result
    return inner

def add_milk(coffee):
    return {**coffee, 
            "description": coffee["description"] + ", milk",
            "cost": coffee["cost"] + 1}

def add_sugar(coffee):
    return {**coffee,
            "description": coffee["description"] + ", sugar",
            "cost": coffee["cost"] + 0.5}

make_coffee = compose(add_milk, add_sugar)
coffee = make_coffee(base_coffee())
  • Note: FP uses built in language decorators and function composition instead of object composition and shared interfaces.

Factory & Abstract Factory

Problem: Create objects without specifying exact class.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from abc import ABC, abstractmethod

class Button(ABC):
    @abstractmethod
    def render(self): pass

class WindowsButton(Button):
    def render(self):
        return "Windows Button"

class MacButton(Button):
    def render(self):
        return "Mac Button"

class GUIFactory(ABC):
    @abstractmethod
    def create_button(self): pass

    @staticmethod
    def get_gui_factory(platform):
        if platform == 'windows':
            return WindowsFactory()
        elif platform == 'mac':
            return MacFactory()

class WindowsFactory(GUIFactory):
    def create_button(self):
        return WindowsButton()

class MacFactory(GUIFactory):
    def create_button(self):
        return MacButton()

factory = GUIFactory.get_gui_factory('windows')
button = factory.create_button()
print(button.render())
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Return data or functions depending on need/intent

def create_button(platform):
    buttons = {
        'windows': lambda: "Windows Button",
        'mac': lambda: "Mac Button"
    }
    return buttons[platform]()

# Or with factory functions
def windows_gui():
    return {
        'button': lambda: "Windows Button",
        'checkbox': lambda: "Windows Checkbox"
    }

def mac_gui():
    return {
        'button': lambda: "Mac Button",
        'checkbox': lambda: "Mac Checkbox"
    }

def create_gui(platform):
    factories = {'windows': windows_gui, 'mac': mac_gui}
    return factories[platform]()

gui = create_gui('windows')
print(gui['button']())
  • Note: In FP, factories are functions that return data or other functions. No class hierarchy needed.

Template Method

Problem: Define algorithm skeleton, let subclasses override steps.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from abc import ABC, abstractmethod

class DataProcessor(ABC):
    def process(self):
        data = self.read_data()
        processed = self.process_data(data)
        self.save_data(processed)

    @abstractmethod
    def read_data(self): pass

    @abstractmethod
    def process_data(self, data): pass

    @abstractmethod
    def save_data(self, data): pass

class CSVProcessor(DataProcessor):
    def read_data(self):
        return "CSV data"

    def process_data(self, data):
        return data.upper()

    def save_data(self, data):
        print(f"Saved: {data}")
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Use Higher-order function that takes the varying parts as arguments

def process_data(read_fn, process_fn, save_fn):
    data = read_fn()
    processed = process_fn(data)
    save_fn(processed)

def read_csv():
    return "CSV data"

def uppercase(data):
    return data.upper()

def save_to_disk(data):
    print(f"Saved: {data}")

# Use it
process_data(read_csv, uppercase, save_to_disk)

# Or with partial application for reusable processors
from functools import partial

csv_processor = partial(process_data, read_csv, uppercase, save_to_disk)
csv_processor()
  • Note: FP passes variable behavior as function arguments. The “template” is a higher-order function.

Adapter

Problem: Make incompatible interfaces work together.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class EuropeanSocket:
    def provide_220v(self):
        return "220V"

class USADevice:
    def connect_to_110v(self, voltage):
        print(f"Running on {voltage}")

class Adapter:
    def __init__(self, socket: EuropeanSocket):
        self.socket = socket

    def provide_110v(self):
        voltage = self.socket.provide_220v()
        return "110V"  # Conversion logic

socket = EuropeanSocket()
adapter = Adapter(socket)
device = USADevice()
device.connect_to_110v(adapter.provide_110v())
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Write an adapter function

def european_socket():
    return "220V"

def usa_device(voltage):
    print(f"Running on {voltage}")

def voltage_adapter(socket_fn):
    voltage = socket_fn()
    return "110V"  # Conversion logic

usa_device(voltage_adapter(european_socket))

# Or use a decorator
def adapt_voltage(device_fn):
    def adapted(socket_fn):
        voltage = voltage_adapter(socket_fn)
        return device_fn(voltage)
    return adapted

@adapt_voltage
def my_device(voltage):
    print(f"Running on {voltage}")

my_device(european_socket)
  • Note: Adapters in FP are transformation functions instead of composed object adapters of the same interface.

Facade

Problem: Provide simplified interface to complex subsystem.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CPU:
    def freeze(self): pass
    def execute(self): pass

class Memory:
    def load(self): pass

class HardDrive:
    def read(self): pass

class ComputerFacade:
    def __init__(self):
        self.cpu = CPU()
        self.memory = Memory()
        self.hd = HardDrive()

    def start(self):
        self.cpu.freeze()
        self.memory.load()
        self.hd.read()
        self.cpu.execute()

computer = ComputerFacade()
computer.start()
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Compose the functions

def freeze_cpu():
    print("CPU frozen")

def load_memory():
    print("Memory loaded")

def read_hard_drive():
    print("Hard drive read")

def execute_cpu():
    print("CPU executing")

def start_computer():
    freeze_cpu()
    load_memory()
    read_hard_drive()
    execute_cpu()

start_computer()  # Simple function composition
  • Note: A facade is just a function that calls other functions.

Iterator

Problem: Access elements sequentially without exposing structure.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class BookCollection:
    def __init__(self):
        self.books = []

    def add_book(self, book):
        self.books.append(book)

    def __iter__(self):
        return BookIterator(self.books)

class BookIterator:
    def __init__(self, books):
        self.books = books
        self.index = 0

    def __next__(self):
        if self.index >= len(self.books):
            raise StopIteration
        book = self.books[self.index]
        self.index += 1
        return book

    def __iter__(self):
        return self

collection = BookCollection()
collection.add_book("Book 1")
for book in collection:
    print(book)
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Use generators - generators ARE iterators

def book_collection(books):
    for book in books:
        yield book

# Or with lazy evaluation
def lazy_reader(filename):
    with open(filename) as f:
        for line in f:
            yield line.strip()

# Higher-order iteration
def filter_books(books, predicate):
    for book in books:
        if predicate(book):
            yield book

books = ["Book 1", "Book 2", "Short"]
for book in filter_books(books, lambda b: len(b) > 6):
    print(book)
  • Note: FP uses generators and lazy sequences. Iterators are built into the FP language paradigm.

Composite

Problem: Treat individual objects and compositions uniformly.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from abc import ABC, abstractmethod

class Component(ABC):
    @abstractmethod
    def operation(self): pass

class Leaf(Component):
    def __init__(self, name):
        self.name = name

    def operation(self):
        return self.name

class Composite(Component):
    def __init__(self):
        self.children = []

    def add(self, component):
        self.children.append(component)

    def operation(self):
        results = [child.operation() for child in self.children]
        return f"Branch({', '.join(results)})"

tree = Composite()
tree.add(Leaf("Leaf1"))
tree.add(Leaf("Leaf2"))
print(tree.operation())
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Use recursive data structures with pattern matching

def evaluate(tree):
    if isinstance(tree, str):  # Leaf
        return tree
    elif isinstance(tree, list):  # Composite
        results = [evaluate(child) for child in tree]
        return f"Branch({', '.join(results)})"

tree = [
    "Leaf1",
    "Leaf2",
    ["Leaf3", "Leaf4"]
]

print(evaluate(tree))

# Or with tagged unions (using dicts)
def evaluate_tagged(tree):
    match tree['type']:
        case 'leaf':
            return tree['value']
        case 'branch':
            results = [evaluate_tagged(child) for child in tree['children']]
            return f"Branch({', '.join(results)})"

tree = {
    'type': 'branch',
    'children': [
        {'type': 'leaf', 'value': 'Leaf1'},
        {'type': 'leaf', 'value': 'Leaf2'}
    ]
}
  • Note: FP uses recursive data structures. No need for class hierarchies.

Builder

Problem: Construct complex objects step by step.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class House:
    def __init__(self):
        self.walls = None
        self.roof = None
        self.windows = None

class HouseBuilder:
    def __init__(self):
        self.house = House()

    def build_walls(self):
        self.house.walls = "Walls"
        return self

    def build_roof(self):
        self.house.roof = "Roof"
        return self

    def build_windows(self):
        self.house.windows = "Windows"
        return self

    def get_house(self):
        return self.house

house = (HouseBuilder()
         .build_walls()
         .build_roof()
         .build_windows()
         .get_house())
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Use function chaining or reduce

def build_house(**options):
    return options

def with_walls(house):
    return {**house, 'walls': 'Walls'}

def with_roof(house):
    return {**house, 'roof': 'Roof'}

def with_windows(house):
    return {**house, 'windows': 'Windows'}

# Functional pipeline
from functools import reduce

def build(*steps):
    def builder(initial=None):
        return reduce(lambda acc, fn: fn(acc), steps, initial or {})
    return builder

house = build(with_walls, with_roof, with_windows)()

# Or even simpler - just use kwargs
house = build_house(walls='Walls', roof='Roof', windows='Windows')
  • Note: FP uses function composition or immutable updates.

Pipeline

Problem: Process data through sequence of operations.

OOP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from abc import ABC, abstractmethod

class Pipeline:
    def __init__(self):
        self.stages = []

    def add_stage(self, stage):
        self.stages.append(stage)
        return self

    def execute(self, data):
        result = data
        for stage in self.stages:
            result = stage.process(result)
        return result

class Stage(ABC):
    @abstractmethod
    def process(self, data): pass

class UppercaseStage(Stage):
    def process(self, data):
        return data.upper()

pipeline = Pipeline()
pipeline.add_stage(UppercaseStage())
result = pipeline.execute("hello")
FP Approach
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# This is what FP is designed for!

from functools import reduce

def pipeline(*functions):
    def process(data):
        return reduce(lambda acc, fn: fn(acc), functions, data)
    return process

def uppercase(text):
    return text.upper()

def add_exclamation(text):
    return text + "!"

def reverse(text):
    return text[::-1]

process = pipeline(uppercase, add_exclamation, reverse)
result = process("hello")
print(result)  # "!OLLEH"

# Or using compose
def compose(*functions):
    return lambda x: reduce(lambda acc, fn: fn(acc), functions, x)

# Or the pythonic way
text = "hello"
result = text.upper()
result = result + "!"
result = result[::-1]

# Or method chaining (if available)
result = ("hello"
          .upper()
          .__add__("!")
          .__getitem__(slice(None, None, -1)))
  • Note: Pipelines ARE functional programming. compose/reduce is the FP pattern.

Executive Summary

“Objects are a poor man’s closures, and closures are a poor man’s objects.”

  • OOP → FP mapping
  • Singleton → Module-level variables
  • Strategy → Functions as arguments
  • Command → Closures/partial application
  • Observer → List of Callback functions
  • Decorator → Built-in Decorators or Function composition
  • Factory → Functions returning data or functions
  • Template Method → Higher-order functions
  • Adapter → Transformation functions
  • Facade → Function composition
  • Iterator → Generators/lazy sequences
  • Composite → Recursive data structures
  • Builder → Immutable updates/reduce
  • Pipeline → compose/reduce