咖喱可以通过协程来实现吗?

Can currying be implemented via coroutines?

提问人:Enlico 提问时间:4/3/2023 更新时间:4/4/2023 访问量:144

问:

是否可以通过协程实现函数咖喱?你会怎么做?

通常,如果我需要咖喱函数,我会使用 boost::hana::curry就像这样,但我很好奇 C++20 的协程是否也可以执行相同的任务。

C++ 函数式编程 20 咖喱 C++协程

评论

5赞 nurettin 4/3/2023
协程提供并发性,currying 是一种函数式编程范式。你认为这两者为什么相关?
1赞 Enlico 4/3/2023
@nurettin,协程提供并发性?我没有看到用于定义生成器的协程中有任何并发性。当它们用于组合返回可选的操作时,我也没有看到并发性。顺便说一句,后者是典型的函数式编程。
1赞 nurettin 4/3/2023
是的,除了运行另一个函数之外,没有其他逻辑可以暂停执行一个函数。我不知道当你问如何用石头烤蛋糕时,你在想什么。但是,询问有关人际关系的问题背后必须有一个暗示或推理,否则我们只会得到垃圾。所以我想知道这是否是垃圾。
2赞 Caleth 4/3/2023
我认为答案是肯定的,但它看起来像 ,其中返回一个返回下一个提供的参数的等待者。我不确定这中间不需要一个普通的咖喱实现。curry_t<R(Args...)> curry(invocable<R, Args...> f) { f( co_yield... ); }curry_t::promise_type::yield_value
1赞 Caleth 4/3/2023
@Enlico是的,您必须将现有参数存储在某个地方。它还将仅限于对基础函数的一次调用,因为协程是单通道的

答:

1赞 Caleth 4/3/2023 #1

我要说的是,不,你不能合理地创建一个带有 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

评论

0赞 Nicol Bolas 4/3/2023
咖喱函数必须是可复制的吗?
0赞 Caleth 4/3/2023
@NicolBolas作为基线,可多次调用,不一定可复制。在我的示例中,我们没有复制f1
0赞 Nicol Bolas 4/3/2023
好的,但这是咖喱机制提供的必要设施吗?
0赞 Caleth 4/3/2023
@NicolBolas 这是一个如此巨大的陷阱,我会说是的,这是必要的。
0赞 Enlico 4/4/2023
我不认为仅仅为了向它传递连续参数的多个备选方案而编写部分应用函数的故事有多大价值。我的意思是,我可以写.毕竟,重复应该不是问题,因为无论如何都没有做任何实际工作,不是吗?也就是说,我也认为不能多次调用这些部分应用的函数(或本身)是一个大问题。auto f2 = curry(g)(10)(12); f3 = curry(g)(10)(25);curry(g)(10)f
2赞 Nicol Bolas 4/3/2023 #2

在 C++ 中咖喱或多或少需要看起来像这样。

有一个启动函数,它接受某种类型的可调用对象并返回一个 currying 对象:

auto curry = curry_func(some_function);

currying 对象有一个重载,该重载接受下一个参数并返回新的 currying 对象(从当前对象复制或从当前对象移动)或调用函数并返回其值。最后一部分需要一些体操(或一些同等的东西)。这意味着 currying 对象需要是某种模板,它至少知道它期望的参数数量。所以它不妨知道它们的类型。operator()if constexpr

所以真的,可能是.curry_funccurry_func<RetyrnType(Params...)>

那么咖喱对象是怎么回事呢?它需要某种方式来存储所有内容,以便最终可以调用咖喱调用。它需要按顺序存储它们,然后需要使用它们来调用可调用对象。此外,我们还会在每次调用时创建新的 currying 对象。Paramsoperator()

协程可以在 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_yieldcurried_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 的函数来生成参数值。eyield_valueco_awaitco_yieldawait_resumeco_yield e

接下来,当我们 时,我们需要告诉 promise 我们尝试访问哪个参数。我们需要以一种承诺可以返回不同类型的方式做到这一点(因为返回类型将决定如何解压缩它)。co_yieldco_await

这就是诀窍的重点。我们通过模板参数推导通过函数参数发送编译时常量。这允许返回 ,其中传递了索引。反过来,这允许根据 返回不同的值。此编译时索引将转发给 promise,以便它也可以返回正确的类型。integral_constantyield_valueawait_for_index<I>Iawait_for_index<I>::await_resumeI

那么,我们究竟该如何引导价值呢? 除了参数类型和返回值之外,它本身还有一个索引。这告诉我们我们正在谈论哪个参数索引。这个函子既不可复制,也不可移动(它可以可移动,但我会把它留给读者作为练习)。它的重载为下一个参数返回另一个参数或函数的实际返回值。curried_functionoperator()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_returncurried_function

请注意,它是声明的。这很重要,因为它实际上已经从新的咖喱功能中转移了过来。因此,如果您不将其称为 prvalue,则必须使用显式 .这使得很难多次调用它,并确保每个人都在视觉上理解您实际上是在移动,因此在此操作后不应触摸它。&&*thisstd::move*this

这也意味着在任何时候都只有一个对象具有有效的对象。coroutine_handle

请注意,制作此重载的多参数版本不会太困难。operator()

最后一部分是 promise 如何引导参数。它将其存储在内部。这样做是为了防止 promise 不必存储所有参数,并确保参数不必是默认可构造的。variant

请注意,我们不会在实际函数调用中解包,因为在 C++ 中,不能保证按顺序计算参数。我们需要在这里进行有序评估,因为协程和机制需要在当前检索的参数方面保持一致。因此,我们将它们解压缩在初始化 a 中的大括号初始化列表中。co_yieldstd::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;
}

评论

0赞 Caleth 4/3/2023
我不认为拥有咖喱组合器那么难