A simpler and more reusable way to curry
The DRY principle
(i.e. do not repeat yourself) is a well practice principle in function programming, and tedious tasks like having to rewrite all our functions in a curried format would simply not be acceptable or practical. So we need to have a way to curry a function without having to change the way in which we specify it.
Curry helper methods to the rescue
The process of currying a method is essentially a repetitive task that can be simplified with a few helper static methods. To curry for example an existing binary integer function we need a perform the following conversion; expressed as
Hindley-Milner type signature:
C#:
curry :: ((int, int) -> int) -> (int -> int -> int)
Here's the same concept expressed as a generic function in C#:
C#:
public static class ƒ {
public static Func<A, Func<B, C>> curry<A, B, C>(this Func<A, B, C> func) {
return a => b => func(a, b);
}
}
The curry function is defined also using extension method syntax i.e. this keyword, which means we could use it both as a standard static function call, and as an extension method call, for example:
C#:
// Traditional Add function
static int Add(int a, int b) {
return a + b;
}
var curried_Add = ƒ.curry<int, int, int>(Add); // using static function call
// Lambda add function
Func<int, int, int> add = (a, b) => a + b;
var curried_add = add.curry(); // using extension method syntax
In the above examples; you should notice that in the second example we could curry a lambda function quite simply, by using the
.curry() extension method syntax, and the C# compiler was also able to infer the type signature for us.
In the first example, using the traditional static function syntax; we had to use the curry helper as a static function call, and we also had to provide the types because unfortunately the C# compiler today still can't automatically relate the syntax of a traditional function with its lambda equivalent; and for similar reasons we also could not use
curry as an extension method in this context.
In both cases the type for the
curried_Add and
curried_add variables is
Func<int, Func<int, int>>:
Note:
We can also create curry helper functions for the other arities that we would typically encounter in our codebase, for example:
C#:
// function ƒ synbol
// macOS: alt-f
// Windows: ctrl-alt f
public static class ƒ {
#region Currying
public static Func<A, Func<B, C>> curry<A, B, C>(this Func<A, B, C> func) {
return a => b => func(a, b);
}
public static Func<A, Func<B, Func<C, D>>> curry<A, B, C, D>(this Func<A, B, C, D> func) {
return a => b => c => func(a, b, c);
}
public static Func<A, Func<B, Func<C, Func<D, E>>>> curry<A, B, C, D, E>(this Func<A, B, C, D, E> func) {
return a => b => c => d => func(a, b, c, d);
}
// ... greater arities
#endregion
}
Currying: Why does it matter?
...more importantly why would we need this?
In mathematics and functional programming, one of the key precepts for functions, is the ability to create a new function that is the composition of other functions, for example:
C#:
var result = Math.Cos(Math.Sin(1.2)); // 0,596198168589484
result in the above example is a computation that is the combined result of the
Math.Sin(1.2) passed into
Math.Cos as it's input parameter. This is essentially function composition.
Now if we consider that the average function only a returns a single value; we should be able to acknowledge that composition is far easier to accomplish with functions when the input arity always is match to the output. i.e. one parameter in, and one result out.
For example, it is impossible to compose these two functions together:
C#:
int Add(int a, int b) {
return a + b;
}
int Multiply(int a, int b) {
return a * b;
}
Because both expect two input parameters, and only return a single output value, and like mismatched puzzle pieces; these are simply not composable.
So, why is composition so important?
Ok so before we explore how Currying can be used to overcome miss matched function arities; let's first reason about why composition is such an important concept, and why it's something we'd want to take advantage of in our code.
In the same way we tend to prefer a finished product as opposed to a bag of electronic bits that were used during its construction...
Composition is simply a way to build something that is more valuable than just the sum of its parts:
- it's a simple conceptual affordance that not only allows us to more easily embrace software engineering's DRY principle. i.e. we avoid unnecessary duplication
- ... but we also end up with a compositional wrapper that can help simplify the use of more complex, under the wrapper, computations.
Composition
We've touched on function composition a few of times in passing now, but what does it actually mean?
Wikipedia
In computer science, function composition (not to be confused with object composition) is an act or mechanism to combine simple functions to build more complicated ones. Like the usual composition of functions in mathematics, the result of each function is passed as the argument of the next, and the result of the last one is the result of the whole.
The most typical type of function composition is with two unary functions:
C#:
compose :: (a -> b) -> (b -> c) -> (a -> c)
To make function composition more easier to use, let's define a helper static extension method for this.
C#:
public static class ƒ {
public static Func<A, C> compose<A, B, C>(this Func<A, B> lhs, Func<B, C> rhs) {
return a => rhs(lhs(a));
}
}
Ok so let's have a look at the following two binary functions, how can we compose them when their input arities are incompatible with their output?
C#:
Func<int, int, int> add = (a, b) => a + b;
Func<int, int, int> multiply = (a, b) => a * b;
Composing these two functions will require us to fix at least one of the arguments of
add:
C#:
var add_multiply = add.curry()(2).compose(multiply.curry()); // 1st Add argument is 2
var result = add_multiply(4)(6); // 36
// which is the equivalent of :
var result2 = multiply(add(2, 4), 6); // 36
We achieve the fixing of only one of the arguments of
add by first currying it, then passing in our fixed argument value of
2, which leaves us with a unary function. We then compose this unary function with the curried version of
multiply; because
multiply is a binary function; expecting two arguments, but we only have one, so by currying
multiply; we are able to compose our unary function with the curried version of
multiply.
It's a bit of syntactic brain teaser, but it should hopefully start to give an idea of how currying allows us to manipulate the normal computation flow. More so, currying a function will allow us to exploit a number of techniques that derive from mathematics.
Note:
The concept of currying and composition is likely to be still a bit muddled at this point, but this should hopefully clear up later on in this thread
Partial Application
Partial application and currying are distinctly different techniques that happen to share some similarities with the type signature and implementation. The idea of partial application is that if you fix the first
n arguments of the function, you get back a function that only requires the remaining arguments; the partial applied arguments are essentially encapsulated in the "new" function in a style similar to dependency injection.
Similar to composition and currying, we can define a set of helper static methods to make use of partial function application easier, for example:
C#:
public static class ƒ {
public static Func<B, C> partial<A, B, C>(this Func<A, B, C> func, A a) {
return b => func(a, b);
}
}
Here's an example of using it to fix the 1st argument of the add function:
C#:
var partialAdd = add.partial(2);
var result1 = partialAdd(3); // 5
var result2 = partialAdd(4); // 6
partialAdd is a unary function formed by fixing the 1st argument of
add to the value of
2.
Note:
We can also create partial helper functions for the other arities that we would typically encounter in our code base, for example:
C#:
public static class ƒ {
#region Partial Application
public static Func<B, C> partial<A, B, C>(this Func<A, B, C> func, A a) {
return b => func(a, b);
}
public static Func<B, C, D> partial<A, B, C, D>(this Func<A, B, C, D> func, A a) {
return (b, c) => func(a, b, c);
}
public static Func<C, D> partial<A, B, C, D>(this Func<A, B, C, D> func, A a, B b) {
return c => func(a, b, c);
}
// ... greater arities
#endregion
}
...and that's it for this article. I've specifically avoided high order functions, because this post is already quite lengthy.
Next Post
The next post will focus on higher order functions, including the recreation of a few of C#'s
Linq extension methods, ...with a twist.