Currying in Python

  mtomassoli        2012-03-19 12:59:10       4,395        0    

What is Currying?

Currying is like a kind of incremental binding of function arguments. Let’s define a simple function which takes 5 arguments:

1def f(a, b, c, d, e):
2    print(a, b, c, d, e)

In a language where currying is supported, f is a function which takes one argument (a) and returns a function which takes 4 arguments. This means that f(5) is the following function:

1def g(b, c, d, e):
2    f(5, b, c, d, e)

We could emulate this behavior the following way:

1def f(a):
2    def g(b, c, d, e):
3        print(a, b, c, d, e)
4    return g
5 
6f1 = f(1)
7f1(2,3,4,5)

Now, f(1) returns the function g(b, c, d, e) which, for all b, c, d and e, behaves exactly like f(1, b, c, d, e). Since g is a function, it should support currying as well. Then what we really want is this:

1def f(a):
2    def g(b):
3        def h(c, d, e):
4            print(a, b, c, d, e)
5        return h
6    return g
7 
8f(1)(2)(3,4,5)

So what is f? It’s a function which takes an argument a and returns another function g which “remembers” that argument. The same way, g is a function which takes an argument b and returns another function h which “remembers” that argument. Basically, h is the function obtained from f by binding the first two arguments to a and b, respectively. Note that the last line of the code above is equivalent to this:

1f1 = f(1)
2f12 = f1(2)
3f12(3,4,5)

I know what you’re thinking: we aren’t done yet! Here’s the final version in all its glory:

01def f(a):
02    def g(b):
03        def h(c):
04            def i(d):
05                def j(e):
06                    print(a, b, c, d, e)
07                return j
08            return i
09        return h
10    return g
11 
12f(1)(2)(3)(4)(5)

That’s currying for you!

But what about f(1, 2, 3, 4, 5)? Ops… f doesn’t take 5 arguments anymore :(

Well, there are some functional languages (Haskell, for instance) where function application is indicated by juxtaposition. For instance, f(a, b, c, d, e) would be expressed like this:

1f a b c d e

Does that mean that f is a function which takes 5 arguments? Nope. Like in the example above, f is a function which takes an argument and returns a function which takes an argument, and so on… Let’s see it this way: the space (‘ ‘) is an operator that applies the right operand to the left operand (a function). The space is left-associative, meaning that the expression above is grouped this way:

1((((f a) b) c) d) e

If you don’t like the operator space, let’s use some other operator:

1((((f<-a)<-b)<-c)<-d)<-e

The operator ‘<-‘ passes its right operand to its left operand, which is a function. Indeed, these are functions:

  • f
  • f<-a
  • (f<-a)<-b
  • ((f<-a)<-b)<-c
  • (((f<-a)<-b)<-c)<-d

We can remove the brackets, of course:

1f<-a<-b<-c<-d<-e

What am I trying to say? Simply that classic currying is better left to languages which use juxtaposition to pass arguments to functions.

Why currying?

Simple enough: it’s an easy way to get specialized functions from more general functions. Here are a few simple examples:

  • times3 = myMul(3)
    • Usage:
      listTimes3 = map(times3, list)          # evaluates myMul(3, x) for all x in list
  • printErrMsg = printMsg(“Error”)
    printWrnMsg = printMsg(“Warning”)
    • Usage:
      printErrMsg(“Divide by Zero!”)
      printWrnMsg(“You should save your document.”)
  • lexicSort = mySort(lexicCmp)
    • Usage:
      lexicSort(listOfStrings)

Currying VS partial application

Partial application is partial binding:

1from functools import partial
2 
3def f(a, b, c, d):
4    print(a, b, c, d)
5 
6g = partial(f, 1, 2, 3)
7g(4)

Looks familiar?

Is that currying? Not quite. It’s more like manual currying:

1from functools import partial
2 
3def f(a, b, c, d):
4    print(a, b, c, d)
5 
6g = partial(partial(partial(f, 1), 2), 3)
7g(4)

Is currying right for us?

We said that, in Haskell, we can write

1f a b c d e

while, in Python, we would have to write

1f(1)(2)(3)(4)(5)

I don’t know about you, but I prefer this one:

1f(1, 2, 3, 4, 5)

We should also keep in mind that Python has keyword argument, so this should also be allowed:

1f(1, 2, e = 5)

Is that real currying? It depends on whom you ask. But if you like it and find it useful, who cares?

Incremental binding

So let’s call it incremental binding because we can bind arguments in any order we like and how many arguments we feel like at once. Let’s pretend we have implemented a magical function called cur. Here’s a simple function we can use to test cur:

1# Simple Function.
2def func(a, b, c, d, e, f, g = 100):
3    print(a, b, c, d, e, f, g)

Now let’s create a curried version of that function:

1# f is the "curried" version of func.
2f = cur(func)

Now we would like to be able to write the following (and get the expected result, of course!):

1c1 = f(1)
2c2 = c1(2, d = 4)               # Note that c is still unbound
3c3 = c2(3)(f = 6)(e = 5)        # now c = 3
4c3()                            # () forces the evaluation              <====
5                                #   it prints "1 2 3 4 5 6 100"
6c4 = c2(30)(f = 60)(e = 50)     # now c = 30
7c4()                            # () forces the evaluation              <====
8                                #   it prints "1 2 30 4 50 60 100"

The lines that print to the screen are marked with the double arrow ‘<====’.

The first line binds the argument a to 1 and save this less-free (i.e. more-bounded) function in c1. Now we have two currying-able functions: f and c1.

Here comes the interesting part: the second line binds b to 4 and d to 4, while c remains unbounded. Basically, at each step we can bind some positional arguments and some keyword arguments. If we have bound 2 arguments so far, then the 3rd argument will be the next positional argument to be bound. Indeed, line 3 binds c to 3 and then binds the two keyword arguments f and e (the order doesn’t matter, as you can see). Note that I talk of positional arguments and keyword arguments, but here positional and keyword don’t describe the arguments but the way the arguments are bound.

Note how line 4 forces the evaluation.

But… shouldn’t the evaluation be automatic? I would say no and here’s why.

As you may know, a function is called Referentially Transparent if its behavior depends on its arguments alone. This means that a function without arguments is a constant: it behaves the same way every time. Since functions don’t have side effects in pure functional languages, “to behave” is not the most appropriate verb: I should have said that a constant function always returns the same value. Now let’s look at c3 in the previous example. Is that a function? In a pure functional language the answer would be no: it’s a constant. But that means we can’t evaluate a function more than once. As you can see, func has side effects: it prints to the screen! Why shouldn’t we be able to call c3 more than once? So, manual evaluation is really a feature! Learning from other languages is a good think but trying to emulate the same exact behavior at all cost is a mistake.

The last two lines in the example above show another binding starting from c2.

Here’s our second example:

1f = curr(func)                  # f is a "curried" version of func
2                                # curr = cur with possibly repeated
3                                #   keyword args
4c1 = f(1, 2)(3, 4)
5c2 = c1(e = 5)(f = 6)(e = 10)() # ops... we repeated 'e' because we     <====
6                                #   changed our mind about it!
7                                #   again, () forces the evaluation
8                                #   it prints "1 2 3 4 10 6 100"

What is curr? It’s a variant of cur which lets you rebind arguments by keyword. In the example above, we first bind e to 5 and then rebind it to 10. Note that, even if that’s a one-line statement, we’re binding in 3 different steps. Finally, we forces the evaluation with ‘()’.

But I can feel some of you still want automatic evaluation (Why???). Ok, me too. So how about this:

1f = cur(func, 6)        # forces the evaluation after 6 arguments
2c1 = f(1, 2, 3)         # num args = 3
3c2 = c1(4, f = 6)       # num args = 5
4c3 = c2(5)              # num args = 6 ==> evalution                    <====
5                        #   it prints "1 2 3 4 5 6 100"
6c4 = c2(5, g = -1)      # num args = 7 ==> evaluation                   <====
7                        #   we can specify more than 6 arguments, but
8                        #   6 are enough to force the evaluation
9                        #   it prints "1 2 3 4 5 6 -1"

You see how nicely default-valued arguments are handled? In the example above, 6 arguments are enough to force the evaluation, but we can also define all 7 of them (if we’re quick enough!). If we feel like it, we could even use some reflection to find out the number of non default-valued arguments (or at least I think so: I’ve been programming in Python for just 3 days and I haven’t looked at reflection yet). I don’t feel like it, but it’s simple to write a little wrapper around cur and curr.

Are we sure that our (still imaginary) cur works like expected? Let’s try something more convoluted:

01def printTree(func, level = -1):
02    if level == -1:
03        printTree(cur(func), level + 1)
04    elif level == 6:
05        func(g = '')()      # or just func('')()
06    else:
07        printTree(func(0), level + 1)
08        printTree(func(1), level + 1)
09 
10printTree(f)

What does it do? It prints the first 64 non negative integers in base two. Here’s the exact output:

010 0 0 0 0 0
020 0 0 0 0 1
030 0 0 0 1 0
040 0 0 0 1 1
050 0 0 1 0 0
060 0 0 1 0 1
070 0 0 1 1 0
080 0 0 1 1 1
090 0 1 0 0 0
100 0 1 0 0 1
110 0 1 0 1 0
120 0 1 0 1 1
130 0 1 1 0 0
140 0 1 1 0 1
150 0 1 1 1 0
160 0 1 1 1 1
170 1 0 0 0 0
180 1 0 0 0 1
190 1 0 0 1 0
200 1 0 0 1 1
210 1 0 1 0 0
220 1 0 1 0 1
230 1 0 1 1 0
240 1 0 1 1 1
250 1 1 0 0 0
260 1 1 0 0 1
270 1 1 0 1 0
280 1 1 0 1 1
290 1 1 1 0 0
300 1 1 1 0 1
310 1 1 1 1 0
320 1 1 1 1 1
331 0 0 0 0 0
341 0 0 0 0 1
351 0 0 0 1 0
361 0 0 0 1 1
371 0 0 1 0 0
381 0 0 1 0 1
391 0 0 1 1 0
401 0 0 1 1 1
411 0 1 0 0 0
421 0 1 0 0 1
431 0 1 0 1 0
441 0 1 0 1 1
451 0 1 1 0 0
461 0 1 1 0 1
471 0 1 1 1 0
481 0 1 1 1 1
491 1 0 0 0 0
501 1 0 0 0 1
511 1 0 0 1 0
521 1 0 0 1 1
531 1 0 1 0 0
541 1 0 1 0 1
551 1 0 1 1 0
561 1 0 1 1 1
571 1 1 0 0 0
581 1 1 0 0 1
591 1 1 0 1 0
601 1 1 0 1 1
611 1 1 1 0 0
621 1 1 1 0 1
631 1 1 1 1 0
641 1 1 1 1 1

Was it really necessary to cut and paste all that? Probably not… I could’ve written it by hand.

Here’s the code again (that’s what long unneeded listings do to your articles):

01def printTree(func, level = -1):
02    if level == -1:
03        printTree(cur(func), level + 1)
04    elif level == 6:
05        func(g = '')()      # or just func('')()
06    else:
07        printTree(func(0), level + 1)
08        printTree(func(1), level + 1)
09 
10printTree(func)

Our printTree is a function which takes a function func and an optional level which defaults to -1. There are 3 cases:

  • level is –1:
    • we didn’t call ourselves, so someone has just called us and passed to us (we hope so) a “normal” function;
    • we call ourselves recursively, but this time the way we like: with a curried version of func and with level 0. That’s better.
  • level is 6:
    • we called ourselves recursively many times and we’re finally at level 6;
    • our (very own private) func should have the first 6 arguments already bounded;
    • we bind the last argument gto something that won’t show up on the screen: the empty string(if you’re wondering why I disregard the last argument this way, you should try to include a monotonous 128-line output in one of your articles!);
    • we evaluate func and print a number on the screen!
  • level is between 0 and 5 (limits included):
    • our func is a function which have the first level arguments bound and the remaining arguments unbound;
    • we can’t evalute func: we need more arguments;
    • we control the argument x in position level;
    • we bound x to 0 and let another instance of printTree handle the remaining arguments;
    • we now bound x to 1 and call yet another instance of printTree.

If you hate recursion, you can very well skip this example and go to the last one which is… ops… recursive as well:

1def f2(*args):
2    print(", ".join(["%3d"%(x) for x in args]))
3 
4def stress(f, n):
5    if n: stress(f(n), n - 1)
6    else: f()               # enough is enough
7 
8stress(cur(f2), 100)

It prints this:

1100,  99,  98,  97,  96,  95,  94,  93,  92,  91,  90,  89,  88,  87,  86,  85,
2 84,  83,  82,  81,  80,  79,  78,  77,  76,  75,  74,  73,  72,  71,  70,  69,
3 68,  67,  66,  65,  64,  63,  62,  61,  60,  59,  58,  57,  56,  55,  54,  53,
4 52,  51,  50,  49,  48,  47,  46,  45,  44,  43,  42,  41,  40,  39,  38,  37,
5 36,  35,  34,  33,  32,  31,  30,  29,  28,  27,  26,  25,  24,  23,  22,  21,
6 20,  19,  18,  17,  16,  15,  14,  13,  12,  11,  10,   9,   8,   7,   6,   5,
7  4,   3,   2,   1

Why did I use recursion? Because I was so caught up in implementing cur that I forgot Python has loop constructs :)

Beginners shouldn’t have too much of a problem with this example and I’m getting a little bored myself, so let’s proceed!

Wonderful, but how do we implement cur???

Let’s proceed a step at a time.

We might implement a function which remembers the arguments it has received so far:

01myArgs = []
02def f(*args):
03    global myArgs           # we will refer to the global var
04    if len(args):           # some more args!
05        myArgs += args
06    else:                   # time to evaluate...
07        print(*myArgs)
08 
09f(1,2)
10f(3,4)
11f()

The last line prints “1 2 3 4”.

Our function asks to receive an arbitrary number of arguments packed in the local array args and then there are two cases:

  1. if len(args) is non-zero, we have received some arguments from the caller so we append them to our global array myArgs;
  2. if len(args) is zero, the caller wants to force the evaluation so we act as if we were called with all the arguments in myArgs from the start.

Any objections? Like

  1. Why aren’t we writingg = f(1,2)but justf(1,2)?
  2. What aboutf(1,2)(3,4)()?
  3. What if we want two different bindings of f at the same time?

Objection 2 is easy to deal with:

01myArgs = []
02def f(*args):
03    global myArgs           # we will refer to the global var
04    if len(args):           # some more args!
05        myArgs += args
06        return f
07    else:                   # time to evaluate...
08        print(*myArgs)
09 
10f(1, 2)(3, 4)()

We just needed to add return f so now f(1, 2) returns f and so on…

What about objection 3? That is strictly related the first objection: we don’t need assignments because we are allowing a single global binding per function and that defeats the purpose of having currying. The solution is easy: let’s give each partially bounded f its private myArgs.

We can do that by wrapping myArgs and f inside another function so that myArgs is not shared anymore:

01def g(*args):
02    myArgs = []
03    def f(*args):
04        nonlocal myArgs         # now it's non-local: it isn't global
05        if len(args):           # some more args!
06            myArgs += args
07            return f
08        else:                   # time to evaluate...
09            print(*myArgs)
10    return f(*args)
11 
12g1 = g(1, 2)
13g2 = g(10, 20)
14g1(3, 4)()
15g2(30, 40)()

The last two lines print “1 2 3 4” and “10 20 30 40”, respectively. We did it!

Let’s try something a little different:

1a = g(1)
2b = g(2)
3a1 = a(1, 1)()
4a2 = a(2, 2)()

It prints “1 1 1” and… ops… “1 1 1 2 2”. That’s right: functions a and b behave exactly like our old f with its global myArgs. It’s true that myArgs isn’t global anymore, but while a and b receive two different myArgs, every successive use of a will use that same array. So the last line adds 2, 2 to a’s myArgs which already contains 1, 1, 1. The second line is just there for symmetry :)

How do we solve this problem? Hmm… more recursion!!! … Nope… that wouldn’t work. How about less recursion?

Let’s look at the code again: “a = g(1)” returns an f-type (so to speak) function so the next “a(1, 1)” doesn’t work correctly (we’ve just postponed the problem we were facing from the start with f). And if a itself returned “g(1, 1, 1)”? It’d be as if the caller had performed a single binding by calling g. History would be forgotten and totally irrelevant. Every binding would look like the first and only binding!

Before proceeding, we should get rid of another flaw. Look at this example:

1a = g(1)
2b = g(2)
3a(1, 1)
4a(2, 2)()

This prints “1 1 1 2 2”. Our old f, here referred to by a, still likes to remember things it shouldn’t. We can solve both problems at the same time:

01def g(*args):
02    myArgs = args
03    def f(*args):
04        nonlocal myArgs
05        if len(args):           # some more args!
06            return g(*(myArgs + args))
07        else:                   # time to evaluate...
08            print(*myArgs)
09    return f
10 
11a = g(1)
12b = g(2)
13a(1, 1)
14a(2, 2)()

Now, g gives each f its arguments and no f can change them in any way. When f receives more arguments (args), it creates and returns a new version of f which includes all the arguments seen so far. As you can see, the new f is created by calling g one more time at line 6.

We can do even better:

1def g(*myArgs):
2    def f(*args):
3        if len(args):           # some more args!
4            return g(*(myArgs + args))
5        else:                   # time to evaluate...
6            print(*myArgs)
7    return f

The argument args was already a local variable of g (in a sense), so why create another local variable myArgs? Also, we don’t need nonlocal because we won’t modify myArgs: we’ll just read it.

We still miss a step: I hope you won’t code any single function by hand like that! We should implement cur as a curried version of a general function. That’s easy enough:

01def cur(func):
02    def g(*myArgs):
03        def f(*args):
04            if len(args):           # some more args!
05                return g(*(myArgs + args))
06            else:                   # time to evaluate...
07                func(*myArgs)
08        return f
09    return g
10 
11def f(a, b, c, d, e):
12    print(a, b, c, d, e)
13cf = cur(f)
14 
15a = cf(1)
16b = cf(2)
17a(1, 1)
18a(2, 2)(3, 4)()

Now we can include keyword arguments:

01def cur(func):
02    def g(*myArgs, **myKwArgs):
03        def f(*args, **kwArgs):
04            if len(args) or len(kwArgs):    # some more args!
05                newArgs = myArgs + args
06                newKwArgs = dict.copy(myKwArgs);
07                newKwArgs.update(kwArgs)
08                return g(*newArgs, **newKwArgs)
09            else:                           # time to evaluate...
10                func(*myArgs, **myKwArgs)
11        return f
12    return g
13 
14def f(a, b, c, d, e):
15    print(a, b, c, d, e)
16cf = cur(f)
17 
18a = cf(1, e = 10)
19b = cf(2)
20a(1, 1)
21a(2, d = 9)(3)()

Final Version

Now you should be able to understand the final version, which has some additional features. Here’s the entire code (examples included):

01def genCur(func, unique = True, minArgs = -1):
02    """ Generates a 'curried' version of a function. """
03    def g(*myArgs, **myKwArgs):
04        def f(*args, **kwArgs):
05            if len(args) or len(kwArgs):    # some more args!
06                # Allocates data to assign to the next 'f'.
07                newArgs = myArgs + args
08                newKwArgs = dict.copy(myKwArgs)
09 
10                # Adds/updates keyword arguments.
11                if unique:
12                    # We don't want repeated keyword arguments.
13                    for k in kwArgs.keys():
14                        if k in newKwArgs:
15                            raise(Exception("Repeated kw arg while unique = True"))
16                newKwArgs.update(kwArgs)
17 
18                # Checks whether it's time to evaluate func.
19                if minArgs >= 0 and minArgs <= len(newArgs) + len(newKwArgs):
20                    return func(*newArgs, **newKwArgs)  # time to evaluate func
21                else:
22                    return g(*newArgs, **newKwArgs)     # returns a new 'f'
23            else:                               # the evaluation was forced
24                func(*myArgs, **myKwArgs)
25        return f
26    return g
27 
28def cur(f, minArgs = -1):
29    return genCur(f, True, minArgs)
30 
31def curr(f, minArgs = -1):
32    return genCur(f, False, minArgs)
33 
34# Simple Function.
35def func(a, b, c, d, e, f, g = 100):
36    print(a, b, c, d, e, f, g)
37 
38# NOTE: '<====' means "this line prints to the screen".
39 
40# Example 1.
41f = cur(func)                   # f is a "curried" version of func
42c1 = f(1)
43c2 = c1(2, d = 4)               # Note that c is still unbound
44c3 = c2(3)(f = 6)(e = 5)        # now c = 3
45c3()                            # () forces the evaluation              <====
46                                #   it prints "1 2 3 4 5 6 100"
47c4 = c2(30)(f = 60)(e = 50)     # now c = 30
48c4()                            # () forces the evaluation              <====
49                                #   it prints "1 2 30 4 50 60 100"
50 
51print("\n------\n")
52 
53# Example 2.
54f = curr(func)                  # f is a "curried" version of func
55                                # curr = cur with possibly repeated
56                                #   keyword args
57c1 = f(1, 2)(3, 4)
58c2 = c1(e = 5)(f = 6)(e = 10)() # ops... we repeated 'e' because we     <====
59                                #   changed our mind about it!
60                                #   again, () forces the evaluation
61                                #   it prints "1 2 3 4 10 6 100"
62 
63print("\n------\n")
64 
65# Example 3.
66f = cur(func, 6)        # forces the evaluation after 6 arguments
67c1 = f(1, 2, 3)         # num args = 3
68c2 = c1(4, f = 6)       # num args = 5
69c3 = c2(5)              # num args = 6 ==> evalution                    <====
70                        #   it prints "1 2 3 4 5 6 100"
71c4 = c2(5, g = -1)      # num args = 7 ==> evaluation                   <====
72                        #   we can specify more than 6 arguments, but
73                        #   6 are enough to force the evaluation
74                        #   it prints "1 2 3 4 5 6 -1"
75 
76print("\n------\n")
77 
78# Example 4.
79def printTree(func, level = -1):
80    if level == -1:
81        printTree(cur(func), level + 1)
82    elif level == 6:
83        func(g = '')()      # or just func('')()
84    else:
85        printTree(func(0), level + 1)
86        printTree(func(1), level + 1)
87 
88printTree(func)
89 
90print("\n------\n")
91 
92def f2(*args):
93    print(", ".join(["%3d"%(x) for x in args]))
94 
95def stress(f, n):
96    if n: stress(f(n), n - 1)
97    else: f()               # enough is enough
98 
99stress(cur(f2), 100)

That’s all!

Please feel free to let me know what you think by leaving a comment!

Source : http://mtomassoli.wordpress.com/2012/03/18/currying-in-python/

PYTHON  CURRING  BINDING  IMPLEMENT 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Frontend vs backend