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 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, 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 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}")
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.
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}")
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}")
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)}")
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 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}")
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: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}")