Hello and welcome to C++ Weekly. I'm your host, Jason Turner. In this episode I want to tell you
a fun story about how my meetup got nerd sniped and the pretty awesome result of what happened.
So we had someone at the meetup who basically said that there's no great way to write a currying
function in C++, and the requirements went something like this: they want it to be kind
of like bind, but it only binds the first N parameters of the function, so it's more
like bind_front in the case that you pass it some number of parameters; and then it either
returns a new function with N parameters bound, or
executes the function. So the test case might look something like this:
So the first time you call the function
you are getting back a new function. You're saying, "I want to curry this
'add' function, and I want the first two parameters, x and y, to be bound to one and two;
and then I have a new function and the next time I call it I want to pass in three;" and now I expect
something like this: I expect that now 6 is going to be returned." This particular person said that
this is a complicated problem to solve in C++, and if you know me, I am a huge fan of lambdas,
so I said, you know, "Hold my beer. I will be right back" and came up with this as a
potential solution. In my version I called it "bind" not "curry" because I'm thinking about
bind first. There's a lot of ellipses to digest here, but we've got this thing that takes a
templated callable and a set of templated parameters (that's about what we expected
the interface to look like), and we create a new lambda function that takes a copy of that callable
and does a variadic capture of the parameters. I believe that was variadic capture added in C++20,
and generic lambda support which was C++14. Now I have a new function that has the set of parameters
that were passed to me and expects another set of parameters, and it returns 'callable';
but I have not actually done anything with this yet. I do an 'if constexpr', and I am using
this technique which I've covered in previous episodes that I got from Chris Jusiak, which he
got some version of from Andre Alexandrescu, but this requires C++20 Concepts and if constexp to
work in C++. I say if the callable thing that was passed to me is callable with the currently known
set of parameters, then I simply execute and return that value; if it is not then I return the
new bound thing, and this works - mostly. So I am binding the first parameter, and I am calling it
with the second and third parameters, and you can see that I have 6 being returned here from main. I
still have my Compiler Explorer set to ARM mode, so don't worry about it. It's r0 instead of EAX,
and if I try to pass too many parameters to it I'm going to get too many arguments to function
because no there's no way to call this thing with four parameters.
And the original test case, if I swap things around, it looks like this, and that works;
and if I pass all three parameters to it then I'm actually getting a value back here,
not a new function. So that works, but where mine falls down
(that works as well) where mine falls down is if I wanted to do something like this. I don't have any
way of cascading more than once, so then since, as I stated the meetup got nerd sniped, Ben Dean
actually pulls this version out, which I will paste in. Ben's is very succinct. Now
Ben has actually gone to the effort of naming his curry, and it works for this particular use case.
We can curry a function with no parameters, then add a parameter, and add a parameter,
and add a parameter and get the execution back; and the compiler has no problem with this and it
returns 6. Ben's relies on recursive instantiation of these variadic templates, and we will attempt
to walk through this. We call curry the first time we call it we pass it a function object - that is
the callable, and we are binding that callable in with zero additional parameters. This variadic set
is zero. Again, we're creating a lambda that can take any number of subsequent parameters.
Then it does effectively the same technique that I was looking at. If this thing is callable
with the original set of parameters followed by the next set of parameters,
then we call it and return its value. If it is not callable in this situation,
then we call curry again. Now we already know what the original callable was interestingly.
This is kind of fascinating. So it doesn't have to keep wrapping the callable over and over again,
it repackages it with the set of parameters that have been bound up to this point, plus the new
set of parameters which were passed in here. So now it is a callable back into this thing
with the original function, followed by the new set of parameters. So that set of parameters
is being copied multiple times, and that could be a problem depending on your specific
use case, but this works again, and it works for any set of parameters. It's
going to take any callable thing and it is going to call it if it can. Now we probably
actually want to use std::invoke here so that it's a little bit more generic of a solution,
but that's where we have it now. But it was then pointed out that this version
doesn't actually work in just, kind of, the standard case, which mine did work in.
If we do this and say we just want to call this function with these three parameters,
we get, well, a difficult to handle compiler error. That is certain because it doesn't
take into account the case that the very first invocation of this thing could return a result
instead of a function. So Ben updated his version, so now it looks like this. It has a test up front
that says - if constexpr this thing is callable with these parameters then simply return the
result of calling with those parameters, otherwise jump into the code that we have already dissected
where it knows how to return a new function in the case that it needs to, or it knows how to
just invoke the function in the case where it can. This is good, but we're still not quite there yet.
Vaughn Cato, who also comes to my meetup, pointed out that the inner 'if constexpr' is now unneeded,
so that's this case right here. We already have a pre-check
to see if it's callable with the set of parameters, we don't need the post check,
if you will. We don't need the inner check because the pre-check caught that scenario.
And so the final version becomes something like this, and I'm going to
go a little bit against what we got from clang-format. This version is, what is this? um...
10 lines of code long, and it works in every situation that we can throw at it,
and it kind of even works in situations where you don't necessarily expect it to work.
Like this - if we invoke it with an empty set of parameters it's, well, going to do exactly what
you think it should do. The function is not yet callable with that set of parameters. You passed
it no new parameters. It's effectively just going to return itself back to you, which is pretty cool
really. So it made for an awesome meetup, and I hope it made for a fun C++ Weekly video. I plan to
put some version of this in that tools repository that I have been sharing with you all maybe. In
the last few episodes and the last couple of months here I've mentioned it a few times.
It's not necessarily exactly perfect because it doesn't handle forwarding,
everything is copied. What do we do in that case? If we take things by reference and only store
references to them, then we have a problem with object lifetime and values going out of scope.
If we always take copies then we have a problem with making too many copies.
If we parameterize all of this to say, "Well if I was past an Rvalue then take a copy,
otherwise take a reference to it," it gets very complicated quickly. The copy only version
for simple types is, as you can see here, really often going to do what you want it to do;
and if you really feel like digging into this don't forget that you can always click on
"CppInsights" and you can walk through this to see what the compiler thinks of all of it,
which is an interesting experiment also. Have fun with this. Play with it. Hopefully I'll remember
to put some sort of a link to this implementation in the video description, and be sure
to attend your local C++ meetup because you really have no idea what's going to happen there.