Jonathan Boccara's blog

Strong lambdas: strong typing over generic types

Published February 20, 2017 - 5 Comments

This post is a new one in the series about strong types. I didn’t intend the series to contain more than 3 posts initially, covering the topics of strong types to make interfaces clearer and more robust.

But I later encountered a need, still about strongly typing interfaces and that I shall describe in the motivation section, that made including the aspect of generic types in the series compelling. It may be like when you already have several kids and a new one unexpectedly enters the family but you love him just as much. In fact my wife and I don’t have kids as of this writing, so don’t trust me too much on the analogy.

I’m taking this opportunity to thank my wonderful wife Elisabeth for her encouragements on my projects, her precious advice, and for letting me invest the necessary time to come up with 2 posts a week without a complain. And for advising that I should check if I didn’t forget a semicolon when I loudly complain when my code doesn’t compile. I’m sure I’ll forget to put one some day 😉

Anyway, kids are fun but lambdas are pretty cool to play around with too. Let’s get into it.

For reference, here are the other post from the series:

Note that all the code for strong types is available on the GitHub project.

Motivation

In the article on higher-level algorithms on sets, we had the function set_aggregate that took itself two functions (or function objects) as arguments: one to compare two elements of the sets, and one to aggregate two elements of the sets together. A call to set_aggregate, by passing lambdas could look like this:

std::map<int, std::string> left = {{1, "a"}, {2, "b"}, {3, "c1"}};
std::map<int, std::string> right = {{3, "c2"}, {4, "d"}};

std::vector<std::pair<int, std::string>> results;

set_aggregate(left, right, std::back_inserter(results),
              [](auto const& p1, auto const& p2){ return p1.first < p2.first; },
              [](auto const& p1, auto const& p2){ return std::make_pair(p1.first, p1.second + p2.second); });

// results contains {{1, "a"}, {2, "b"}, {3, "c1c2"}, {4, "d"}} in unspecified order

The interface of set_aggregate can be improved: the above code makes it hard to understand what the lambdas are used for. Worse, if there were possible implicit conversion between their return types (typically with bool, char, int and so on) they could be swapped by mistake with the code still compiling, but really not doing what you intended it to do.

One solution would be to apply the technique shown in the post about Making code expressive with lambdas by taking the lambda out in another function. However this seems too much because the inside of the lambda have a level of abstraction that is quite close to the surrounding code. What would be better would be to name the lambdas, with a name strong enough that the compiler would recognize it and prevent compilation if they were passed in the wrong order.

This is how this post relates to strong types. As seen in Strong types for Strong interfaces, strong types allow to give a name to types in order to express both your intention to the human reader and to the compiler.

The technique presented in that post consisted in wrapping the passed type into a type with a specific name. The named type could be declared in just one line, the following way:

using Width = NamedType<double, struct WidthParameter>;

Instead of directly using a double we use the strong type Width that can be passed around in interfaces.

Here we would also like to use specific names such as Comparator and Aggregator to wrap the lambdas. But lambdas have unspecified types that are chosen by the compiler. So the above technique cannot be used in our case. What to do then?

A solution

Let’s take away all the sets machinery and reduce the problem the to the following:

template<typename Function1, typename Function2>
void set_aggregate(Function1 comparator, Function2 aggregator)
{
   std::cout << "Compare: " << comparator() << std::endl;
   std::cout << "Aggregate: " << aggregator() << std::endl;
}

int main()
{
   set_aggregate([](){ return "compare"; }, [](){ return "aggregate"; }); // OK
   set_aggregate([](){ return "aggregate"; }, [](){ return "compare"; }); // Compiles, but not what we want
}

The natural thing to do here would be to create a named type templated on the type it wraps, so that the compiler can fill it out itself with the type of the lambda. A possible implementation for the comparator is:

template<typename Function>
struct Comparator : NamedType<Function, Comparator<Function>>
{
    using NamedType<Function, Comparator<Function>>::NamedType;
};

If you haven’t read the post about strong types, it will explain everything about NamedType.

And since templated types can be deduced for functions but not for classes (more on this further down), we need a function that deduces the type of the lambda to construct a Comparator object:

template<typename Function>
Comparator<Function> comparator(Function const& func)
{
    return Comparator<Function>(func);
}

And the same thing can be done for the aggregator function:

template<typename Function>
struct Aggregator : NamedType<Function, Aggregator<Function>>
{
    using NamedType<Function, Aggregator<Function>>::NamedType;
};

With the helper function to build it:

template<typename Function>
Aggregator<Function> aggregator(Function const& value)
{
    return Aggregator<Function>(value);
}

This solves the problem by allowing to write the following code:

template<typename Function1, typename Function2>
void set_aggregate(Comparator<Function1> c, Aggregator<Function2> a)
{
   std::cout << "Compare: " << c.get()() << std::endl;
   std::cout << "Aggregate: " << a.get()() << std::endl;
}

int main()
{
   set_aggregate(comparator([](){ return "compare"; }), aggregator([](){ return "aggregate"; }));
}

This documents your code by tagging the lambdas with the purpose you want to give them, and also provides protection against passing the function parameters the wrong way around because comparator and aggregator return different types.

A generic solution

This is arguably quite a lot of work for just adding a tag on a lambda. Plus, the components implemented for comparator and aggregator look very similar. This code begs us not to stop here and to factor it. At first I didn’t see it but my colleague Kevin helped me realize that since NamedType is itself a class templated on the underlying type, we could just use a templated using declaration!

And then a strong generic type could be declared the following way:

template<typename Function>
using Comparator = NamedType<Function, struct ComparatorParam>;

or:

template<typename Function>
using Aggregator = NamedType<Function, struct AggregatorParam>;

Now we still need a function to deduce the type to pass to NamedType. After thinking about it I suggest this function be called make_named. I’m not sure it is the best we can do as a name so if you have a better proposition, by all means please let me know.

template<template<typename T> class GenericTypeName, typename T>
GenericTypeName<T> make_named(T const& value)
{
    return GenericTypeName<T>(value);
}

Finally, the client code can be written:

template<typename Function1, typename Function2>
void set_aggregate(Comparator<Function1> c, Aggregator<Function2> a)
{
    std::cout << "Compare: " << c.get()() << std::endl;
    std::cout << "Aggregate: " << a.get()() << std::endl;
}

int main()
{
    set_aggregate(make_named<Comparator>([](){ return "compare"; }), make_named<Aggregator>([](){ return "aggregate"; }));
}

And swapping the arguments would trigger a compile error.

Note that in C++17, we could have thought that the template argument deduction for class template constructors to let us write the named generic type without the need for a helper function, making make_named obsolete anyway:

template<typename Function1, typename Function2>
void set_aggregate(Comparator<Function1> c, Aggregator<Function2> a)
{
    std::cout << "Compare: " << c.get()() << std::endl;
    std::cout << "Aggregate: " << a.get()() << std::endl;
}

int main()
{
    set_aggregate(Comparator([](){ return "compare"; }), Aggregator([](){ return "aggregate"; }));
}

But, as observed by Guillaume in the comments section, this doesn’t work for alias templates such as Comparator which aliases NamedType. We can still hope this will be possible in C++20.

Conclusion

This generalisation of strong types to generic types allows to tag generic types or unknown types like lambdas with a meaningful name. This can make your code more robust and more expressive, letting both humans and compilers know more about your intentions.

 

This can be used with the following syntax, symmetric with the one we used on strong types previously:

template<typename Function>
using Comparator = NamedType<Function, struct ComparatorParam>;

And a helper function is necessary to construct the named generic types:

template<template<typename T> class GenericTypeName, typename T>
GenericTypeName<T> make_named(T const& value)
{
    return GenericTypeName<T>(value);
}

If you want to see more about the implementation of strong types or play around with it you can have a look at the GitHub project.

Related articles:

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

Comments are closed