An Introduction to the Art of Computer Programming Using Python in the Age of Generative AI

XV. Functional Programming

Introduction to Functional Programming

Functional programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing states or mutable data. It emphasizes the use of functions, in contrast to the procedural and object-oriented programming paradigms. In Python, we can apply functional programming principles to write clearer, more concise, and more predictable code.

Pure Functions

Pure functions are the foundation of functional programming. The output of a pure function is determined solely by its input values, with no observable side effects (like printing to the console, modifying a global variable, or writing to a file). This means that a pure function does not change any external state or depend on any external state that might change. As a result, pure functions are predictable, easier to test, and easier to run in parallel.


def add(a: int, b: int) -> int:
    return a + b

# This function is pure: same input always equals same output
result = add(3, 4)
print(f"Result: {result}")
        

In the example above, the add function is pure because it always produces the same output for the same input and does not affect any external state.

Lambda Functions

Lambda functions, also called anonymous functions, are integral to functional programming in Python. They let you create small, unnamed functions inline, which is especially handy for constructs like map, filter, and reduce that often take functions as arguments. The syntax for a lambda function is:


lambda arguments: expression
        

A common real-world use case for lambdas is sorting complex data structures, such as a list of dictionaries, by a specific key.


# Sorting a list of AI models by accuracy
models = [
    {'name': 'Model A', 'accuracy': 0.85},
    {'name': 'Model B', 'accuracy': 0.92},
    {'name': 'Model C', 'accuracy': 0.78}
]

# Use lambda to sort by the 'accuracy' key
sorted_models = sorted(models, key=lambda x: x['accuracy'], reverse=True)

for m in sorted_models:
    print(f"{m['name']}: {m['accuracy']}")
        

Higher-Order Functions: Map, Filter, and Reduce

Higher-order functions either take one or more functions as arguments or return a function as a result. They offer more abstract and flexible code. Python's built-in map and filter functions are prime examples of higher-order functions.


numbers = [1, 2, 3, 4, 5]

# Map: Apply a function to every item
squared_numbers = map(lambda x: x ** 2, numbers)
print(f"Squares: {list(squared_numbers)}")

# Filter: Keep items where the function returns True
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(f"Evens: {list(even_numbers)}")
        

Another classic functional tool is reduce, which is part of the functools module. It processes a list of items to produce a single cumulative result.


from functools import reduce

numbers = [1, 2, 3, 4, 5]

# Calculate the product of all numbers: (((1 * 2) * 3) * 4) * 5
product = reduce(lambda x, y: x * y, numbers)
print(f"Product: {product}")
        
Generative AI Insight: Data Pipelines
Functional programming is vital for ETL (Extract, Transform, Load) pipelines in AI. When preparing training data, you often need to chain operations: clean_text -> tokenize -> embed. Frameworks like JAX (used for high-performance ML) are built entirely on functional programming principles, requiring pure functions to enable automatic parallelization across GPUs.

Pythonic Style: Comprehensions vs. Map/Filter

While map and filter are traditional functional tools, Python developers (and the "Pythonic" style) generally prefer List Comprehensions (covered in Chapter VIII) for readability.


numbers = [1, 2, 3, 4, 5]

# Functional style
squares_map = list(map(lambda x: x**2, numbers))

# Pythonic style (List Comprehension)
squares_comp = [x**2 for x in numbers]

print(f"Map result: {squares_map}")
print(f"Comp result: {squares_comp}")
        

First-Class Functions

In Python, functions are first-class citizens, meaning they can be assigned to variables, passed as arguments, and returned by other functions. This property facilitates higher-order functions and supports functional programming patterns.


def square(x):
    return x * x

def cube(x):
    return x * x * x

def my_map(func, arg_list):
    result = []
    for i in arg_list:
        result.append(func(i))
    return result

input_data = [1, 2, 3, 4, 5]

squares = my_map(square, input_data)
print(f"Squares: {squares}")

cubes = my_map(cube, input_data)
print(f"Cubes: {cubes}")
        

Partial Functions

Sometimes you want to "freeze" some arguments of a function to create a new, simpler function. The functools.partial utility allows this. This is very useful for configuring AI model hyperparameters.


from functools import partial

def power(base, exponent):
    return base ** exponent

# Create a new function 'square' where exponent is fixed to 2
square_func = partial(power, exponent=2)
# Create a new function 'cube' where exponent is fixed to 3
cube_func = partial(power, exponent=3)

print(f"5 squared is: {square_func(5)}")
print(f"5 cubed is: {cube_func(5)}")
        

Recursion

In functional programming, loops (like for and while) are often replaced by recursion—a technique where a function calls itself to solve smaller instances of the problem. Recursion is particularly useful for traversing tree-like data structures, which are common in AI algorithms (like Decision Trees).


def factorial(n):
    # Base Case: Stop recursion (Handle 0 and 1 correctly)
    if n <= 1:
        return 1
    # Recursive Case: Function calls itself
    else:
        return n * factorial(n - 1)

print(f"Factorial of 5: {factorial(5)}")
        

Immutability

Immutability is another cornerstone of functional programming, meaning data is not modified after creation. Instead of mutating data, new data structures are formed. This promotes predictable code with fewer hidden side effects, reducing bugs and making code thread-safe.


# Using tuple instead of list to ensure immutability
numbers = (0, 1, 2, 3, 4)

# Functions that operate on immutable data return NEW data structures
def add_one_to_each(nums):
    return tuple(x + 1 for x in nums)

new_numbers = add_one_to_each(numbers)

print(f"Original: {numbers}")
print(f"New:      {new_numbers}")
        

Prompting AI for Implementing Functional Programming

Be clear about the functionality you need when consulting AI for functional programming. Instead of asking, "How do I use functional programming?", specify your intent: "Help me write Python functions that adhere to functional principles, such as pure functions and higher-order functions."

Example Prompt:
Write a functional program for a Morse code translator. Include pure functions to encode and decode messages, and use map/join for processing.

Resulting AI-generated code:


MORSE_CODE_DICT = {
    'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.', 'F': '..-.',
    'G': '--.', 'H': '....', 'I': '..', 'J': '.---', 'K': '-.-', 'L': '.-..',
    'M': '--', 'N': '-.', 'O': '---', 'P': '.--.', 'Q': '--.-', 'R': '.-.',
    'S': '...', 'T': '-', 'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-',
    'Y': '-.--', 'Z': '--..',
    '0': '-----', '1': '.----', '2': '..---', '3': '...--', '4': '....-',
    '5': '.....', '6': '-....', '7': '--...', '8': '---..', '9': '----.',
    ',': '--..--', '.': '.-.-.-', '?': '..--..', '/': '-..-.', '-': '-....-',
    '(': '-.--.', ')': '-.--.-', ' ': '/'
}

def encode_char(char):
    """Pure function to map a single char to Morse."""
    return MORSE_CODE_DICT.get(char.upper(), '')

def encode_message(message):
    """Encodes message using map and join."""
    return ' '.join(map(encode_char, message))

def decode_word(morse_word):
    """Pure function to decode a single morse character."""
    # Invert dictionary for lookup
    reverse_dict = {value: key for key, value in MORSE_CODE_DICT.items()}
    return reverse_dict.get(morse_word, '')

def decode_message(morse_code):
    """Decodes message using map."""
    return ''.join(map(decode_word, morse_code.split(' ')))

# Encoding a message
message = "HELLO WORLD"
encoded_message = encode_message(message)
print(f"Encoded: {encoded_message}")

# Decoding a message
decoded_message = decode_message(encoded_message)
print(f"Decoded: {decoded_message}")
Back to Home