Functional Programming

This is where code is structured as functions, arising from lambda calculus. In OOP, we use objects, which have templates called classes, and are instantiated. They have fields to store data, and methods to manipulate it. An object can be passed as an argument.

In FP, functions are the main things, and we use them to store and manipulate data, which can include other functions.

First, note that imperative programming solves problems by describing step-by-step solutions, while declarative programming relies on the underlying framework or language to solve it. For example, if the problem was to have coffee, imperative would be the steps of making it, while declarative would be asking a barista to make it for you.

For a programming analogy, sorting a list imperatively would be writing the steps out themselves, e.g. iterating through the list, checking whether each element is lower, etc. Declarative would be using sorted(list).

OOP is imperative, functional is declarative.

Functions

Functions in FP should

These are important for maintaining scalability and unwanted effects like race conditions.

For example,

ans = 0

# this is not a pure function
def add(x,y):
    ans = x+y

# this is a pure function
def add(x,y):
    return x+y

ans = add(2,3)

Reusability is different from being pure:

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

# this function is not pure or reusable
def app():
    lst[0] = 0

# this function is pure but not reusable
# it relies on but does not modify an external variable
def app():
    res = list(lst)
    res[0] = 0
    return res

# this function is reusable but not pure
# it is passed an external variable, but it modifies it
def app(lst):
    lst[0] = 0
    return lst

# this function is reusable and pure
def app(lst):
    res = list(lst)
    res[0] = 0
    return res

Recursion and Loops

OOP uses loops, but these are not pure as they maintain an external counter which is altered. FP instead uses recursion. Recursive functions have the following structure:

def recursive_function(n):
    # base case to terminate recursion
    # call to recursive_function with new parameter

For example,

def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)

These are particularly powerful as they are easy to make into generators using yield instead of return, saving memory. They also avoid confusing errors with global variables, at the expense of a more complicated structure.

Functions as arguments

In OOP, you pass objects as arguments to functions. In FP, you pass functions. This promotes brevity by deconstructing functions into their base steps.

For example,

def add(x,y):
    return x+y

def sub(x,y):
    return x-y

def times3(a,b,func):
    return 3 * func(a,b)

# now we can call, e.g.
times3(1,2,add)
times3(3,4, lambda x,y: x**y)

This also leads to decorator functions, which are particularly useful as general-purpose function modifiers.

One can also use the functools package for more options.

Data Types

To ensure purity, it’s useful to use immutable data types, which allow thread-safe manipulation.

We could use e.g. tuples, which allow mixed datatypes as elements. However, this relies on the programmer remembering the position of each piece of data. Instead, collections.namedtuple might be better: it acts as essentially a mini class.

from collections import namedtuple

student = namedtuple("student", ["name", "age", "grade"])

scott = student("Scott", 28, "A")

print(scott.name)