Many coders have heard of the term "functional programming", but have often never really used it or its properties. Especially if they came from Java, where there are no real functions, only methods each function has to be attached to an object, and cannot stand by itself. Functions are commomly thought of as defining behavior. However, functions can also be thought of as data, instead of just instructions. They are data that define your instructions.
You can manipulate functions like other types of data. You can pass it around, alter functions, and use functions to manipulate other data. If you were to think of functions as data, limitations like having to attach a method to an object to define behavior goes away.
In this first blog post, we shall experiment with treating functions as data that can define behavior. To give an example of this, we will implement a calculator. We will start with a more object oriented version to contrast object oriented vs functional, and then progressively replace functionality of the calculator with functional parts, while maintaining the same overall goal of having a calculator.
We want it to be able to Add, Subtract, and Multiply. Let's define a Calculator object to hold our accumulated value during calculations. An accumulated value is simply so we can remember the previous value for successive operations. $1+2=3$, then three is the accumulated value. Our next operation will use $3$ and a another number. We'll also define a Do method to compute an action.
import math
class Calculator(object):
def __init__(self):
self.acc = 0.0
def Do(self, opt, v):
if opt == 'Add':
self.acc += v
elif opt == 'Sub':
self.acc -= v
elif opt == 'Mul':
self.acc *= v
else:
print("Undefined Operation")
return self.acc
Above we have defined a Calculator object. It has a Do method takes in a string opt to determine which operation to conduct, and a value v float or int. It currently has three actions, and returns the accumulated value once it is done computing the value. Below is an example of usage.
c = Calculator()
print(c.Do("Add", 100))
print(c.Do("Sub", 50))
print(c.Do("Mul", 2))
100.0
50.0
100.0
Our Calculator is quite limited as of right now. It can't really do much. If we wanted to add some other functionality to the calculator, such as calculating the square root of the accumulated value, it would require adding another elif
to the Do method.
We would have to add another conditional for every operation we wanted to do. This is not maintainable in the long run, and not user friendly. If the user wanted to define his or her own operation to use with the calculator, they would have to edit the source code itself.
Let's see if we can move functionality outside of the Do method. Let's rewrite our calculator a little bit, but this time using a slightly more functional style.
class Calculator(object):
def __init__(self):
self.acc = 0.0
def Do(self, opfunc, v):
self.acc = opfunc(self.acc, v)
return self.acc
Now we have changed our calculator a bit. Do is much simpler now, and takes in two standard parameters, opfunc and v. What this calculator does is to take in an operation function opfunc, as well as the previous value v. Whenever Do is called, it calls the operation we pass in, using the accumulated value the Calculator object holds and and value v passed in to compute a value. For example, an adding function would take
self.acc + v
and return the value.
Taking a moment to digest this, the Do method simply takes a function and executes it. It doesn't take in a number value, but takes a function, evaulates that function using v, and returns the computed value. The advantage of this is that functionality has moved outside the Do method inside the calculator. Now the Do method only "does" a function that we supply it.
We can now redefine our previous Add, Sub, and Mul functions. These are the functions that we pass into Do. Examples are given below.
New operation functions must follow the schema below. It requires two numerical inputs and returns one computed value.
def Opt(value_a, value_b):
return operation
def Add(a, b):
return a + b
def Sub(a, b):
return a - b
def Mul(a, b):
return a * b
Our Add, Sub, and Mul function all follow the above schema. Take a moment to verify this.
To use this calculator, all we have to do is pass in the function to the Do method. This makes it very simple to define new operation functions and utilize them. Below is an example. The numerical outputs are still the same as before with the pre-defined if-statements.
c = Calculator()
print(c.Do(Add, 5))
print(c.Do(Sub, 3))
print(c.Do(Mul, 8))
5.0
2.0
16.0
Extending the Calculator
Let's say some user wanted a square root function. Easy, we just define another function!
def Sqrt(a, _):
return math.sqrt(a)
Here is where our code got a little ugly. Sqrt doesn't need an extra value v because it only acts on the accumulated value. Previously, all of our operations required two numbers to use, v and the accumulated value. This is why we have an underscore variable _ in place of v for the second parameter, because we ignore the input value v anyway. An example of using Sqrt is below.
print(c.Do(Sqrt, 0)) # Last operand ignored
4.0
The Sqrt function currently requires the user to ignore the second operand, which is a bit clunky. We should clean this up.
This problem occurs because our defined schema for the Do method is rather restricting. It requires that we have two numerical inputs. What if an operation only needs one parameter like Sqrt? How about 3 parameters? With the Sqrt we could just ignore a parameter, but that's more of a hack than a real solution. And notice that our Sqrt function was really just a wrapper for the math.sqrt(a)
from the standard library. It would be more efficient and cleaner if we could just pass in the math.sqrt(a)
function to the Do method instead.
Things get a bit more complicated now. Let's rewrite our operation functions from a function that takes two parameters and returns a value to a function that returns a function. This returned function will take a value and return a value. An example of an Add function is defined below.
def Add(v):
def operation(acc):
return acc + v
return operation
We also need to update our Do method. Do now takes in a function that takes in one value and returns a value.
class Calculator(object):
def __init__(self):
self.acc = 0.0
def Do(self, opfunc):
self.acc = opfunc(self.acc)
return self.acc
Why does our Add function return a function? Here's the cool part about passing around functions as data. We can modify a function before it is executed. Let's give an example about how the add function would play out.
Let's try Add(10)
. Add sets v equal to $10$. It defines a function called operation that takes in a second value acc and adds it to v. Then add returns operation.
What specifically is returned when we call Add(10)
? The result of the function looks like this
def operation(acc):
return acc + 10
Add(10)
returns a function that adds $10$ to acc. Add(9)
would return a function that adds $9$ to acc.
def operation(acc):
return acc + 9
Let's see how adding works with Do now.
c = Calculator()
print(c.Do(Add(10)))
print(c.Do(Add(20)))
10.0
30.0
When calling Do, we don't call it with the Add
function itself, but the result of evaluating Add(10)
. The type of evaluating Add(10)
is a function that takes in a value and returning a value. We are calling the operation function that is returned by Add(10)
.
Do then passes in self.acc
into the operation function to add $10$ to self.acc
.
We can define other functions in a similiar style. Thus we can easily define other functions like this as well.
def Sub(v):
def operation(acc):
return acc - v
return operation
def Mul(v):
def operation(acc):
return acc * v
return operation
c = Calculator()
print(c.Do(Add(5)))
print(c.Do(Sub(3)))
print(c.Do(Mul(8)))
5.0
2.0
16.0
Now how would we implement Sqrt? It's quite simple now.
def Sqrt():
def operation(acc):
return math.sqrt(acc)
return operation
c = Calculator()
print(c.Do(Add(5)))
print(c.Do(Sub(3)))
print(c.Do(Mul(8)))
print(c.Do(Sqrt()))
5.0
2.0
16.0
4.0
Our implementation avoids the awkardness of having to ignore an input variable. But again Sqrt is just a wrapper of math.sqrt(acc)
, so we can simply put that into the calculator instead.
We can get real crazy and also implement different operations with any number of inputs. Examples below.
def Crazy(a, b, c, d, e):
def operation(acc):
return (((a + b) * d) ** e) + (c * acc )
return operation
c = Calculator()
print(c.Do(Add(5)))
print(c.Do(Sub(3)))
print(c.Do(Mul(8)))
print(c.Do(math.sqrt))
print(c.Do(math.cos))
print(c.Do(Crazy(1,2,3,4,5)))
5.0
2.0
16.0
4.0
-0.6536436208636119
248830.03906913742
Recap
We removed if statements from our Calculator object and went to an object that took in a function to evaluate. But while this model was more flexible in that functions could be easily defined outside the Calculator and passed in, it could only take in two parameters.
To fix that problem, we adjusted our functions a little bit. Instead, we created functions that return functions that compute an operation. This allowed us to
Bonus
If we wanted to, we could get rid of the object Calculator and simply have a closure function instead. This makes our implementation nearly entirely purely functional. We need the nonlocal
statement to indicate that acc
is not within the score of the inner function.
Note that because Calculator actually makes sense as an object since it's a noun, it would be better to leave it as an object. However, this is just to show that closures can take the place of objects and can even be better.
def Calculate():
acc = 0.0
def Do(opfunc):
nonlocal acc
acc = opfunc(acc)
return acc
return Do
c = Calculate()
print(c(Add(5)))
print(c(Sub(3)))
print(c(Mul(8)))
print(c(math.cos))
print(c(Crazy(1,2,3,4,5)))
5.0
2.0
16.0
-0.9576594803233847
248829.12702155902
Conclusion
We went from a hard coded layered if statement to a function model, wherein we can pass in any function with any number of parameters to computer an accumulated value. This kind of abstraction is the kind that can only be offered by functional programming.
Why is this useful? Well, come see next the next blog post, where we'll go over using functions to define API configurations.