This post acquaints the reader with the fundamentals of Functional Programming in the context of Python. Most programmers rarely touch upon languages with a primary functional focus- such as Lisp or Haskell, except maybe as a part of an academic course. Since Python is a widely-used language that supports (mostly) all functional programming constructs, this post tries to demonstrate their usage and advantages. Functional Programming may not be the best/Pythonic way of doing everything in this language, but it has its advantages in some applications and that is what this post is all about.
1. What is Functional Programming?
Functional programming is a programming paradigm that revolves around pure functions. If you have ever done coding in your life, you probably associate functions with subroutines. That is the ‘CS’ perspective. Here, we lean more towards the mathematical definition:
A pure function is essentially any function that can be represented as a mathematical expression only. Which means no side-effects: no I/O operations, no global state changes, no database interactions (You can’t really represent these in a mathematical expression, can you?). The output from a pure function is ONLY dependent on its inputs. So if you were to call a pure function with the same inputs a million times, you would get the same result every single time. Ofcourse, in practical functional programming languages such as Haskell, there are ways to do I/O operations etc., but the focus is still on pure functions.
2. The lambda construct
The easiest way to initialize a pure function in Python is by using the lambda operative:
>>> square_func = lambda x: x**2 >>> square_func(2) 4 >>> some_list = [(1, 3), (5, 2), (6, 1), (4, 6)] >>> some_list.sort(key=lambda x: x**2 - x*3) >>> some_list [(1, 3), (4, 6), (5, 2), (6, 1)]
The lambda operative helps you define functions in a one-line fashion, which is pretty convenient if its just a mathematical expression anyways. In fact, the lambda keyword is pretty prominent in functional programming (and not just Python), and has its roots in Lambda Calculus – one of the ‘ancestors’ of functional programming.
Functions initialized with lambda can also be called anonymous functions. If you look at line 5 in the code above, you are passing a lambda-initialized function to the sort method. You aren’t really giving it a name, just defining it on-the-go and passing it as an argument. Hence the term ‘anonymous’. Ofcourse, you can always assign anonymous functions to a variable (as on line 1) and call them like usual functions (as on line 2).
3. Functions as First-Class citizens
In Functional Programming, functions are first-class citizens. Which basically means that you can treat them as any other objects – you can assign them to variables, you can pass them as arguments, or even get them returned from other functions. You already saw some of this happening in the code before. Here’s some more:
>>> square_func = lambda x: x**2 >>> function_product = lambda F, m: lambda x: F(x)*m >>> square_func(2) 4 >>> function_product(square_func, 3)(2) 12
is a function by itself. on the other hand, is a higher-order function that takes two inputs- A function and a multiplier . It returns a function which is equal to . Therefore on line 5, is a function that is being called with the argument , returning .
4. Immutability of Data and Data Flows
Immutability in the context of objects means that you never modify the value of data once initialized. In Functional Programming, whenever you call a function on some data, you always get new instances as a result – you never ‘update’ the value of any arguments. Programmatically, this implies that once you initialize a variable like , the variable will never appear on the LHS of a statement again.
As a result, any functional code can be thought of as a feed-forward data flow. You never ‘come back’ to change the value of any variable, hence data always moves forward from the inputs to the ultimate output(s) – from one function, to another.
This immutability of data leads to another property called Referential Transparency. It means that the value of an expression is the same anywhere it might occur in the program – as long as the required variables are defined. Since you never update the value of any variables/objects (including functions), they mean the same in any context once defined. For this reason, functional code is extremely easy to analyze and debug. You never need to track the value of state variables or remember any updates.
This enables usage of memoization – you basically ‘remember’ the outputs of expensive functions with some common arguments in a sort of lookup table. This reduces the computational complexity at the expense of memory.
Functional programming doesn’t really provide for iteration via or statements. Neither does it have the provision of state-updates. As a result, recursion is a strong theme in functional programming. It is worthwhile to remember that any iterative code can be converted to recursive code.
Here’s a functional version of the n-th Fibonacci number calculator:
>>> fibonacci = (lambda x, x_1=1, x_2=0: x_2 if x == 0 else fibonacci(x - 1, x_1 + x_2, x_1)) >>> fibonacci(1) 1 >>> fibonacci(5) 5 >>> fibonacci(6) 8
Line 2 defines the base case of the computation, while line 3 makes the recursive call. If you think about it, , and are essentially state variables whose updated versions are being made and passed into every recursive call. This is usually how state is handled in functional code.
It is always better to implement tail-recursion when writing functional code, especially in pure-functional languages such as Scheme. There’s a good reason for this. Tail-recursive code is easily optimized into iterative code by the under-lying compiler (though this doesn’t apply to Python), making the compiled code more efficient.
6. Lazy evaluation
This is an aspect of Functional Programming that Python does not adopt. In many pure functional languages such as Haskell, objects that don’t necessarily need evaluation are not evaluated. Evaluation means computing the value of a function expression. Consider the following line:
length([3 + 4, 5, 1/0])
In a language like Python, the presence of 1/0 would case an Exception immediately. However, if we were to implement lazy-evaluation, the value 3 would be returned- since there exist 3 objects in the list, and their values don’t need to be evaluated for the count. This causes a sort of graph reduction in the data flow, leading to lesser function calls (at the risk of ignoring errors).
Python 3.x does support a different kind of lazy evaluation, that returns iterators for calls such as . This prevents the entire data from being loaded into memory, and is efficient from a programming perspective.
7. No iterators as sequences
This is a small point, but since the value of the next element in an iterator depends on its state(which violates Referential Transparency), iterators aren’t present in pure-functional code. Instead if we are writing pure-functional code we only deal with explicit immutable tuples – which you can generate from an iterator in Python using .
7. map, reduce and filter
, and are three higher-order functions that appear in all pure functional languages – and in Python, too. Their prevalence suggests how often they are used in functional code to make it more elegant.
map basically provides a kind of parallelism by calling a function over all elements in a list/array. Here’s an example:
>>> map(lambda x: x**2, [1, 2, 3]) [1, 4, 9]
Notice how map can be parallelized – since you are calling the same function over all elements in an array without any modifications, you can make the calls in any given order.
filter offers another parallelism over a sequence. It takes as an input a boolean-returning function and a sequence, and retains only those values from the sequence that return True from the function. Example:
>>> filter(lambda x: x % 2 == 0, [1,2,3,4,5,6]) [2, 4, 6]
The above line filters out all the odd numbers from the list.
reduce is a construct that performs a serial iteration over a sequence. Its first argument is a function , which takes in two arguments – An accumulator and the current input . computes and returns the new value of . The second argument to reduce is the sequence itself, , and the third argument is the initial accumulator . Internally, what it does is:
and so on… What you get returned at the end, is . An example:
>>> reduce(lambda x, y: x + y, [1, 2, 3], 0) 6
The accumulator doesn’t have to be of the same type as the elements in the list! It can be a sequence as well. Here’s an example of reduce being used to reverse a sequence:
>>> reduce(lambda L, element: [element] + L, [1, 2, 3], ) [3, 2, 1]
Notice how no list is being modified in the above line of code. (As a side note, needs to be imported from the library from Python 3.x).
Also, understand that since every call of that reduce makes on an element depends on the elements that come before it, reduce cannot be parallelized.
Notice the usage of the words ‘map’ and ‘reduce’? These operators in Functional programming provide a good context to understand MapReduce, and are even touched upon in the old Google Lectures on the subject.
End note1: This blog post provides very succinct examples and code for you to better understand how functional programming should be done in Python using the aforementioned keywords/operators. It also introduces some advanced techniques such as pipelines. Do have a look!
End Note2: This article provides some perspective on why functional programming is the preferred approach in only specialized operations, and comments on the human-understandability of functional code (which is low).