提问人:Enlico 提问时间:4/3/2023 更新时间:4/4/2023 访问量:144
咖喱可以通过协程来实现吗?
Can currying be implemented via coroutines?
答:
我要说的是,不,你不能合理地创建一个带有 C++ corotine 的机制,因为 corotine 状态是不可复制的,即以下代码无法工作curry
int g(int x, int y, int z) { return x + y + z; }
auto f = curry(g);
auto f1 = f(10);
auto f2 = f1(12);
auto f3 = f1(25); // error, can't re-use f1
评论
f1
auto f2 = curry(g)(10)(12); f3 = curry(g)(10)(25);
curry(g)(10)
f
在 C++ 中咖喱或多或少需要看起来像这样。
有一个启动函数,它接受某种类型的可调用对象并返回一个 currying 对象:
auto curry = curry_func(some_function);
currying 对象有一个重载,该重载接受下一个参数并返回新的 currying 对象(从当前对象复制或从当前对象移动)或调用函数并返回其值。最后一部分需要一些体操(或一些同等的东西)。这意味着 currying 对象需要是某种模板,它至少知道它期望的参数数量。所以它不妨知道它们的类型。operator()
if constexpr
所以真的,可能是.curry_func
curry_func<RetyrnType(Params...)>
那么咖喱对象是怎么回事呢?它需要某种方式来存储所有内容,以便最终可以调用咖喱调用。它需要按顺序存储它们,然后需要使用它们来调用可调用对象。此外,我们还会在每次调用时创建新的 currying 对象。Params
operator()
协程可以在 currying 实现中使用吗?是的,但并非没有重要的警告。
每个 curry 函数对象(您使用参数调用的东西)只能调用一次。协程状态的元素不能被复制,所以如果一个咖喱函数可以被复制,它必须是一个浅拷贝。
但也有一些好处。大多数 C++ curring 实现都是元编程废话的可怕噩梦。即使在如何将参数应用于函数调用的最高级别,代码也会变得非常丑陋。
相比之下,协程咖喱实现在应用参数的地方非常可读。例如,这是我将要介绍的代码中的样子:
template<std::size_t Sz>
using int_const = std::integral_constant<std::size_t, Sz>;
template<typename Ret, typename ...Params, std::size_t ...Ixs>
curried_function<0, Ret, Params...> curry_func_detail(Ret(*some_func)(Params...), std::index_sequence<Ixs...>)
{
std::tuple args{ (co_yield int_const<Ixs>{})...};
co_return std::apply(some_func, args);
}
template<typename Ret, typename ...Params>
curried_function<0, Ret, Params...> curry_func(Ret(*some_func)(Params...))
{
return curry_func_detail(some_func, std::make_index_sequence<sizeof...(Params)>{});
}
curry_func_detail
非常简单:一个解压缩的整数常量序列。所有的复杂性都隐藏在它所伴随的类型中。co_yield
curried_function
这意味着您可以相对容易地使用完全相同的协程机制(及其类似机制)来开始玩不同的游戏。例如,这是将两个不同的函数纳入其中所需要的:curried_function
template<std::size_t Sz>
using int_const = std::integral_constant<std::size_t, Sz>;
template<typename Ret1, typename ...Params1, std::size_t ...Ixs1, typename Ret2, typename ...Params2, std::size_t ...Ixs2>
curried_function<0, std::tuple<Ret1, Ret2>, Params1..., Params2...> double_curry_func_detail(
Ret1(*func1)(Params1...), , std::index_sequence<Ixs1...>,
Ret2(*func2)(Params2...), std::index_sequence<Ixs2...>)
{
std::tuple args1{ (co_yield int_const<Ixs1 + 0>{})...};
auto ret1 = std::apply(func1, args);
std::tuple args2{ (co_yield int_const<Ixs2 + sizeof...(Params1)>{})...};
auto ret2 = std::apply(func2, args);
co_return std::tuple{ret1, ret2};
}
template<typename Ret1, typename ...Params1, typename Ret2, typename ...Params2>
auto double_curry_func(
Ret1(*func1)(Params1...), Ret2(*func2)(Params2...))
{
return double_curry_func_detail(func1, std::make_index_sequence<sizeof...(Params1)>{},
func2, std::make_index_sequence<sizeof...(Params2)>{});
}
相比之下,采用现有的咖喱系统并让它做一些相对简单的事情......难。我们甚至可以在将参数传递给 cured 函数之前开始做一些事情,例如转换参数或其他东西。
总的一点是,累积值并调用函数的代码不同于将值从调用方引导到累积代码的代码。前者是以一种相当直接的方式编写的。
这确实是使用协程的唯一优势。
那么,在实施方面,这里到底发生了什么?这是一个工作示例,让我们逐步完成它。
co_yield e
是一种在协程函数和外界之间传递值的机制,使用 promise 对象作为中介。 通过调用 来赋予 Promise。但是该调用的返回值是可以等待的。这不仅允许我们暂停每个协程,还允许我们使用 awaitable 的函数来生成参数值。e
yield_value
co_await
co_yield
await_resume
co_yield e
接下来,当我们 时,我们需要告诉 promise 我们尝试访问哪个参数。我们需要以一种承诺可以返回不同类型的方式做到这一点(因为返回类型将决定如何解压缩它)。co_yield
co_await
这就是诀窍的重点。我们通过模板参数推导通过函数参数发送编译时常量。这允许返回 ,其中传递了索引。反过来,这允许根据 返回不同的值。此编译时索引将转发给 promise,以便它也可以返回正确的类型。integral_constant
yield_value
await_for_index<I>
I
await_for_index<I>::await_resume
I
那么,我们究竟该如何引导价值呢? 除了参数类型和返回值之外,它本身还有一个索引。这告诉我们我们正在谈论哪个参数索引。这个函子既不可复制,也不可移动(它可以可移动,但我会把它留给读者作为练习)。它的重载为下一个参数返回另一个参数或函数的实际返回值。curried_function
operator()
curried_function
auto operator()(parameter_t<ParamIndex, Params...> param) &&
{
promise_type &promise = hdl_.promise();
promise.template set_param_value<ParamIndex>(param);
hdl_.resume();
if constexpr(ParamIndex + 1 == sizeof...(Params))
{
return promise.get_return_value();
}
else
{
return curried_function<ParamIndex + 1, Ret, Params...>(std::move(*this));
}
}
该函数为 promise 提供适当的参数,然后恢复协程。如果这是最后一个参数,则它假定协程 ed 并返回返回值。否则,它将返回一个为下一个参数提供服务的新参数。co_return
curried_function
请注意,它是声明的。这很重要,因为它实际上已经从新的咖喱功能中转移了过来。因此,如果您不将其称为 prvalue,则必须使用显式 .这使得很难多次调用它,并确保每个人都在视觉上理解您实际上是在移动,因此在此操作后不应触摸它。&&
*this
std::move
*this
这也意味着在任何时候都只有一个对象具有有效的对象。coroutine_handle
请注意,制作此重载的多参数版本不会太困难。operator()
最后一部分是 promise 如何引导参数。它将其存储在内部。这样做是为了防止 promise 不必存储所有参数,并确保参数不必是默认可构造的。variant
请注意,我们不会在实际函数调用中解包,因为在 C++ 中,不能保证按顺序计算参数。我们需要在这里进行有序评估,因为协程和机制需要在当前检索的参数方面保持一致。因此,我们将它们解压缩在初始化 a 中的大括号初始化列表中。co_yield
std::tuple
所以这是完整的代码:
#include <iostream>
#include <utility>
#include <coroutine>
#include <tuple>
#include <variant>
#include <optional>
template<std::size_t ParamIndex, typename ...Params>
using parameter_t = std::tuple_element_t<ParamIndex, std::tuple<Params...>>;
template<std::size_t ParamIndex, typename Ret, typename ...Params>
class curried_function;
template<typename Ret, typename ...Params>
class curry_promise
{
public:
curry_promise() = default;
template<std::size_t Ix>
void set_param_value(parameter_t<Ix, Params...> param)
{
curr_param_.template emplace<Ix + 1>(param);
}
template<std::size_t Ix>
parameter_t<Ix, Params...> get_curr_param()
{
return std::get<Ix+1>(curr_param_);
}
template<std::size_t I>
auto yield_value(std::integral_constant<std::size_t, I>)
{
return await_for_index<I>(*this);
}
auto initial_suspend() noexcept {return std::suspend_never{};}
auto final_suspend() noexcept {return std::suspend_always{};}
curried_function<0, Ret, Params...> get_return_object();
void unhandled_exception() { std::terminate(); }
void return_value(Ret ret)
{
ret_.emplace(ret);
}
Ret get_return_value() const
{
return *ret_;
}
template<std::size_t Ix>
class await_for_index
{
public:
await_for_index(curry_promise &promise) : promise_{promise} {}
bool await_ready() noexcept {return false;}
void await_suspend(std::coroutine_handle<curry_promise> h) noexcept {}
parameter_t<Ix, Params...> await_resume() noexcept
{
return promise_.template get_curr_param<Ix>();
}
private:
curry_promise &promise_;
};
private:
std::variant<std::monostate, Params...> curr_param_;
std::optional<Ret> ret_;
};
template<std::size_t ParamIndex, typename Ret, typename ...Params>
class curried_function
{
public:
using promise_type = curry_promise<Ret, Params...>;
auto operator()(parameter_t<ParamIndex, Params...> param) &&
{
promise_type &promise = hdl_.promise();
promise.template set_param_value<ParamIndex>(param);
hdl_.resume();
if constexpr(ParamIndex + 1 == sizeof...(Params))
{
return promise.get_return_value();
}
else
{
return curried_function<ParamIndex + 1, Ret, Params...>(std::move(*this));
}
}
//Non-copyable, non-moveable.
curried_function(curried_function const&) = delete;
~curried_function() { hdl_.destroy(); }
template<std::size_t Sz, typename Ret2, typename ...Params2>
friend class curried_function;
friend class curry_promise<Ret, Params...>;
private:
template<std::size_t OtherIndex>
curried_function(curried_function<OtherIndex, Ret, Params...> &&prev_func)
: hdl_{std::exchange(prev_func.hdl_, nullptr)}
{
}
curried_function(std::coroutine_handle<promise_type> hdl)
: hdl_(hdl) {}
private:
std::coroutine_handle<promise_type> hdl_;
};
template<typename Ret, typename ...Params>
curried_function<0, Ret, Params...> curry_promise<Ret, Params...>::get_return_object()
{
return curried_function<0, Ret, Params...>(std::coroutine_handle<curry_promise>::from_promise(*this));
}
template<std::size_t Sz>
using int_const = std::integral_constant<std::size_t, Sz>;
template<typename Ret, typename ...Params, std::size_t ...Ixs>
curried_function<0, Ret, Params...> curry_func_detail(Ret(*some_func)(Params...), std::index_sequence<Ixs...>)
{
std::tuple args
{ (co_yield int_const<Ixs>{})...};
co_return std::apply(some_func, args);
}
template<typename Ret, typename ...Params>
curried_function<0, Ret, Params...> curry_func(Ret(*some_func)(Params...))
{
return curry_func_detail(some_func, std::make_index_sequence<sizeof...(Params)>{});
}
float test(int a, float b, int c)
{
return a + b + c;
}
void co_test()
{
}
int main()
{
std::cout << test(1, 2.3, 3) << std::endl;
std::cout << curry_func(test)(1)(2.3)(3) << std::endl;
return 0;
}
评论
curry_t<R(Args...)> curry(invocable<R, Args...> f) { f( co_yield... ); }
curry_t::promise_type::yield_value