OOP VS FP
Comparison Between Functional Programming (FP) and Object-Oriented Programming (OOP)
Functional Programming (FP)
- Focus: FP emphasizes the use of pure functions, immutable data, and a declarative style of programming.
- State Management: FP encourages the use of stateless, pure functions that transform input data into output data without modifying the original data.
- Composition: FP promotes the composition of small, reusable functions to build complex programs.
- Abstraction: FP focuses on abstraction through higher-order functions and function composition.
- Paradigm: FP is a paradigm that treats computation as the evaluation of mathematical functions.
Object-Oriented Programming (OOP)
- Focus: OOP focuses on the creation of objects, which are instances of classes, and the interactions between these objects.
- State Management: OOP allows for the management of state through the use of instance variables and methods within objects.
- Encapsulation: OOP promotes the encapsulation of data and behavior within objects, hiding implementation details from the outside world.
- Inheritance: OOP supports the concept of inheritance, where new classes can be derived from existing classes, inheriting their properties and behaviors.
- Paradigm: OOP is a paradigm that treats computation as the manipulation of objects.
Similarities
- Abstraction: Both FP and OOP promote the use of abstraction to manage complexity and provide reusable components.
- Modularity: Both paradigms encourage the decomposition of programs into smaller, modular units, whether they are functions or objects.
- Code Reuse: Both FP and OOP aim to facilitate code reuse, either through function composition or inheritance.
Differences
- State Management: FP favors immutable state and stateless functions, while OOP allows for the management of mutable state within objects.
- Composition vs. Inheritance: FP promotes function composition, while OOP emphasizes the use of inheritance to extend and reuse code.
- Paradigm: FP is based on the mathematical concept of functions, while OOP is based on the concept of objects and their interactions.
- 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.
- 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
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.
| 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.
| 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.
| 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
|
Goal: Take a “User” dictionary and create a formatted string for a profile.
Object-Oriented Approach
- The data and the formatting logic live together.
| 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.
| 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.
| 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.
| 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