Currying



General Idea

Curry with Currying

In mathematics and computer science, currying is the technique of breaking down the evaluation of a function that takes multiple arguments into evaluating a sequence of single-argument functions.

Currying is not only used in programming but in theoretical computer science as well. The reason is that it is often easier to transform multiple argument models into single argument models.

The need for currying arises for example in the following case: Let us assume that we have a context, in which we can only use a function with one argument. We have to use a function with multiple parameters. So we need a way to transform this function into a function with just one parameter. Currying provides the solution to this problem. Currying means rearranging a multiple-parameter function into a chain of functions applied to one argument. It is always possible to transform a function with multiple arguments into a chain of single-argument functions.

Python is not equipped for this programming style. This means there are no special syntactical constructs available to support currying. On the other hand, Python is well suited to simulate this way of programming. We will introduce various ways to accomplish this in this chapter of our tutorial.

Some readers not interested in the mathematical details can skip the following two subchapters, because the a mathematically focussed.



Composition of Functions

We define the composition h of two function f and g

$$h(x) = g(f(x)$$

often written as

$$h = (g \circ f)(x)$$

in the following Python example.

The composition of two functions is a chaining process in which the output of the inner function becomes the input of the outer function.



Currying

As we have already metnioned in the introduction, currying means transforming a function with multiple parameters into a chain of functions with one parameter.

We will start with the simplest case, i.e. two parameters. Given is a function $f$ with two parameters $x$ and $y$. We can curry the function in the following way:

We have to find a function $g$ which returns a function $h$ when it is applied to the second parameter $y$ of $f$. $h$ is a function which can be applied to the first parameter $x$ of $f$, satisfying the condition

$$f(x, y) = g(y)(x) = h(x)$$

Now we have a look at the general case of currying. Let us assume that we have a function $f$ with $n$ parameters:

$$f(x_1, x_2, \dots x_n)$$

Currying leads to a cascade of functions:

$$f_{n-1} = f_n(x_n)$$$$...$$$$f_1 = f_2(x_2)$$$$f(x_1, x_2, \dots x_n) = f_1(x_1)$$

Composition of Functions in Python

Two Functions

The function compose can be used to create to compose two functions:

def compose(g, f):
    def h(x):
        return g(f(x))
    return h

We will use our compose function in the next example. Let's assume, we have a thermometer, which is not working accurate. The correct temperature can be calculated by applying the function readjust to the temperature values. Let us further assume that we have to convert our temperature values into degrees fahrenheit. We can do this by applying compose to both functions:

def celsius2fahrenheit(t):
    return 1.8 * t + 32

def readjust(t):
    return 0.9 * t - 0.5

convert = compose(readjust, celsius2fahrenheit)

convert(10), celsius2fahrenheit(10)
Output::
(44.5, 50.0)

The composition of two functions is generally not commutative, i.e. compose(celsius2fahrenheit, readjust) is different from compose(readjust, celsius2fahrenheit)

convert2 = compose(celsius2fahrenheit, readjust)

convert2(10), celsius2fahrenheit(10)
Output::
(47.3, 50.0)

convert2 is not a solution to our problem, because it is not readjusting the original temperatures of our thermometer but the transformed Fahrenheit values!

"compose" with an Arbitrary Number of Arguments

The function compose which we have just defined can only cope with single-argument functions. We can generalize our function compose so that it can cope with all possible functions. This is not currying of course but nevertheless also an interesting function.

def compose(g, f):
    def h(*args, **kwargs):
        return g(f(*args, **kwargs))
    return h

Example using a function with two parmameters.

def BMI(weight, height):
    return weight / height**2

def evaluate_BMI(bmi):
    if bmi < 15:
        return "Very severely underweight"
    elif bmi < 16:
        return "Severely underweight"
    elif bmi < 18.5:
        return "Underweight"
    elif bmi < 25:
        return "Normal (healthy weight)"
    elif bmi < 30:
        return "Overweight"
    elif bmi < 35:
        return "Obese Class I (Moderately obese)"
    elif bmi < 40:
        return "Obese Class II (Severely obese)"
    else:
        return "Obese Class III (Very severely obese)"


f = compose(evaluate_BMI, BMI)

weight = 1
while weight > 0:
    weight = float(input("weight (kg) "))
    height = float(input("height (m) "))
    print(f(weight, height))
weight (kg) 70
height (m) 1.76
Normal (healthy weight)
weight (kg) 0
height (m) 1
Very severely underweight

BMI Chart

This is off-topic, because it is not about currying. Yet, it is nice to have a BMI chart, especially if it is created with Python means. We use contour plots from the Matplotlib module in our program. If you want to learn more about contour plots, you can go to chapter on Contour Plots of our Matplotlib tutorial.

%matplotlib inline

import matplotlib.pyplot as plt
import pylab as pl

xlist = pl.linspace(1.2, 2.0, 50)
ylist = pl.linspace(50, 100, 50)
X, Y = pl.meshgrid(xlist, ylist)

Z = Y / (X**2)
plt.figure()
levels = [0, 15, 16, 18.5, 25, 30, 35, 40, 100]

cp = plt.contour(X, Y, Z, levels)
pl.clabel(cp, colors = 'k', fmt = '%2.1f', fontsize=12)

c = ('#0000FF', '#0020AA', '#008060', '#00AA40', '#00FF00', '#40AA00', '#992200', '#FF0000')
cp = plt.contourf(X, Y, Z, levels,  colors=c)
plt.colorbar(cp)
plt.title('Contour Plot')
plt.xlabel('x (m)')
plt.ylabel('y (kg)')

plt.show()

Currying Examples in Python

Currying BMI

We used the function BMI in a composition in the previous example. We will now use it as a first currying example. The height of grown-ups is principally a constant. Okay, I know, we shrink every day from dawn to dusk about one centimetre and we get the loss returned over night. We define a function f which takes a height and returns a function whith one parameter (weight) to return the BMI:

def BMI_weight(height):
    def h(weight):
        return weight / height**2
    return h



for weight in [60, 68, 74]:
    for height in [164, 168, 172]:
        print(BMI_weight(height)(weight), BMI(weight, height))
0.00223081499107674 0.00223081499107674
0.0021258503401360546 0.0021258503401360546
0.0020281233098972417 0.0020281233098972417
0.002528256989886972 0.002528256989886972
0.002409297052154195 0.002409297052154195
0.002298539751216874 0.002298539751216874
0.002751338488994646 0.002751338488994646
0.0026218820861678006 0.0026218820861678006
0.002501352082206598 0.002501352082206598

Example: Currency Conversion

In the chapter on Magic Functions of our tutorial we had an excercise, in which we defined a class for currency conversions.

We will define now a function exchange, which takes three arguments:

  1. The source currency
  2. The target currency
  3. The amount in the source currency

To function needs the actual exchange rates. We can download them from finance.yahoo.com website with the function get_currencies. Though in our example we use some old exchange rates:

currencies =  {'CHF': 1.0821202355817312, 
               'CAD': 1.488609845538393, 
               'GBP': 0.8916546282920325, 
               'JPY': 114.38826536281809, 
               'EUR': 1.0, 
               'USD': 1.11123458162018}
def exchange(from_currency, to_currency, amount):
    result = amount * currencies[to_currency] 
    result /= currencies[from_currency]
    return result

exchange("CHF", "CAD", 100)
    
Output::
137.56418155678784

We can now define curried functions from the function exchange:

def exchange_from_CHF(to_currency, amount):
    return exchange("CHF", to_currency, amount)

def CHF2EUR(amount):
    return exchange_from_CHF("EUR", amount)

print(exchange_from_CHF("EUR", 90))
    
print(CHF2EUR(90))
83.17005545286507
83.17005545286507

We want to rewrite the function exchange in a curryable version:

def curry_exchange(from_currency=None, 
                   to_currency=None, 
                   amount=None):
    if from_currency:
        if to_currency:
            if amount:
                def f():
                    return exchange(from_currency, to_currency, amount)
            else:
                def f(amount):
                    return exchange(from_currency, to_currency, amount)
        else:
            if amount:
                def f(to_currency):
                    return exchange(from_currency, to_currency, amount)
            else:
                def f(to_currency=None, amount=None):
                    if amount:
                        if to_currency:
                            def h():
                                return exchange(from_currency, to_currency, amount)
                        else:
                            def h(to_currency):
                                if to_currency:
                                    return exchange(from_currency, to_currency, amount)
                    else:
                        if to_currency:
                            def h(amount):
                                return exchange(from_currency, to_currency, amount)
                        else:
                            def h(to_currency, amount):
                                return exchange(from_currency, to_currency, amount) 
                    return h
    else:
        def f(from_currency, to_currency, amount):
                return exchange(from_currency, to_currency, amount)
    return f
        

We can redefine exchange_from_CHF and CHF2EUR in a properly curried way:

exchange_from_CHF = curry_exchange("CHF")

print(exchange_from_CHF("EUR", 90))

CHF2EUR = curry_exchange("CHF", "EUR")

print(CHF2EUR(90))
<function curry_exchange.<locals>.f.<locals>.h at 0x7f7543673268>
83.17005545286507

You will find various calls to curry_exchange in the following examples:

print(curry_exchange("CHF")( "EUR", 100))
print(curry_exchange("CHF", "EUR")(100))
f = curry_exchange("CHF")
print(f("EUR", 100))
g = f("EUR")
print(g(100))

CHF2EUR= curry_exchange("CHF", "EUR")
print(CHF2EUR(100))

k = curry_exchange("CHF", "EUR", 100)
print(k())

print(curry_exchange("CHF", "EUR", 100))
f = curry_exchange("CHF")(amount=100)
print(f("EUR"))

f = curry_exchange("CHF")
print(f("EUR", 100))

f = curry_exchange("CHF")
g = f("EUR")
print(g(100))

g2 = f(amount=120)

for currency in currencies:
    print(currency, g2(currency))
<function curry_exchange.<locals>.f.<locals>.h at 0x7f7543438950>
92.41117272540563
<function curry_exchange.<locals>.f.<locals>.h at 0x7f754372a730>
92.41117272540563
92.41117272540563
92.41117272540563
<function curry_exchange.<locals>.f at 0x7f7543438840>
92.41117272540563
<function curry_exchange.<locals>.f.<locals>.h at 0x7f7543673268>
92.41117272540563
CHF 120.00000000000001
CAD 165.07701786814542
GBP 98.87861983980284
JPY 12684.9044978435
EUR 110.89340727048676
USD 123.22858903265559

So far we have written custom-made curry functions. We will define a general currying function in the following chapter of our tutorial.

General Currying

def arimean(*args):
    return sum(args) / len(args)

def curry(func):
    f_args = []
    f_kwargs = {}
    def f(*args, **kwargs):
        nonlocal f_args, f_kwargs
        if args or kwargs:
            f_args += args
            f_kwargs.update(kwargs)
            return f
        else:
            return func(*f_args, **f_kwargs)
    return f
            
s = curry(arimean)
s(2)(5)(9)(4, 5)
s(5, 9)
print(s())
s2 = curry(arimean)
s2(2)(500)(9)(4, 5)
s2(5, 9)
s2()

def add(x, y, z):
    return x + y + z

s2 = curry(add)
s2(3, 5)(8)()
5.571428571428571
Output::
16
def exchange(from_currency, to_currency, amount):
    result = amount * currencies[to_currency] 
    result /= currencies[from_currency]
    return result

e = curry(exchange)
print(e)
f = e("CHF", "EUR")
print(f(10)())

e2 = curry(exchange)
f = e2(to_currency="USD", amount=100, from_currency="CHF")
f()
#print(f(from_currency="CHF")())
<function curry.<locals>.f at 0x7f7543702bf8>
9.241117272540563
Output::
102.69049086054633
def arimean(*args):
    return sum(args) / len(args)

def curry(func):
    f_args = []
    f_kwargs = {}
    def f(*args, **kwargs):
        nonlocal f_args, f_kwargs
        if args or kwargs:
            f_args += args
            f_kwargs.update(kwargs)
            try:
                return func(*f_args, **f_kwargs)
            except TypeError:
                return f
        else:
            return func(*f_args, **f_kwargs)
    return f
            
s = curry(arimean)
s(2, 4, 5)

e = curry(exchange)
print(e)
f = e("CHF", "EUR")
print(f(10))



e2 = curry(exchange)
f = e2(to_currency="USD", amount=100)
f("CHF")
<function curry.<locals>.f at 0x7f7543457048>
9.241117272540563
Output::
102.69049086054633

partial-Function

The function partial from the module functools can be used to simulate currying as well. It can also be used to "freeze" some of the function's arguments. This means it simplifies the functions signature.

We use it now to curry our function exchange once more in a different way:

from functools import partial

f = partial(exchange, 
            to_currency="USD", 
            amount=100)

f("CHF")
f = partial(f, "CHF")
f()
Output::
102.69049086054633