Okay, it wasn't that I didn't understand the principles; I'd read plenty of articles covering it, looked at lots of code in Groovy and Scala. No, the idea behind currying seemed pretty easy enough: using functions to create functions so that the required parameters can be reduced (by including them as part of the generated function). Yep, all pretty straightforward.

What I didn't understand was why the hell I'd ever need to do that. What I'd been lacking, all these years, was a decent use case.

And a few weeks ago, one presented itself from the most unlikely of places: me!

When I decide I'm going to knuckle down and learn a new programming language, I like to have a crack a simple poject that would have some real-world value. (I've done 'Hello World' apps to death.) Now I'm a big fan of the Alfred launcher, so I thought I'd have a go at a workflow written in Python. I decided to go for a basic date calculator, as that's something I always seem to need, and I sometimes can't be bothered to go find the iPhone.

dcalc for Alfred

As well as being able to use dates and times in the calculations, I thought it would be nice if I included a constants (like Christmas, birthays) and variables (Easter, which moves around in ways I cannot begin to fathom). I also thought it would be nice to make easy for folk to add their own functions in the future. To this end, I came up with a simple scheme which did the business using a list of Python functions and a map.

So we start with the function:

def next_easter(date_format):
    easter_rule = rrule(freq=YEARLY, byeaster=0)
    return easter_rule.after(_get_current_date(), inc=False), date_format

This returns the date that the next Easter Sunday falls on. It takes a date format as a parameter and also returns it, along with the calculated date, when the function exits. We've got a similar function for giving back the current date:

def current_date(date_format):
    return _get_current_date(), date_format

To expose the functions to the workflow, I simply put them in a map:

DATE_FUNCTION_MAP = {

    "date": current_date,
    "today": current_date,
    "time": current_time,
    "now": now,
    "yesterday": yesterday,
    "tomorrow": tomorrow,
    "easter": next_easter,
}

The map key acts as the keyword to call the function and supply it to the workflow. When the user types the key, the workflow will pick the function from the map, run it and return the date.

return DATE_FUNCTION_MAP[key](date_format_str)

As you can see, the function can do anything as long as it takes a date format as a single parameter and returns a date object along with a date format. (And I love the fact that you can return more than one value from a Python function.) I was pretty chuffed with the whole setup because it meant I could run clever little calculations like this:

dcalc for Alfred

Now the trouble came when a friend asked if I could add another function to the workflow. 'Name it,' I said confidently. 'Well, if I type in a day of the week, could you tell me the next date that it falls on?' 'I reckon so. What if I type "Tue" and the current day is Tuesday?' 'Then I want the date for the next Tuesday.'

No problem. All I needed was a function that took the day of the week and returned the next occurrence. The only thing was that this function has to take a single parameter (the date format) and return two values (a date object and a date format). So, my first uneducated attempt looked like this:

DATE_FUNCTION_MAP = {

    "date": current_date,
    "today": current_date,
    "time": current_time,
    "now": now,
    "yesterday": yesterday,
    "tomorrow": tomorrow,
    "easter": next_easter,
    "mon": get_Monday,
    "tue": get_Tuesday,
    "wed": get_Wednesday,
    "thu": get_Thursday,
    "fri": get_Friday,
    "sat": get_Saturday,
    "sun": get_Sunday
}

Seven functions, one for each day of the week. It wasn't bad; it did the job, but it just didn’t feel very Pythony. What I was really after was single function that could take a parameter to indicate the day of the week I was looking to process:

DATE_FUNCTION_MAP = {

    "date": current_date,
    "today": current_date,
    "time": current_time,
    "now": now,
    "yesterday": yesterday,
    "tomorrow": tomorrow,
    "easter": next_easter,
    "mon": weekday('mon'),
    "tue": weekday('tue'),
    "wed": weekday('wed'),
    "thu": weekday('thu'),
    "fri": weekday('fri'),
    "sat": weekday('sat'),
    "sun": weekday('sun'),
}

Much better to look at , but it wouldn't work; I was now supplying a second parameter, the weekday, which means that when it came to using the function I would be calling a function (because of the parameter list) rather than just supplying one.

What to do . . .

And this is when I had my currying epiphany.

Yes, the weekday function was being called, but what if I used it to generate a function object that carried the correct parameter and return that instead? And I'd finally discovered a use for currying: reducing the required parameter list by returning a function with the extra parameter included.

def weekday(day_of_week_str):
    """
    This one one is a little bit trickier. We don't want a separate
    function for each day of the week, so we need to use a bit of
    currying to return a function that can handle the mapping.
    :param day: The day of the week as a string, which we will use to map into a table for the
    calculation.
    :param day_of_week_str: the day of week as a three character string
    :return: a function that will calculate the day of week and return it along with the format
    """
    def _weekday(date_format):
        return _get_current_date() + DAY_MAP[day_of_week_str.lower()], date_format

    return _weekday

In Python you can define and function inside a function and then return it in exactly the same way as you can return a value or an object. Here, I'm returning a function that takes a single date_format parameter and returns a date object and the same parameter – just as my DATE_FUNCTION_MAP requires. By handling the day key string (day_of_week_str) inside the function returned, I've reduced the need for the parameter when the map is used.


Comments

comments powered by Disqus