Jonathan Boccara's blog

Curried Objects in C++

Published May 3, 2019 - 0 Comments

Curried objects are like facilitators. They consist in intermediary objects between a caller and a callee, and helps them talk to each other in a smooth way. This ability makes the code simpler and easier to read.

While having seen and used the pattern at various places, the first time I encountered the actual term “Curried object” was in an article from James Noble, which clarified the bigger picture about those friendly little creatures.

An typical example of usage for curried objects is when outputting a line of strings separated by commas. If you’ve ever tried it, you probably encountered the obnoxious problem of the last word that should not be followed by a comma, and that forces us to write annoying bookkeeping code to check whether or not to print the bloody comma.

As we’ll see, curried object can relieve your code from those concerns. But this involves mutable curried objects, which we tackle in Part 2 of the series.

There are other uses for curried objects too, and for now we focus on constant curried objects.

Indeed, this series on curried objects contains:

  • Curried objects – Part 1: Constant curried objects
  • Curried objects – Part 2: Mutable curried objects
  • Curried objects – Part 3: Curried objects and the STL

We’ll begin with a simple example and gradually build more elaborate ones. Let’s get more into the details of those little beings that want to make our lives easier.

Constant curried objects

Curried objects are closely related to functions. In fact, the word “currying” essentially means partial application of a function.

What does that mean in practice?

Imagine that we have a function that takes several (or even too many) parameters, and that you need to call that function multiple times by making only a limited number parameters vary every time.

For instance, consider this function that draws a point at coordinates x and y, and z:

void drawAt(float x, float y, float z)
{
    std::cout << x << ',' << y << ',' << z << '\n';
}

For the sake of the example, this function only prints out the points coordinates. To simplify the graphics generation in the examples that follow, I will feed the program outputs into MS Excel and generate the associated chart.

Factorizing a common parameter

Let’s try out this function to draw each of the four cardinal points in the plane at z=0. We could write:

drawAt(1, 0, 0);
drawAt(0, 1, 0);
drawAt(-1, 0, 0);
drawAt(0, -1, 0);

But the last parameter doesn’t bring any information when reading code here. Indeed, we only work in a plane at z=0, so we think in terms of x and y only.

We can therefore partially apply drawPoint by fixing the last argument at 0, which would result into a function that only takes x and y as parameters. This is called currying, but in practice we can implement it with a familiar lambda:

auto drawInPlaneAt = [](float x, float y){ drawAt(x, y, 0); };

drawInPlaneAt(1, 0);
drawInPlaneAt(0, 1);
drawInPlaneAt(-1, 0);
drawInPlaneAt(0, -1);

No more third coordinate to read about here.

Here is the code outputs:

1,0,0
0,1,0
-1,0,0
0,-1,0

And the corresponding chart:

currying

Adapting parameters

Not convinced it’s worth it? Let’s see a slightly more complex example that does not only make a partial application, but also makes an adaptation of parameters (so strictly speaking, this is not only “currying” then).

We now want to draw a line of points identified by a slope and an y-intercept. We can refine our curried object to take a slope and a y-intercept and draw a point on this line, given an abscissa x:

#include <iostream>

void drawAt(float x, float y, float z)
{
    std::cout << x << ',' << y << ',' << z << '\n';
}

auto drawOnLine(float slope, float yIntercept)
{
    return [slope, yIntercept](float x) { drawAt(x, slope * x + yIntercept, 0); };
}

int main()
{
    auto drawOnMyLine = drawOnLine(0.5, 3);
    for (float x = -5; x < 6; x += 1)
    {
        drawOnMyLine(x);
    }
}

Note that this code uses C++14’s auto return type in order to write expressive code with lambdas, but the lambda could be written in C++11 without the intermediary function drawOnLine. Or even with a functor in C++98. Those are various ways of writing our curried objects, but the idea remains the same: it is an object that facilitates the dialogue between the caller (here, main()) and the callee (here drawAt).

Here is the generated output:

-5,0.5,0
-4,1,0
-3,1.5,0
-2,2,0
-1,2.5,0
0,3,0
1,3.5,0
2,4,0
3,4.5,0
4,5,0
5,5.5,0

And the corresponding graphic:

currying

Let’s now take a more elaborate example: let’s draw a circle!

We now have a drawInPlane method that takes an abscissa x and an ordinate y, and draws a point at that position. But those cartesian coordinates are just one way to identify a position in a plane.

Another representation of the plane is via polar coordinates: a distance r from an origin and an angle theta with the horizontal axis. To draw a circle for example, it is way easier to use polar coordinates than cartesian coordinates.

The curried object that we will create will adapt polar coordinates to cartesian coordinates with the following mathematical formulae:

curried object

curried object

Let’s now create our curried object that will take a succession of angles and draw a point on the circle for each of those angles:

auto drawOnCircle(float xCenter, float yCenter, float radius)
{
    return [xCenter, yCenter, radius](float angle)
    {
        const float xFromCenter = radius * std::sin(angle);
        const float yFromCenter = radius * std::cos(angle);
        drawInPlaneAt(xCenter + xFromCenter, yCenter + yFromCenter);
    };
}

Let’s now use the curried object to generate some points on the circle:

auto drawOnMyCircle = drawOnCircle(2, 1, 3);
for (float angle = -3.14; angle < 3.14; angle += 0.2)
{
    drawOnMyCircle(angle);
}

As a side note, you may have noticed this particular example is in dry need of strong typing, to be able to write something like that:

auto drawOnMyCircle = drawOnCircle(XCenter(2), YCenter(1), Radius(3));

But end of side note, let’s keep the focus on curried objects.

Here is the output of the program:

1.99522,-2,0
1.39931,-1.93925,0
0.827346,-1.76132,0
0.302131,-1.47331,0
-0.155395,-1.08669,0
-0.526992,-0.616884,0
-0.797845,-0.0826181,0
-0.957158,0.494808,0
-0.998578,1.09238,0
-0.920453,1.68626,0
-0.7259,2.25278,0
-0.422674,2.76936,0
-0.0228629,3.21541,0
0.457593,3.57313,0
0.99954,3.82826,0
1.58137,3.97065,0
2.17989,3.9946,0
2.77124,3.89917,0
3.33185,3.68816,0
3.83935,3.36998,0
4.27353,2.95731,0
4.61707,2.46662,0
4.85627,1.91745,0
4.98161,1.33171,0
4.98807,0.732742,0
4.87541,0.144431,0
4.64812,-0.40977,0
4.31526,-0.90777,0
3.89009,-1.32971,0
3.38957,-1.65878,0
2.83366,-1.88184,0
2.2445,-1.99002,0

And here is the corresponding graphic:

currying

Isn’t it too much indirection?

Let’s have a look at the code to generate those points, all put together:

#include <iostream>
#include <cmath>

void drawAt(float x, float y, float z)
{
    std::cout << x << ',' << y << ',' << z << '\n';
}

void drawInPlaneAt(float x, float y)
{
    drawAt(x, y, 0);
}

auto drawOnCircle(float xCenter, float yCenter, float radius)
{
    return [xCenter, yCenter, radius](float angle)
    {
        const float xFromCenter = radius * std::sin(angle);
        const float yFromCenter = radius * std::cos(angle);
        drawInPlaneAt(xCenter + xFromCenter, yCenter + yFromCenter);
    };
}

int main()
{
    auto drawOnMyCircle = drawOnCircle(2, 1, 3);
    for (float angle = -3.14; angle < 3.14; angle += 0.2)
    {
        drawOnMyCircle(angle);
    }
}

Now let’s compare it with an equivalent code, but that doesn’t use any curried object:

#include <iostream>
#include <cmath>

void drawAt(float x, float y, float z)
{
    std::cout << x << ',' << y << ',' << z << '\n';
}

int main()
{
    for (float angle = -3.14; angle < 3.14; angle += 0.2)
    {
        const float xFromCenter = 3 * std::sin(angle);
        const float yFromCenter = 3 * std::cos(angle);
        drawAt(2 + xFromCenter, 1 + yFromCenter, 0);
    }
}

The version with curried objects has more lines of code, and more indirections. Is it a good thing or a bad thing?

By itself, having more lines of code is not a good thing. But to decide if curried objects are worth this investment, let’s consider what they brought us:

  • more labels: if you had first seen the second version of the code above, the one without curried objects, would you have guessed it was drawing a circle? You probably would have, but after how much time? The version with curried objects has more code, but the extra lines carry information about the intent of the code. For this reason, I think they are useful.
  • more reuse: if we want to draw another circle, the function drawOnCircle is there to be reused. And if we have several circles to draw, the version with curried objects will end up having less lines of code. More importantly, this version removes some code duplication that the one without curried objects will have if we multiply the circles.

Now I’d be interested to hear you opinion of this. Are curried objects worth it in your opinion?

What is constant in Constant curried objects

You will notice that all those curried objects, that we have implemented as lambdas, have an operator() that is const (this is the default behaviour of lambdas). They all contain data, but this data is not modified by the application of the curried object.

What happens when the state of the curried object is modifiable? Does it bring any benefit?

It turns out that it does, and this is what we explore in Part 2 of the series on curried objects in C++.

Related articles:

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin