Sprinkle some curry please

Sprinkle some curry please

We are not going to transform code into curry but rather try to curry some code. As a former C# developer, when I started Haskell, I felt like functions were very differents from what I was used to see in object oriented languages. I just forgot maths:

$$
f : \mathbb{Z} \to \mathbb{Z} \to \mathbb{Z} \to \mathbb{Z}
\\
f(x, y, z) = x + y + z
$$

public static int f(int x, int y, int z) => x + y + z;
f :: Int -> Int -> Int -> Int
f x y z = x + y + z

Everyone was used to see the mathematical definition at school. Why do we have such a verbose and ugly notation is OOP? I don't have the answer but I believe that it is far easier to read the Haskell definition.

In opposition, the C# side is mixing both the function signature and its implementation, this is a reason why it is harder to read (note that we are using the version 7 of C#, it would be worse otherwise).

Now let's think a little bit about $f$. The function is telling us:

Give me $x$, $y$, and $z$. I will give you $x + y + z$.

We can rephrase this sentence:

Give me $x$ and then $y$ and finally $z$. I will give you $x + y + z$

This is very important. Providing the three variables at the same time is not mandatory. What I mean is that we can first fix $x$, then fix $y$ and fix $z$ later. Let's welcome currying:

$$
f : x \mapsto y \mapsto z \mapsto x + y + z
$$

This mathematical definition is pretty much straight forward. The function $f$ is a map of map of map, it's possible to fix the variables step by step by step:

$$
\begin{eqnarray*}
g &=& f(1)
\\
g &:& y \mapsto z \mapsto 1 + y + z
\\
\\
h &=& g(2)
\\
h &:& z \mapsto 1 + 2 + z
\\
\\
i &=& h(3)
\\
&=& 6
\\
&=& h(3)
\\
&=& g(2)(3)
\\
&=& f(1)(2)(3)
\end{eqnarray*}
$$

What we want to understand here is that we can treat every function as a function of a single argument:

$$f(1, 2, 3) = f(1)(2)(3)$$

Instead of providing every parameters at once, we fix them one at a time. This give us more flexibility. Now remove the useless commas and parenthesis:

$$f\enspace1\enspace2\enspace3 = f\enspace1\enspace2\enspace3$$

Wow, go back to our Haskell definition:

f :: Int -> Int -> Int -> Int
f x y z = x + y + z

You can partially apply functions in Haskell, every function is a function of a single argument. What you don't see here is the implicit right associative arrow Int -> (Int -> (Int -> Int)). We can read it as a function of function of function.

Applying a parameter to a function is just a matter of space f a. With the application being left associative f a b = (f a) b:

f :: Int -> Int -> Int -> Int
f x y z = x + y + z

k :: Int -> Int
k  = f 1 2

Having this kind of behaviour in C# is pretty hard as we have to write a lot of boilerplate code to reach the same level of abstraction:

public static int f(int x, int y, int z) 
    => x + y + z;

public static Func<int, Func<int, Func<int, int>>> curriedf =
       x
    => y
    => z
    => x + y + z;

Unfortunately, the code is ugly and unreadable, notice how it is hard to understand the nested Func. Perhaps you ask yourself whether this concept is useful or not. Imagine that you want to filter a list of persons by their age:

public static bool OlderThan30(Person person) => person.Age > 30;

// call site
var persons = new List<Person>(...);
var result = persons.Where(OlderThan30);
olderThan30 :: Person -> Bool
olderThan30 person = age person > 30

-- call site
persons = [...]
result = filter olderThan30 persons

Now, you realize that you also want to filter people older than 50. You try to abstract the function by accepting the lower bound as a parameter, but you are stuck to write:

public static bool OlderThan(int lowerBound, Person person) 
    => person.Age > lowerBound;

// call site
var persons = new List<Person>(...);
/* Impossible to write this even if it mathematically equivalent   
    var result = persons.Where(OlderThan(50));
*/
Func<Person, bool> olderThan50 = person => OlderThan(50, person);
var result = persons.Where(olderThan50);
olderThan :: Int -> Person -> Bool
olderThan lowerBound person = age person > lowerBound

-- call site
persons = [...]
result = filter (olderThan 50) persons

Regrettably, the Where function is accepting a function of a single argument, but we are not able to partially apply OlderThan in C#. We are forced to have an intermediate function (olderThan50) to give all the parameters at once...

Moreover, the compiler is not able to infer Func<Person, bool> as we can't replace it by var.

Thanks for reading.


https://en.wikipedia.org/wiki/Currying
https://wiki.haskell.org/Currying

Show Comments