There is a lot of buzz about functional programming these days. However, it might feel a little isolated since Python is not a pure FP language. If you're coming to Python from a functional language like Haskell or Scala and you're wondering how to work in a language that seems to be lacking the things you take for granted in your favorite FP language. Whatever may be the case, this post will help you become a functional Python Programmer. Some of the major topics that we will cover include an overview of functional programming principles, including higher-order functions, first-class functions, and pure functions.
Functional programming, or FP, as it is often called is a programming paradigm. An approach to solving programming problems. It focuses on defining what to do, instead of performing some action. I'll review the main programming paradigms that are used today, and then discuss the main principles behind FP in the context of a business problem if an order processing system and transform it to a functional style programming.
1. Principles of Functional Programming
There are various components of functional programming. One important building block is first-class functions. These are functions that accept another function as an argument or return a function. This is an important quality that leads to increased code reusability and allows for code abstractions. Pure functions are functions that have no side effects. Side effects are any actions performed by a piece of code that affects state outside that code. A common example would be the Python print function. This has a side effect of writing something to the console, and so it's not pure.
Sometimes side effects are more subtle. For example, perhaps an object is passed to a function which then makes changes to that object. Such changes are called mutations. Pure functions mutate nothing and have no side effects. Immutable variables and objects have the characteristic that once set they cannot be changed. Lazy evaluation seeks to only do actual computation when the results are needed. This has many benefits including reduced memory requirements and eliminating unnecessary computation for results that may never be used. Recursion offers an alternative to iteration and a way to reduce or eliminate temporary loop variables.
Most FP languages support the idea that statements can have a return value. Python does not support this construct. However, be rest assured that we are not abandoning object-oriented programming. After all, everything in Python is an object and that's not going to change. FP offers a way to bring some discipline to OOP that can help make programs smaller and more maintainable.
2. First-class Functions in Python
A higher order function is defined as one that takes a function as a parameter or returns a function as a result or both. Let's review how Python supports higher order functions.
2.1. Python functions are first class objects:
It means they have attributes and can be referenced and assigned to variables.
Below, we are assigning a function to a variable. This assignment doesn’t call the function. It takes the function object referenced by greet and creates a second name pointing to it, message.
2.2. Python functions can be passed as arguments to other functions:
As the functions are objects we can pass them as arguments to other functions. Functions that can accept other functions as arguments are called higher-order functions. Below is a function (display) which takes a function as an argument.
2.3. Python functions can return another function:
Again as the functions are objects, this allows them to return another function. This is also known as Closure. Below, the function (addition) returns function (adds).
The idea behind closures is known as Currying, where a function's signature is reduced argument by argument until only one argument is left. This makes the function easier to test.
Now, let's see how we can use some of these concepts of higher order and first class functions in the working examples.
The Order Item class and the Customer class is quite simple, just a list of three attributes. While in the Order class, there are three methods to get parts of the order, but only for expedited orders. Note that there is no constructor at the moment. That means it is necessary to add all the attributes after instantiation. Also, pay attention to the repeated code in the three get methods. For each method, the code creates an empty list, uses a for loop to iterate over the orders, uses an if statement to check the condition, appends the item to the list if the condition is true, and finally returns the completed list. There are actually 12 lines of duplicated code. A repeated code can be a maintenance headache since you have to look down every repetition of any change. Imagine for a second you were asked to write three new methods for the not-expedited orders. You'd wind up then with 6 methods in total and 24 lines of duplicated code.
Let’s go back to the Order class and refactor it. As the three functions do not use the self-argument. That means they can be static. So we add the static method decorator, and then we can remove the argument. Similarly, we do for all the other methods. We define a new function: test_expedited, a static method. It takes just one argument and order item to be checked. Then, it just returns a Boolean, indicating if the order has been expedited or not. Now I can pass in my new function as an argument to a higher order function. So instead of testing the expedited attribute directly, it will call the predicate, that is the passed in function. So basically we removed the duplication using calls to new helper functions, which made the refactored functions higher order through composition. The net result is a cleaner, leaner Order class without special cases.
3. Pure Functions
3.1. Introduction to Pure Functions
Pure functions are simple. They do one thing and do it well. Functions that do lots of work are hard to test and maintain. A better approach is to write smaller functions that do one thing well. Then combine them to increase the overall functionality. Since they are simple, pure functions have a limited number of arguments. Although it’s not always possible, the number of arguments can be 0. If your argument list starts climbing beyond two or three, it's time to refactor your function into two or even more functions. One property of pure functions is that the set of inputs completely determines the output. Also, a pure function always returns the same output given the same input. Pure functions do not use state, nor do they modify state, and that means they have no side effects. They do not use variables outside their own scope. This makes them easy to test and immune to code changes around them. For example, functions that print or use the current time are not pure since they modify our use state and may not return the same result given the same inputs. To achieve greater functionality, you don't want to add complexity to your functions. Instead, you combined functions to achieve the desired result.
3.2. Purify the Functions
Let's make our functions as pure as possible. We need to start with the get_filtered_info method. It actually has two responsibilities. First, filter the list of orders by some predicate, and then build a list of items using the passed in function. We need to break it up so that each function has a single responsibility,
So to break it up, let's start with a filter function. At this point, the function will be different from the built-in filter function since it will return a list. Now to be pure, the filter function must receive the list as an argument. The argument is any Python iterable. The result is built using the built-in filter function, and the predicate passed in as an argument. Using the new filter function, the get_filtered_info function can also be rewritten. It will use the new filter function we just finished and then apply another function to the results like the built-in map function. Let's write a map function we can use here. Now, with the map function in place, here's the new get_filtered_info function, it takes three arguments. The get_order_by_id function can now also be simplified, pure and can call the new filter function directly.
To summarize, we introduced pure functions. These are functions whose output depends solely on the input, and pure functions must have no side effects. I added new functionality to the order class to set the expedited flag of an order. Along the way, I created new pure functions: map and filter, that use the built-in Python functions of the same name but return lists. Then using map and filter and purity principles, the code used to implement the new request was also made pure.
Hopefully, now you get an idea of how can we implement functional programming in Python. Even though Python is an Object-oriented programming language, there are ways we can use this as a functional language. It has various properties which we have discussed here and there are a lot of useful things which can be done using Python in the Functional programming world, thus making the code simpler and pure.