functional_patterns.py

Download
python 538 lines 14.2 KB
  1"""
  2Functional Programming Patterns in Python
  3
  4Key concepts:
  51. Pure Functions - no side effects, same input = same output
  62. Higher-Order Functions - functions that take/return functions
  73. Closures - functions that capture variables from outer scope
  84. Function Composition - combining simple functions to build complex ones
  95. Currying - transforming multi-argument function to chain of single-argument
 106. Map/Filter/Reduce - fundamental data transformation operations
 11"""
 12
 13from typing import Callable, List, TypeVar, Any
 14from functools import reduce, partial
 15import operator
 16
 17T = TypeVar('T')
 18U = TypeVar('U')
 19
 20
 21# =============================================================================
 22# 1. PURE FUNCTIONS
 23# No side effects, deterministic output
 24# =============================================================================
 25
 26print("=" * 70)
 27print("1. PURE FUNCTIONS")
 28print("=" * 70)
 29
 30
 31# ❌ IMPURE: Has side effects, depends on external state
 32counter = 0
 33
 34
 35def impure_increment(x: int) -> int:
 36    """Impure: modifies global state"""
 37    global counter
 38    counter += 1  # Side effect!
 39    return x + counter  # Depends on external state!
 40
 41
 42# ✅ PURE: No side effects, deterministic
 43def pure_add(x: int, y: int) -> int:
 44    """Pure: same inputs always give same output"""
 45    return x + y
 46
 47
 48def pure_square(x: float) -> float:
 49    """Pure: mathematical function"""
 50    return x * x
 51
 52
 53def pure_capitalize_words(text: str) -> str:
 54    """Pure: transforms input without side effects"""
 55    return ' '.join(word.capitalize() for word in text.split())
 56
 57
 58# =============================================================================
 59# 2. HIGHER-ORDER FUNCTIONS
 60# Functions that take or return other functions
 61# =============================================================================
 62
 63print("\n" + "=" * 70)
 64print("2. HIGHER-ORDER FUNCTIONS")
 65print("=" * 70)
 66
 67
 68# Function that takes another function as argument
 69def apply_twice(func: Callable[[T], T], value: T) -> T:
 70    """Apply function twice to a value"""
 71    return func(func(value))
 72
 73
 74def apply_n_times(n: int) -> Callable[[Callable[[T], T], T], T]:
 75    """Returns a function that applies func n times"""
 76
 77    def applier(func: Callable[[T], T], value: T) -> T:
 78        result = value
 79        for _ in range(n):
 80            result = func(result)
 81        return result
 82
 83    return applier
 84
 85
 86# Function that returns another function
 87def make_multiplier(factor: int) -> Callable[[int], int]:
 88    """Returns a function that multiplies by factor"""
 89
 90    def multiplier(x: int) -> int:
 91        return x * factor
 92
 93    return multiplier
 94
 95
 96def make_adder(n: int) -> Callable[[int], int]:
 97    """Returns a function that adds n"""
 98
 99    def adder(x: int) -> int:
100        return x + n
101
102    return adder
103
104
105# =============================================================================
106# 3. CLOSURES
107# Functions that capture variables from their enclosing scope
108# =============================================================================
109
110print("\n" + "=" * 70)
111print("3. CLOSURES")
112print("=" * 70)
113
114
115def make_counter(start: int = 0) -> Callable[[], int]:
116    """Returns a counter function with private state"""
117    count = start  # Captured by closure
118
119    def counter() -> int:
120        nonlocal count  # Allows modification of captured variable
121        count += 1
122        return count
123
124    return counter
125
126
127def make_greeting(greeting: str) -> Callable[[str], str]:
128    """Returns a greeting function that remembers the greeting"""
129
130    def greet(name: str) -> str:
131        return f"{greeting}, {name}!"
132
133    return greet
134
135
136def make_accumulator(initial: float = 0.0) -> Callable[[float], float]:
137    """Returns a function that accumulates values"""
138    total = initial
139
140    def accumulate(value: float) -> float:
141        nonlocal total
142        total += value
143        return total
144
145    return accumulate
146
147
148# =============================================================================
149# 4. FUNCTION COMPOSITION
150# Building complex functions from simple ones
151# =============================================================================
152
153print("\n" + "=" * 70)
154print("4. FUNCTION COMPOSITION")
155print("=" * 70)
156
157
158def compose(*functions: Callable) -> Callable:
159    """
160    Compose functions right to left: compose(f, g, h)(x) = f(g(h(x)))
161    """
162
163    def inner(arg):
164        result = arg
165        for func in reversed(functions):
166            result = func(result)
167        return result
168
169    return inner
170
171
172def pipe(*functions: Callable) -> Callable:
173    """
174    Compose functions left to right: pipe(f, g, h)(x) = h(g(f(x)))
175    More intuitive for data pipelines
176    """
177
178    def inner(arg):
179        result = arg
180        for func in functions:
181            result = func(result)
182        return result
183
184    return inner
185
186
187# Example transformation functions
188def remove_spaces(text: str) -> str:
189    return text.replace(" ", "")
190
191
192def to_uppercase(text: str) -> str:
193    return text.upper()
194
195
196def add_exclamation(text: str) -> str:
197    return text + "!"
198
199
200def double(x: int) -> int:
201    return x * 2
202
203
204def increment(x: int) -> int:
205    return x + 1
206
207
208# =============================================================================
209# 5. CURRYING
210# Transform multi-argument function to chain of single-argument functions
211# =============================================================================
212
213print("\n" + "=" * 70)
214print("5. CURRYING")
215print("=" * 70)
216
217
218# Regular function
219def add_three_numbers(a: int, b: int, c: int) -> int:
220    return a + b + c
221
222
223# Curried version
224def curried_add(a: int) -> Callable[[int], Callable[[int], int]]:
225    def add_b(b: int) -> Callable[[int], int]:
226        def add_c(c: int) -> int:
227            return a + b + c
228
229        return add_c
230
231    return add_b
232
233
234# Generic curry function
235def curry(func: Callable) -> Callable:
236    """
237    Automatically curry a function
238    (simplified version for demonstration)
239    """
240
241    def curried(*args):
242        if len(args) >= func.__code__.co_argcount:
243            return func(*args)
244        return lambda *more_args: curried(*(args + more_args))
245
246    return curried
247
248
249# Using functools.partial (Pythonic currying)
250def multiply(a: int, b: int, c: int) -> int:
251    return a * b * c
252
253
254# Create specialized functions using partial
255double_then_multiply = partial(multiply, 2)
256triple_then_multiply = partial(multiply, 3)
257
258
259# =============================================================================
260# 6. MAP / FILTER / REDUCE
261# Fundamental functional operations
262# =============================================================================
263
264print("\n" + "=" * 70)
265print("6. MAP / FILTER / REDUCE")
266print("=" * 70)
267
268
269def demonstrate_map():
270    """Map: Transform each element"""
271    numbers = [1, 2, 3, 4, 5]
272
273    # Using built-in map
274    squared = list(map(lambda x: x ** 2, numbers))
275
276    # List comprehension (more Pythonic)
277    squared_comp = [x ** 2 for x in numbers]
278
279    return squared, squared_comp
280
281
282def demonstrate_filter():
283    """Filter: Keep elements matching predicate"""
284    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
285
286    # Using built-in filter
287    evens = list(filter(lambda x: x % 2 == 0, numbers))
288
289    # List comprehension (more Pythonic)
290    evens_comp = [x for x in numbers if x % 2 == 0]
291
292    return evens, evens_comp
293
294
295def demonstrate_reduce():
296    """Reduce: Combine all elements into single value"""
297    numbers = [1, 2, 3, 4, 5]
298
299    # Sum using reduce
300    total = reduce(lambda acc, x: acc + x, numbers, 0)
301
302    # Product using reduce
303    product = reduce(lambda acc, x: acc * x, numbers, 1)
304
305    # More Pythonic: use operator module
306    total_op = reduce(operator.add, numbers, 0)
307    product_op = reduce(operator.mul, numbers, 1)
308
309    return total, product, total_op, product_op
310
311
312# =============================================================================
313# PRACTICAL EXAMPLES
314# =============================================================================
315
316def practical_pipeline_example():
317    """Real-world data transformation pipeline"""
318
319    # Data pipeline: process user data
320    users = [
321        {"name": "alice jones", "age": 25, "salary": 50000},
322        {"name": "bob smith", "age": 17, "salary": 0},
323        {"name": "charlie brown", "age": 35, "salary": 75000},
324        {"name": "diana prince", "age": 30, "salary": 90000},
325        {"name": "eve taylor", "age": 16, "salary": 0},
326    ]
327
328    # Functional pipeline
329    pipeline = pipe(
330        # Filter adults
331        lambda users: filter(lambda u: u["age"] >= 18, users),
332        # Extract names and capitalize
333        lambda users: map(
334            lambda u: {**u, "name": u["name"].title()},
335            users
336        ),
337        # Calculate bonus (10% of salary)
338        lambda users: map(
339            lambda u: {**u, "bonus": u["salary"] * 0.1},
340            users
341        ),
342        # Convert to list
343        list,
344    )
345
346    return pipeline(users)
347
348
349def practical_function_factory():
350    """Function factory for validation"""
351
352    def make_validator(
353        min_value: float,
354        max_value: float
355    ) -> Callable[[float], bool]:
356        """Create range validator"""
357
358        def validate(value: float) -> bool:
359            return min_value <= value <= max_value
360
361        return validate
362
363    # Create specialized validators
364    is_percentage = make_validator(0, 100)
365    is_temperature = make_validator(-273.15, float('inf'))
366    is_grade = make_validator(0, 100)
367
368    return is_percentage, is_temperature, is_grade
369
370
371# =============================================================================
372# DEMONSTRATION
373# =============================================================================
374
375def main():
376    print("\n[PURE FUNCTIONS]")
377    print("-" * 50)
378    print(f"pure_add(5, 3) = {pure_add(5, 3)}")
379    print(f"pure_square(4) = {pure_square(4)}")
380    print(f"pure_capitalize_words('hello world') = {pure_capitalize_words('hello world')}")
381
382    # Impure function produces different results
383    print("\nImpure function (different results):")
384    print(f"impure_increment(5) = {impure_increment(5)}")
385    print(f"impure_increment(5) = {impure_increment(5)}")  # Different!
386
387    print("\n[HIGHER-ORDER FUNCTIONS]")
388    print("-" * 50)
389    multiply_by_3 = make_multiplier(3)
390    print(f"multiply_by_3(5) = {multiply_by_3(5)}")
391
392    add_10 = make_adder(10)
393    print(f"add_10(5) = {add_10(5)}")
394
395    print(f"apply_twice(lambda x: x * 2, 5) = {apply_twice(lambda x: x * 2, 5)}")
396
397    apply_3_times = apply_n_times(3)
398    print(f"apply_3_times(double, 2) = {apply_3_times(double, 2)}")
399
400    print("\n[CLOSURES]")
401    print("-" * 50)
402    counter1 = make_counter(0)
403    counter2 = make_counter(100)
404
405    print(f"counter1(): {counter1()}, {counter1()}, {counter1()}")
406    print(f"counter2(): {counter2()}, {counter2()}")
407
408    say_hello = make_greeting("Hello")
409    say_bonjour = make_greeting("Bonjour")
410    print(f"say_hello('Alice') = {say_hello('Alice')}")
411    print(f"say_bonjour('Bob') = {say_bonjour('Bob')}")
412
413    acc = make_accumulator()
414    print(f"Accumulator: {acc(10)}, {acc(5)}, {acc(3)}")
415
416    print("\n[FUNCTION COMPOSITION]")
417    print("-" * 50)
418
419    # Compose text transformations
420    process_text = compose(add_exclamation, to_uppercase, remove_spaces)
421    print(f"process_text('hello world') = {process_text('hello world')}")
422
423    # Pipe is more intuitive
424    process_text_pipe = pipe(remove_spaces, to_uppercase, add_exclamation)
425    print(f"process_text_pipe('hello world') = {process_text_pipe('hello world')}")
426
427    # Compose numeric operations
428    process_number = pipe(increment, double, increment)
429    print(f"process_number(5) = {process_number(5)}")  # (5+1)*2+1 = 13
430
431    print("\n[CURRYING]")
432    print("-" * 50)
433
434    # Regular call
435    print(f"add_three_numbers(1, 2, 3) = {add_three_numbers(1, 2, 3)}")
436
437    # Curried call
438    print(f"curried_add(1)(2)(3) = {curried_add(1)(2)(3)}")
439
440    # Partial application
441    add_5 = curried_add(5)
442    add_5_and_10 = add_5(10)
443    print(f"curried_add(5)(10)(3) = {add_5_and_10(3)}")
444
445    # Using partial
446    print(f"double_then_multiply(3, 4) = {double_then_multiply(3, 4)}")
447    print(f"triple_then_multiply(3, 4) = {triple_then_multiply(3, 4)}")
448
449    print("\n[MAP / FILTER / REDUCE]")
450    print("-" * 50)
451
452    squared, squared_comp = demonstrate_map()
453    print(f"Map - Squared: {squared}")
454
455    evens, evens_comp = demonstrate_filter()
456    print(f"Filter - Evens: {evens}")
457
458    total, product, total_op, product_op = demonstrate_reduce()
459    print(f"Reduce - Sum: {total}, Product: {product}")
460
461    print("\n[PRACTICAL PIPELINE]")
462    print("-" * 50)
463    processed_users = practical_pipeline_example()
464    for user in processed_users:
465        print(f"  {user}")
466
467    print("\n[PRACTICAL FUNCTION FACTORY]")
468    print("-" * 50)
469    is_percentage, is_temperature, is_grade = practical_function_factory()
470
471    print(f"is_percentage(50) = {is_percentage(50)}")
472    print(f"is_percentage(150) = {is_percentage(150)}")
473    print(f"is_temperature(-300) = {is_temperature(-300)}")
474    print(f"is_grade(85) = {is_grade(85)}")
475
476
477def print_summary():
478    print("\n" + "=" * 70)
479    print("FUNCTIONAL PROGRAMMING PATTERNS SUMMARY")
480    print("=" * 70)
481
482    print("""
4831. PURE FUNCTIONS
484   ✓ No side effects
485   ✓ Deterministic (same input → same output)
486   ✓ Easy to test and reason about
487   ✓ Cacheable/memoizable
488
4892. HIGHER-ORDER FUNCTIONS
490   ✓ Functions as first-class citizens
491   ✓ Can take functions as arguments
492   ✓ Can return functions
493   ✓ Enables abstraction and reusability
494
4953. CLOSURES
496   ✓ Functions that capture variables
497   ✓ Private state without classes
498   ✓ Function factories
499   ✓ Data hiding and encapsulation
500
5014. FUNCTION COMPOSITION
502   ✓ Build complex from simple
503   ✓ Compose/pipe for readability
504   ✓ Reusable transformation chains
505   ✓ Declarative style
506
5075. CURRYING
508   ✓ Transform multi-arg → single-arg chain
509   ✓ Partial application
510   ✓ Specialized functions from generic ones
511   ✓ Better function reuse
512
5136. MAP/FILTER/REDUCE
514   ✓ Fundamental transformations
515   ✓ Declarative data processing
516   ✓ Composable operations
517   ✓ Works with any iterable
518
519BENEFITS:
520  • More predictable code
521  • Easier to test
522  • Better concurrency (immutability)
523  • Composability
524  • Declarative style
525
526PYTHON TIPS:
527  • Use list comprehensions instead of map/filter when simple
528  • functools module for partial, reduce
529  • operator module for common operations
530  • itertools for advanced iteration
531  • toolz/fn.py libraries for more FP tools
532""")
533
534
535if __name__ == "__main__":
536    main()
537    print_summary()