Sumukh Barve

Polydojo Founder's blog on Software, Business and more.


2020-12-01

Functional Programming In Python
The Complete, Practical, Step-By-Step Guide

The web is littered with theoretical articles about functional programming. But when it comes to practically helpful material, there's very little. This guide aims to fill that gap. Instead of including technical definitions of terms like "Higher Order Functions" or "Tail Call Optimization", this guide takes a practical, hands-on approach.

Functional Programming Image

Aside: At Polydojo, we love functional programming. But it takes a while for newcomers to stop thinking in classical OOP terms. Hopefully, this guide will help new team members transition to Functional Programming faster.

What Is Functional Programming?

In functional programming, you treat functions as values. To illustrate what I mean, let's turn to a bit of JavaScript:

var n = 1;
var f = function (x) { return x + 1; };

In the above snippet:

  1. We set the variable n to the value 1.
  2. We set the variable f to the value function (x) { return x + 1; }.

Just as 1 is value, function (x) { return x + 1; } is also a value. That is, just like numbers, functions are values. And this holds true in Python too!

Python Is Fully Functional!

Let's translate the above JavaScript snippet to Python:

n = 1;
f = lambda x: x + 1;

Or equivalently:

n = 1;
def f (x):
    return x + 1;

In JavaScript, the functional nature of the language is abundantly clear. Python's functional nature is clear in the lambda form, but not so much in the def form. Nonetheless, lambda and def are equivalent, and functions are values in Python.

Quick Intro To lambda Expressions

If you're familiar with lambda expressions, you may safely skip this section. If not, don't worry. lambda expressions are just a short-hand way for creating single-statement functions.

The syantax is of the form: lambda <params> : <expression>. The <expression> is evaluated and returned, but the return keyword should not be used.

As a quick illustration, here are a few def-form and lambda-form equivalents:

def checkEven (n):
    return n % 2 == 0;

checkEven = lambda n: n % 2 == 0;
def add (x, y):
    return x + y;

add = lambda x, y: x + y;
def factorial (n):
    return 1 if n == 1 else n * factorial(n - 1);

factorial = lambda n: 1 if n == 1 else n * factorial(n - 1);

Functions Are Values. So What?

If a function is a value, then just like any other value, you should be able to:

  1. pass it as an argument to a function,
  2. define it locally within the scope a function, and
  3. return it from a function.

For example:

  1. Just as you can call squareRoot(2), you should be able to call memoize(squareRoot), where memoize and squareRoot are both functions.

  2. Within a function, just as you can set i = 0, you should be able to set
    add1 = lambda x: x + 1, where i and add1 are both local variables.

  3. In a function, just as you can return 0, you should be able to
    return lambda x: x * x.

Aside: If a programming language doesn't allow you to pass, return or locally define functions, then it isn't functional. Luckily for us, Python and JavaScript both are entirely functional languages!

Hmm, That's Interesting. But SO WHAT?

To see the power of functional programming, let's say you're tasked with writing a utility library that includes a number of mathematical transformation functions.

Computing Squares:

Your library must include a function, squareList(.), which accepts a list of numbers, squares each number, and returns the list thereof. That's pretty easy:

def squareList (nList):
    result = [];
    for n in nList:
        result.append(n * n);
    return result;

Computing Halves:

Your library must also include a function, halfList(.), which accepts a list of integers, halves each number, and returns the list thereof. Equally easy:

def halfList (nList):
    result = [];
    for n in nList:
        result.append(n / 2.0);
    return result;

Computing Factorials:

Your library must also include a function, factorialList(.), which accepts a list of positive integers, computes the factorial of each, and returns the list thereof. That's easy too!

def factorial (n):
    return 1 if n == 1 else n * factorial(n - 1);

def factorialList (nList):
    result = [];
    for n in nList:
        result.append(factorial(n));
    return result;

Notice The Similarity?

Did you notice that the functions we just implemented above are very similar. They all seem to follow the pattern:

def somethingList (nList):
    result = [];
    for n in nList:
        result.append(someFunc(n));
    return result;

The only difference seems to be someFunc, which is either a squaring, halving or factorial-computing function.

Wouldn't it be nice, if we could write a function that'd accept a list (like nList) and a transformation (like factorial), and then simply map that transformation onto the list? Something like:

def mapTransform (func, nList):
    result = [];
    for n in nList:
        result.append(func(n));
    return result;

Then, using mapTransform(), and retaining factorial() we could write:

def squareList (nList):
    return mapTransform(lambda n: n*n, nList);

def halfList (nList):
    return mapTransform(lambda n: n/2.0, nList);

def factorialList (nList):
    return mapTransform(factorial, nList);

Boom! That's Functional Programming!

By passing the transformation function func as a parameter to mapTransform, implementing our functions became so much simpler! Instead of repeating the same logic over and over again, we implemented the transformation-mapping logic just once, in mapTransform.

And map() is built-in!

The idea of mapping a transformation function onto a list is so common, that Python's built-in map() does exactly that. The built-in map() is far more flexible than mapTransform(). And instead of returning a list, it returns an iterable. So, using map(), we could write:

def squareList (nList):
    return list(map(lambda n: n*n, nList));

# Alternatively, just for illustration:
squareList = lambda nList: list(map(lambda n: n*n, nList));

But honestly, given map(), is there even any need to implement our library at all? Maybe not?

Aside: Skipping List-Comprehensions
List-comprehensions are syntactically beautiful, and can be used instead of map() (and filter()) in many places. But at Polydojo, we strictly prefer map() instead. Stylistically, we think map() is clearer. Also, if we have to port code to JavaScript,map() calls are easier to translate.

Filtering - More Library Functions

Let's continue with the library example. Let's say that in addition to transformation functions, you're tasked with writing filtering functions too.

Filtering Evens

Your library should include a function keepEvens() which accepts a list of integers and returns a list of just the even numbers therein. This is easy:

def keepEvens (nList):
    result = [];
    for n in nList:
        if n % 2 == 0:
            result.append(n);
    return result;

Filtering Leap Years:

Your library should include a function keepLeaps() which accepts a list of positive integers representing years (like 1990, 2000 etc.) and returns a list with just the leap years therein.

Remember, a year is a leap year if it's a multiple of 4 and 100, or otherwise if it's a multiple of 4 but not of 100.

def checkLeap (n):
    return n % 400 == 0 or (n % 100 != 0 and n % 4 == 0);

def keepLeaps (nList):
    result = [];
    for n in nList:
        if checkLeap(n):
            result.append(n);
    return result;

Filtering Primes

Write a function keepPrimes() which accepts a list of positive integers and returns a list of just the prime numbers therein.

Remember that a number n is prime if it has just two factors, 1 and n.

def checkPrime (n):
    factorCount = 0;
    for i in range(1, n + 1):
        if n % i == 0:
            factorCount += 1;
            if factorCount > 2:
                return False;
    return (factorCount == 2);

def keepPrimes (nList):
    result = [];
    for n in nList:
        if checkPrime(n):
            result.append(n);
    return result;

Aside: You can probably write a more efficient implementation for checkPrime(). But that's outside the scope of this guide.

Notice Another Pattern?

Did you notice that all the filtering examples also follow a pattern? That between keepLeaps() and keepPrimes(), the only difference is checkLeap(n) and checkPrime(n). The rest of the code is identical!

More generally, the only difference between the examples is the filtering-condition itself, which can be easily captured via a function parameter. Consider:

def keepWhere (checkFunc, nList):
    result = [];
    for n in nList:
        if checkFunc(n):
            result.append(n);
    return result;

Now, using keepWhere(), and retaining the definitions of checkLeap() and checkPrime(), we re-implement our functions as:

def keepEvens (nList):
    return keepWhere(lambda n: n % 2 == 0, nList);

def keepLeaps (nList):
    return keepWhere(checkLeap, nList);

def keepPrimes (nList):
    return keepWhere(checkPrime, nList);

Boom Again! That's Functional Programming!

Clearly, the above implementation is far cleaner in comparison with the non-functional examples above.

And filter() is built-in too!

Like map() is built-in for mapping, filter() is built-in for filtering. And like map(), filter() returns an iterable, not a list.

Using the built-in filter(), we can write:

def keepEvens (nList):
    return list(filter(lambda n: n % 2 == 0, nList));

# Alternatively, for illustration only:
keepEvens = lambda nList: list(filter(lambda n: n%2 == 0, nList))

Keep mapli() and filterli() handy:

If you frequently find yourself writing list(map(.)) or list(filter(.)), consider keeping the following functions handy:

mapli = lambda seq, fn: list(map(fn, seq));
filterli = lambda seq, fn=None: list(filter(fn, seq));

The li suffix in mapli and filterli is short for list.

You'll notice that we've reversed the parameter order. The reversed order is something that you may want to skip, but at Polydojo, it's done to achieve parity with Underscore.js' _.map(), _.filter(), etc. Also, by having the function as the second parameter, multi-line lambdas become a bit-more readable.

def filterLeaps (nList):
    return filterli(nList, lambda n: (
        n % 400 == 0 or (n % 100 != 0 and n % 4 == 0)
    ));

Combining map() & filter():

In many situations, you may need to selectively map certain elements only.

Squaring Evens:

Let's write a function that accepts a list of integers and returns a list of the squares of the evens therein.

def squareEvens (nList):
    evens = filterli(nList, lambda n: n % 2 == 0);
    return mapli(evens, lambda n: n * n);

# Alternatively, _just_ for illustration:
squareEvens = lambda nList: mapli(
    filterli(nList, lambda n: n % 2 == 0),
    lambda n: n * n,
);

Tip: Don't write complex lambdas.

While the above two implementations are equivalent, the def-based version is far more readable. Writing convoluted, highly nested lambda-expressions is a bad idea. Please respect future developers (including yourself) who'll be working with your code.

Tip: Keep mapWhere() handy.

Practically, filtering first and then mapping is far more common than mapping first and then filtering. As filtering first is common, you may want to keep the following helper at hand:

def mapWhere(seq, transform, where):
    return list(map(transform, filter(where, seq)));

Using mapWhere, we can write squareEvens as:

def squareEvens (nList):
    return mapWhere(nList, lambda n: n * n, where=lambda n: n % 2 == 0)

Returning Functions

So far, we've passed functions as parameters to other functions. Clearly, doing so saved us a lot of work. Now, let's take a look at a situation where we'd write a function that returns a function.

Greetings!

Before diving into a real-world examples, consider something simple:

def greet (name="World"):
    return "Hello, %s!" % name;

Above, greet is a simple greeter, with the default greeting "Hello, World!".

Let's say that instead you'd like the default greeting to be "Hello, friend!". In that case:

def greetFriend (name="friend"):
    return "Hello, %s!" % name;

And to make the default greeting "Hello, beautiful!", we'd have:

def greetBeautiful (name="beautiful"):
    return "Hello, %s!" % name;

Did you notice something? All the greeters follow the same pattern. Could we write a function for creating greeters with different default greetings? We sure can!

def make_greet (defaultName):
    def greet (name=defaultName):
        return "Hello, %s!" % name;
    return greet;

greet = make_greet("World");
greetFriend = make_greet("friend");
greetBeautiful = make_greet("beautiful");

The function make_greet defines and returns a greet function. The argument passed to make_greet becomes the default value for greet. Thus, make_greet helps make greeters with different default greetings.

Note that each time make_greet is called, a different greet function is defined and returned. Thus, greet and greetFriend are entirely different functions. For clarity, compare the above with the following:

def buildDict (name="Jon"):
    return {"name": name};

jon = buildDict();        # {"name": "Jon"}
tom = buildDict("Tom")    # {"name": "Tom"}
jim = buildDict("Jim")    # {"name": "Jim"}

It's clear that jon, tom and jim are entirely different dictionaries. Similarly, greet, greetFriend and greetBeautiful are entirely different functions.

Adder Maker

Consider the following:

def makeAdd (n):
    def add (m):
        return n + m;
    return add;

Then, in an interactive Python shell:

>>> add1 = makeAdd(1)
>>> add1(10)
11
>>> add5 = makeAdd(5)
>>> add5(100)
105
>>> makeAdd(2)(3)
5
>>> 

What's makeAdd doing? It accepts a parameter n, and returns a function add. In turn, the returned function, i.e. add, accepts a parameter m and returns n + m.

Notice that although n isn't directly a parameter to add, add can access n because n exists in an outer scope.

VF: Validation Functions

Just last week, we released VF: Validation Functions, which is an open source library for validating dict schemas. Almost every function in VF returns a function!

Here, we'll implement a mini-version of VF.

Let's say you're building a blogging app and storing data as dicts in a NoSQL-ish database like MongoDB, CouchDB or PogoDB. You'll have to deal with posts, comments, users, notifications, email-subscriptions, and other types of documents/dicts.

For validating the schema of a dict, say a post, we'd like to write a schema specification as follows:

POST_SCHEMA_SPEC = {
    "_id": lambda x: type(x) is str,
    "title": lambda x: type(x) is str and 5 <= len(x) <= 50,
    "body": lambda x: type(x) is str and 50 <= len(x) <= 5000,
    "userId": lambda x: type(x) is str,
    "published_time": lambda x: type(x) is int,
}

Basically, POST_SCHEMA_SPEC says that each post should have

Let's write a utility function that accepts a dict-type document doc along with a schema (also a dict), and checks if doc matches schema:

In utils.py:

def checkSchema (doc, schema): 
    if set(doc.keys()) != set(schema.keys()):
        return False;
    # otherwise ...
    for key in doc:
        if not schema[key](doc[key]):
            return False;
    # otherwise ...
    return True;

The checkSchema function simply checks that doc and schema have the same keys, and that each value in doc satisfies the corresponding check-function in schema.

We have a handful of document types: posts, comments, users, notifications, and email-subscriptions. Ideally, the schema for each should be defined in a separate (model-specific) module. For example, postModel.py should deal with posts, commentModel.py with comments, etc.

Now, in postModel.py, we could explicitly pass POST_SCHEMA_SPEC to checkSchema on each call, but as it'll be a module-wide constant, wouldn't it make more sense to define checkPostSchema(doc)?

In postModel.py:

from utils import checkSchema;

POST_SCHEMA = {
    "_id": lambda x: type(x) is str,
    "title": lambda x: type(x) is str and 5 <= len(x) <= 50,
    "body": lambda x: type(x) is str and 50 <= len(x) <= 5000,
    "userId": lambda x: type(x) is str,
    "published_time": lambda x: type(x) is int,
};

def checkPostSchema (doc):
    return checkSchema (doc, POST_SCHEMA);

# ... etc ...

Similarly, in commentModel.py:

from utils import checkSchema;

COMMENT_SCHEMA = {
    # ... etc ...
};

def checkCommentSchema (doc):
    return checkSchema(doc, COMMENT_SCHEMA);

# ... etc ...

Continuing similarly, there would be schema-aware checkers in userModel.py, notificationModel.py, etc. And while this setup is already quite modular and readable, consider:

In utils.py:

def make_schemaChecker (schema):
    def checkSchema (doc):
        if set(doc.keys()) != set(schema.keys()):
            return False;
        # otherwise ...
        for key in doc:
            if not schema[key](doc[key]):
                return False;
        # otherwise ...
        return True;
    return checkSchema;

The function make_schemaChecker(schema) defines and returns the function checkSchema(doc), which is bound to the schema.

Then in postModel.py:

from utils import make_schemaChecker;

checkPostSchema = make_schemaChecker({
    "_id": lambda x: type(x) is str,
    "title": lambda x: type(x) is str and 5 <= len(x) <= 50,
    "body": lambda x: type(x) is str and 50 <= len(x) <= 5000,
    "userId": lambda x: type(x) is str,
    "published_time": lambda x: type(x) is int,
});

# ... etc ...

As you can see, there's no need for writing any intermediate function; and for that matter, no need to even define a separate variable (like POST_SCHEMA_SPEC) for holding the schema specification. Plus, the name of the function make_schemaChecker is pretty self-explanatory. As a result, the code is more readable.

Of course, like postModel.py, similar improvements can be made in commentModel.py, notificationModel.py, etc.

Nesting Support

Continuing the above example, to really see the benefit of make_schemaChecker, let's take a peek at userModel.py:

from utils import make_schemaChecker;

checkUserSchema = make_schemaChecker({
    "_id": lambda x: type(x) is str,
    "name": make_schemaChecker({
        "first": lambda x: type(x) is str,
        "last": lambda x: type(x) is str,
    }),
    "email": lambda x: type(x) is str and '@' in x,
    "address": make_schemaChecker({
        "street": lambda x: type(x) is str,
        "city": lambda x: type(x) is str,
        "state": lambda x: type(x) is str,
        "zip": lambda x: type(x) is str,
    }),
    "joined_timestamp": lambda x: type(x) is int,
    # ... etc ...
});

Did you notice the check-functions against the "name" and "address" keys? As make_schemaChecker returns a function, it can easily be used inline for defining nested schemas!

Aside: You should probably use regular expressions for validating email addresses. We've skipped that above.

VF includes a number of helpers for making simple checker functions. Check out VF's docs/code to know more.

Private Variables In Python:

You've probably heard that Python doesn't have private variables. From an OOP standpoint, I suppose that's true. But every functional programming includes private variables, because you can easily setup variables that are private by scope.

To see what I mean, consider the following:

bank.py:

def buildBankAccount (initialBalance):
    private = {
        "balance": initialBalance,
        "transactions": [{"initial": initialBalance}],
    };
    account = {};

    def getBalance ():
        return private["balance"];
    account["getBalance"] = getBalance;

    def deposit (amount):
        private["balance"] += amount;
        private["transactions"].append({"deposit": amount});
    account["deposit"] = deposit;

    def withdraw (amount):
        if amount > private["balance"]:
            raise Exception("Insufficient Funds");
        # otherwise ...
        private["balance"] -= amount;
        private["transactions"].append({"withdraw": amount});
    account["withdraw"] = withdraw;

    return account;

The function buildBankAccount builds and returns an account dictionary. The private dictionary is not returned, and is not directly exposed via account. The only way to effect private['balance'] is to call either account['deposit'] or account['withdraw'].

Further, a trail of transactions is maintained in private['transactions'], making sure that all deposits/withdrawals are properly logged.

In an interactive Python shell:

>>> from bank import buildBankAccount
>>> 
>>> account = buildBankAccount(1000)
>>> account['getBalance']()
1000
>>> account['deposit'](100)
>>> account['getBalance']()
1100         
>>> account['withdraw'](10)
>>> account['getBalance']()
1090
>>> 
>>> account.keys()
dict_keys(['getBalance', 'deposit', 'withdraw'])
>>> 

What's happening here is that the private dict is accessible within buildBankAccount, but not accessible outside. In effect, private is an OOP-style private variable!

Because getBalance, deposit and withdraw are all defined within buildBankAccount, they can access private. Note that account['deposit'] is simply a reference to the deposit function.

While account is a dictionary, it feels like an object. Wouldn't it be nice if we could say account.getBalance() instead of account['getBalance']()? It sure would! And with Dotsi, you can!

Dotsi: Dot-Accessible Dictionaries

Dotsi is an open-source library that brings JavaScript-like dot-access to Python dicts. That is, using Dotsi, we can write account.getBalance(), instead of account['getBalance']().

Re-implementing bank.py using Dotsi:

import dotsi;

def buildBankAccount (initialBalance):
    private = dotsi.fy({
        "balance": initialBalance,
        "transactions": [{"initial": initialBalance}],
    });
    account = dotsi.fy({});

    def getBalance ():
        return private.balance;
    account.getBalance = getBalance;

    def deposit (amount):
        private.balance += amount;
        private.transactions.append({"deposit": amount});
    account.deposit = deposit;

    def withdraw (amount):
        if amount > private["balance"]:
            raise Exception("Insufficient Funds");
        # otherwise ...
        private.balance -= amount;
        private.transactions.append({"withdraw": amount});
    account.withdraw = withdraw;

    return account;

In an interactive Python shell:

>>> from bank import buildBankAccount
>>> 
>>> account = buildBankAccount(2000)
>>> account.getBalance()
2000
>>> account.deposit(200)
>>> account.getBalance()
2200
>>> account.withdraw(100)
>>> account.getBalance()
2100
>>> 

Convention: Builders & Makers

At Polydojo, we follow the convention that:

You Needn't Write Classes

Given that we can write builder functions, there's practically no need to write new classes. At Polydojo, the only good reason to write a class is to subclass Exception. Apart from subclassing Exception, you probably don't need classes. If you look at our open-source contributions, you'll barely find any classes.

Decorators:

So far, we have:

But sometimes, we may want to do all three of those things at the same time!

Caching Example

Let's say you have three slow-running functions: slow1(), slow2() and slow3(). Each function accepts a single string parameter and, always produces the same output for a given input.

Consider the following:

slow1_cache = {};
def faster1 (input):
    if input not in slow1_cache:
        slow1_cache[input] = slow1(input);
    return slow1_cache[input];

Each time faster1 is called with an input, we check if that input is already in slow1_cache. If it's in there, we'll simply return the corresponding result from the cache. If not, we'll call slow1(input), add the result to the cache and then return it.

Thus, when faster1('foo') is called for the first time, slow1('foo'), will be called, which, let's say, evalutes to 'bar'. At that point slow1_cache will include {'foo': 'bar'}. The next time faster1('foo') is called, we'll return 'bar' directly from the cache.

Similarly, we can write faster2() and faster3():

slow2_cache = {};
def faster2 (input):
    if input not in slow2_cache:
        slow2_cache[input] = slow2(input);
    return slow2_cache[input];

slow3_cache = {};
def faster3 (input):
    if input not in slow3_cache:
        slow3_cache[input] = slow3(input);
    return slow3_cache[input];

Clearly, there's a pattern here. Instead writing a separate faster (cached) version for every slow function, could we write a function that produces a faster version of a slow function? Let's try:

def fasterfy (slowFunc):
    cache = {};
    def fasterFunc (input):
        if input not in cache:
            cache[input] = slowFunc(input);
        return cache[input];
    return fasterFunc;

Now, we can write:

faster1 = fasterfy(slow1);
faster2 = fasterfy(slow2);
faster3 = fasterfy(slow3);

Clearly, using fasterfy is a big help. The code is cleaner, smaller and far more readable.

But now that we've defined faster1, we'll need to refactor the rest of our codebase to use faster1 instead of slow1. And this similarly applies to faster2 and faster3 as well. So, we'll need to make multiple changes across our codebase. But instead of making all those changes, could we make slow1 an alias for the new faster1? Consider:

faster1 = fasterfy(slow1);
slow1 = faster1;

faster2 = fasterfy(slow2);
slow2 = faster2;

faster3 = fasterfy(slow3);
slow3 = faster3;

Or equivalently:

slow1 = fasterfy(slow1);
slow2 = fasterfy(slow2);
slow3 = fasterfy(slow3);

Writing slow1 = fasterfy(slow1) is very similar to writing number = number + 1. After slow1 = fasterfy(slow1), slow1 becomes a cached version of it's former self. And since this cached version is also called slow1, we could avoid making changes to the rest of our codebase!

Decorator Syntax

The idea of writing something like slow1 = fasterfy(slow1) is so common, that there's a special syntax for that. Assuming that the time module and fasterfy are imported/defined, the following two examples are equivalent:

Without Decorator Syntax:

def slow4 (x):
    time.sleep(5);    # Sleep 5 sec.
    return "foobar";

slow4 = fasterfy(slow4);

With Decorator Syntax:

@fasterfy
def slow4 (x):
    time.sleep(5);    # Sleep 5 sec.
    return "foobar";

The @fasterfy decorator syntax is just syntactic sugar. Writing @fasterfy just before def is equivalent to writing slow1 = fasterfy(slow1) immediately after the def-block.

Aside: The function slow4 doesn't do anything interesting, but it's useful for demonstration purposes.

General Decorators

The fasterfy() decorator requires the decorated function (i.e. slow4 etc.) to accept exactly one parameter. But to write a more general decorator, we should use *args and **kwargs instead; so that the decorator can be used to decorate any function.

import time;

def timefy (fn):
    "Simple timing decorator.";
    def wrapper (*args, **kwargs):
        "Wrapper function.";
        t0 = time.time();
        result = fn(*args, **kwargs);
        print("Time taken: %s sec." % (time.time() - t0));
        return result;
    return wrapper;

@timefy
def slow_add (x, y):
    "Simple addition.";
    time.sleep(x + y);
    return x + y;

The @timefy decorator prints the time taken before returning the decorated function's result. At it uses *args and **kwargs, it can can be used to decorate any function. Let's see it in action:

In an interactive Python shell:

>>> slow_add(1, 1)
Time taken: 2.006281852722168 sec.
2
>>> slow_add(1, 2)
Time taken: 3.00673508644104 sec.
3
>>> slow_add(2, 2)
Time taken: 4.0057408809661865 sec.
4
>>> 

As expected, the time taken in seconds is roughly equal to the sum of the numbers passed. That's entirely by definition.

Note that there's no print() call in slow_add(). The printing is done by @timefy. Thus, decorators can be designed to have such (desirable) side-effects.

Wrapper Updating:

Continuing with the previous example, in the same Python shell:

>>> slow_add.__name__
'wrapper'
>>> slow_add.__doc__
'Wrapper function.'
>>> 

The above result may seem strange at first, as it says that slow_add's name and docstring (documentation-string) are 'wrapper' and 'Wrapper function.' respectively. But if you think about it, isn't that expected?

Remember that @timefy is equivalent to slow_add = timefy(slow_add). What does timefy return? It returns wrapper. Thus, slow_add.__name__ is 'wrapper', not 'slow_add'.

But obviously, slow_add's docstring being 'Wrapper function.' is not very helpful. It would be nice if we could retain it's original name and docstring, i.e. 'slow_add' and 'Simple addition.' respectively.

Python's functools module has a helper for that, functools.update_wraper(). Let's use it:

import time;
import functools;

def timefy (fn):
    "Simple timing decorator.";
    def wrapper (*args, **kwargs):
        "Wrapper function.";
        t0 = time.time();
        result = fn(*args, **kwargs);
        print("Time taken: %s sec." % (time.time() - t0));
        return result;
    return functools.update_wrapper(wrapper, fn); # <-- Line Changed

@timefy
def slow_add (x, y):
    "Simple addition.";
    time.sleep(x + y);
    return x + y;

Now, in a Python shell:

>>> slow_add.__name__
'slow_add'
>>> slow_add.__doc__
'Simple addition.'
>>> 

The functools module also includes functools.wraps(), which is a convenience around functools.update_wrapper(). You should check it out.

Combining Decorators:

Retaining the definition of timefy() above, let's write a general caching decorator and then rewrite slow_add():

import json;

def cachefy (fn):
    "JSON-serialization based caching decorator.";
    cache = {};
    def wrapper (*args, **kwargs):
        jkey = json.dumps([args, kwargs]);
        if jkey not in cache:
            cache[jkey] = fn(*args, **kwargs);
        return cache[jkey];
    return functools.update_wrapper(wrapper, fn);

@timefy            # <-- Same as before
@cachefy           # <-- Newly added!
def slow_add (x, y):
    "Simple addition.";
    time.sleep(x + y);
    return x + y;

The cachefy decorator uses json.dumps() for generating a hashable cache-key, jkey. As it uses json.dumps(), cachefy can only work with functions that accept JSON-serializable arguments. (Otherwise, a TyepError will be raised.)

The interesting thing to note, of course, is that slow_add now has two decorators! Without using decorator syntax, that's equivalent to:

slow_add = timefy(cachefy(slow_add));

Let's see it in action:

>>> slow_add(2, 3); # First call, should be slow.
Time taken: 5.0025999546051025 sec.
5
>>> slow_add(2, 3); # Subsequent calls should be FAST!
Time taken: 5.412101745605469e-05 sec.
5
>>> slow_add(2, 3); # FAST!
Time taken: 6.389617919921875e-05 sec.
5
>>> 

As you can see, the first call to slow_add(2, 3) was slow, taking about 5 seconds. But subsequent calls were super-fast, taking about 50 microseconds; as the result would've be returned directly from the cache.

Aside: A microsecond is one-millionth of a second. Thus, a response time of 50 microseconds is 100,000 times faster in comparison with 5 seconds.

Returning Decorators:

Alright. We've seen that decorators are functions that accept and return functions. But as decorators are themselves functions, could we write a function that returns a decorator?

Consider:

def make_transformDecorator (transform):
    def transformDecorator (fn):
        def wrapper (*args, **kwargs):
            return transform(fn(*args, **kwargs));
        return functools.update_wrapper(wrapper, fn);
    return transformDecorator;

upperCase = make_transformDecorator(str.upper);

@upperCase
def concat(x, y):
    return x + y;

And in a Python shell:

>>> concat('foo', 'bar')
'FOOBAR'
>>> concat('Sumukh', 'Barve')
'SUMUKHBARVE'
>>> concat('Poly', 'dojo')
'POLYDOJO'
>>> 

What's happening here?

First and foremost, note that make_transformDecorator is NOT itself a decorator. But instead, it returns transformDecorator, which IS indeed a decorator. Thus, upperCase = make_transformDecorator(str.upper) creates a decorator that calls str.upper on the decorated function's result.

Hence:

concat('Poly', 'dojo')
  => str.upper('Poly' + 'dojo')
  => 'POLYDOJO'

Instead of explicitly defining upperCase, we could have:

@make_transformDecorator(str.upper)
def concat (x, y):
    return x + y;

Flask, Bottle & Vilo: They All Return Decorators!

If you're building a web-app with Flask, you'll often write something like:

@app.route("/hello")
def route_hello ():
    return "Hello, World!";

What's happening here? It's simple: app.route() returns a decorator for binding route_hello() to the "/hello" path.

Bottle and Vilo also follow a similar, decorator-based routing pattern.

Aside: As the author of Vilo, I'm obviously biased. But it's a pretty sleek framework! In fact, this webpage, the one you're reading right now, is served via Vilo. (This blog is powered by ViloLog, which builds atop Vilo.)

You Now Know Functional Programming!

In this guide, we've covered a number of aspects of functional programming. We passed functions as arguments, defined them locally, returned them, did all of that together by defining decorators, and we even wrote a function that returns a decorator.

I sincerely hope that you had fun. And I hope that this guide will help propel you further on your path toward eschewing classical OOP in favor of Functional Programming. The folks who wrote Lisp and Scheme really knew what they were doing. And while Python and JavaScript have a different syntax from Lisp/Scheme, the languages are entirely functional.

Everything I said in this guide about Polydojo's practices, to the best of my knowledge, is entirely true. You can actually check out our open source contributions to find examples of maker functions, builder functions, and of course, barely any classes.

As we gather feedback from you, we'll expand this guide with additional examples and explanations. And at this point, if you'd like to explore functional programming further, I first recommend perusing the docs for Python's built-in functools module.

Thank You!

Thank you, for reading all the way to the end. I sincerely hope that this guide was helpful. If you have any questions, comments or suggestions, please feel free to ask. You can email me at [email protected]. I'll always be happy to hear from you.

PS:
Any function that accepts/returns a function is called a Higher Order Function. I find this a bit weird, as it seems to imply that such functions are somehow special. They aren't. They're just functions. And all functions are equally awesome.


Next: Solving NxN Sudoku Via Deductions & BFS in Python
Previous: Build Your Own Python Template Engine