C++中的函数式编程.实施f(a)(b)(c)

Gig*_*xel 63 c++ functional-programming currying c++11 std-function

我已经开始使用C++进行函数式编程的基础知识.我正在尝试创建一个f(a)(b)(c)将返回的函数a + b + c.我成功实现了f(a)(b)返回a + b 的函数.这是它的代码:

std::function<double(double)> plus2(double a){
    return[a](double b){return a + b; };
}
Run Code Online (Sandbox Code Playgroud)

我只是无法弄清楚如何实现f(a)(b)(c)我之前声明应该返回的功能a + b + c.

Jon*_*nas 114

你可以通过让函数f返回一个仿函数,即一个实现的对象来实现operator().这是一种方法:

struct sum 
{
    double val;

    sum(double a) : val(a) {}

    sum operator()(double a) { return val + a; }

    operator double() const { return val; }
};

sum f(double a)
{
    return a;
}
Run Code Online (Sandbox Code Playgroud)

链接

int main()
{
    std::cout << f(1)(2)(3)(4) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

模板版本

您甚至可以编写一个模板化版本,让编译器推断出类型.在这里试试吧.

template <class T>
struct sum 
{
    T val;

    sum(T a) : val(a) {}

    template <class T2>
    auto operator()(T2 a) -> sum<decltype(val + a)> { return val + a; }

    operator T() const { return val; }
};

template <class T>
sum<T> f(T a)
{
    return a;
}
Run Code Online (Sandbox Code Playgroud)

在这个例子中,T最终会解决double:

std::cout << f(1)(2.5)(3.1f)(4) << std::endl;
Run Code Online (Sandbox Code Playgroud)

  • @PanagiotisKanavos你在谈论什么......标准算法需要函数或函数对象.lambda表达式是函数对象*的*语法糖.没有任何问题,因为它完全相同,甚至在标准中也是这样定义的.此外,`std :: function`与讨论相切 - 即类型擦除. (46认同)
  • @PanagiotisKanavos:首先,您应该意识到在C++中,lambda表达式只是一种替代语法来定义一个重载`operator()`的类.使用"普通"语法来创建这样的类(尤其是对于lambda表达式语法不能很好地工作的情况),世界上没有任何错误. (26认同)
  • @PanagiotisKanavos:lambda的问题在于它们是匿名的.但正如你在这里看到的那样,`sum :: operator()`返回`sum`.缺乏名称,lambda的这种自我引用是不可能的. (16认同)
  • @PanagiotisKanavos:通过这个论点,你已经排除了lambdas,它们也是类似函数的类,而不是实际的函数. (15认同)
  • @PanagiotisKanavos:请注意,不同的语言对_function_的确切构成有不同的定义.C++已经从C继承了_function_的定义,因此它需要一个更广泛概念的新术语.名义差异并不意味着C++中的_callable objects_在功能语言中与_functions_在结构上不同.如果它像鸭子一样走路,像鸭子一样说话,它就是一只鸭子. (14认同)
  • @Logman:正如你所看到的,`sum(a)(b)(c)`的评估状态在每个`sum`中被完全捕获.既没有地方也没有全球国家. (9认同)
  • @PanagiotisKanavos该死的...通过这种逻辑,任何编程语言都没有区别,它们都被"转换"为二进制代码: - / (8认同)
  • @Gigaxel回答你喜欢的不实用,你不想手动编写lambdas的所有组合用于链调用. (7认同)
  • @PanagiotisKanavos:大多数普通的C库/对象_are_ OO.相同的概念适用.不是所有C,而是大多数大型C对象.你有一些内部变量数据,一个构造函数,一些"方法"函数和一个析构函数.组成是微不足道的.继承是不平凡的,但仍然很容易实现.`shape*s = makeSquare(3); int area = s-> area(s); S-> freeShape(一个或多个);` (7认同)
  • 注意,这里的整个类也可以是函数的本地.所以不用担心污染全局命名空间. (6认同)
  • @Gigaxel如果您不喜欢它,它是比您选择的答案更好的答案 (6认同)
  • lambdas和`std :: function`的重点在于你*不需要创建一个只是为了实现`()`的类.您可以创建实际的功能并传递它们.使用函数对象不被认为是很好的C++实践.我没有downvote但是*如果你试图将它用于迭代或任何其他需要lambda的STL方法,那么这个*类会引起问题 (4认同)
  • @TomSwirly:在功能上下文中,这些是相同的 (2认同)

Uri*_*iel 57

只需使用2个元素解决方案并将其展开,然后用另一个lambda包装即可.

因为你想返回一个得到a的lambda double并返回一个doubles'加法lambda,你需要做的就是用另一个函数包装你当前的返回类型,并将一个嵌套的lambda添加到你当前的lambda中(一个返回lambda的lambda) ):

std::function<std::function<double(double)>(double)> plus3 (double a){
    return [a] (double b) {
        return [a, b] (double c) {
            return a + b + c;
        };
    };
}
Run Code Online (Sandbox Code Playgroud)
  • 正如@Ðan指出的那样,你可以跳过std::function<std::function<double(double)>(double)>并相处auto:

    auto plus3 (double a){
        return [a] (double b) {
            return [a, b] (double c) { return a + b + c; };
        };
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 您可以使用更深层次的嵌套lambda为每个元素扩展此结构.演示4个元素:

    auto plus4 (double a){
        return [a] (double b) {
            return [a, b] (double c) {
                return [a, b, c] (double d) {
                    return a + b + c + d;
                };
            };
        };
    }
    
    Run Code Online (Sandbox Code Playgroud)

  • 很好,得到OP想要的东西,但它不能很好地扩展. (11认同)
  • @Jonas:通常的解决方案是非类型模板参数,特别是用'plus <N-1>`来定义`plus <N>`.缺点很明显; 你还需要预先指定确切的参数数量. (8认同)
  • 这正是我试图实现它的方式,但我没有包括std :: function <double(double)>(double).谢谢你,它让很多东西更加清晰,现在我现在如何更深入地嵌套它(尽管我不想更进一步). (4认同)
  • 很好,是否有可能推广到例如更少和更多的元素? (3认同)
  • @Uriel我知道嵌套更多lambda的可能性.如果不定义像`plus2`和`plus4`这样的新函数,你将如何实现呢? (3认同)

vso*_*tco 30

这是一种稍微不同的方法,它返回*thisfrom 的引用operator(),因此您没有任何副本浮动.这是一个非常简单的仿函数实现,它以递归方式存储状态和左折叠:

#include <iostream>

template<typename T>
class Sum
{
    T x_{};
public:
    Sum& operator()(T x)
    {
        x_ += x;
        return *this;
    }
    operator T() const
    {
        return x_;
    }
};

int main()
{
    Sum<int> s;
    std::cout << s(1)(2)(3);
}
Run Code Online (Sandbox Code Playgroud)

Live on Coliru

  • 这适用于任何数量的`(summand)`并且是递归的,即不需要为每个级别定义另一个函数/ lambda. (4认同)

Bar*_*rry 15

这不是f(a)(b)(c)而是curry(f)(a)(b)(c).我们进行换行f,使得每个附加参数都返回另一个curry或者实际上急切地调用该函数.这是C++ 17,但可以在C++ 11中实现,并带来一些额外的工作.

请注意,这是一个用于调整函数的解决方案 - 这是我从问题中得到的印象 - 而不是折叠二元函数的解决方案.

template <class F>
auto curry(F f) {
    return [f](auto... args) -> decltype(auto) {
        if constexpr(std::is_invocable<F&, decltype(args)...>{}) {
            return std::invoke(f, args...);
        }
        else {
            return curry([=](auto... new_args)
                    -> decltype(std::invoke(f, args..., new_args...))
                {
                    return std::invoke(f, args..., new_args...);
                });
        }
    };  
}
Run Code Online (Sandbox Code Playgroud)

为简洁起见,我跳过了转发参考.示例用法是:

int add(int a, int b, int c) { return a+b+c; }

curry(add)(1,2,2);       // 5
curry(add)(1)(2)(2);     // also 5
curry(add)(1, 2)(2);     // still the 5th
curry(add)()()(1,2,2);   // FIVE

auto f = curry(add)(1,2);
f(2);                    // i plead the 5th
Run Code Online (Sandbox Code Playgroud)

  • C++ 17使它变得如此整洁.在C++ 14中编写我的函数curry要痛苦得多.请注意,为了提高效率,您可能需要一个函数对象,它根据调用上下文有条件地移动其状态元组.这可能需要从lambda捕获列表中解耦`tup`和'f`,因此可以使用不同的rvalueness传入它,具体取决于调用`()`的方式.然而,这样的解决方案超出了这个问题的范围. (4认同)

Jus*_*ica 12

我能想到的最简单的方法是定义plus3()来讲plus2().

std::function<double(double)> plus2(double a){
    return[a](double b){return a + b; };
}

auto plus3(double a) {
    return [a](double b){ return plus2(a + b); };
}
Run Code Online (Sandbox Code Playgroud)

这将前两个参数列表组合成一个arglist,用于调用plus2().这样做可以让我们以最少的重复次数重用我们已有的代码,并且可以在将来轻松扩展; plusN()只需要返回一个调用的lambda plusN-1(),它会将调用依次传递给前一个函数,直到它到达为止plus2().它可以像这样使用:

int main() {
    std::cout << plus2(1)(2)    << ' '
              << plus3(1)(2)(3) << '\n';
}
// Output: 3 6
Run Code Online (Sandbox Code Playgroud)

考虑到我们只是在线调用,我们可以很容易地将其转换为函数模板,从而无需为其他参数创建版本.

template<int N>
auto plus(double a);

template<int N>
auto plus(double a) {
    return [a](double b){ return plus<N - 1>(a + b); };
}

template<>
auto plus<1>(double a) {
    return a;
}

int main() {
    std::cout << plus<2>(1)(2)          << ' '
              << plus<3>(1)(2)(3)       << ' '
              << plus<4>(1)(2)(3)(4)    << ' '
              << plus<5>(1)(2)(3)(4)(5) << '\n';
}
// Output: 3 6 10 15
Run Code Online (Sandbox Code Playgroud)

这里看到两个.

  • 这很棒!它整齐地扩展,实现很简单. (2认同)

Yak*_*ont 11

我要去玩了.

你想做一个curried折叠添加.我们可以解决这个问题,或者我们可以解决一类包含这个问题的问题.

所以,首先,补充:

auto add = [](auto lhs, auto rhs){ return std::move(lhs)+std::move(rhs); };
Run Code Online (Sandbox Code Playgroud)

这很好地表达了添加的概念.

现在,折叠:

template<class F, class T>
struct folder_t {
  F f;
  T t;
  folder_t( F fin, T tin ):
    f(std::move(fin)),
    t(std::move(tin))
  {}
  template<class Lhs, class Rhs>
  folder_t( F fin, Lhs&&lhs, Rhs&&rhs):
    f(std::move(fin)),
    t(
      f(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs))
    )
  {}
  template<class U>
  folder_t<F, std::result_of_t<F&(T, U)>> operator()( U&& u )&&{
    return {std::move(f), std::move(t), std::forward<U>(u)};
  }
  template<class U>
  folder_t<F, std::result_of_t<F&(T const&, U)>> operator()( U&& u )const&{
    return {f, t, std::forward<U>(u)};
  }
  operator T()&&{
    return std::move(t);
  }
  operator T() const&{
    return t;
  }
};
Run Code Online (Sandbox Code Playgroud)

它需要种子值和T,然后允许链接.

template<class F, class T>
folder_t<F, T> folder( F fin, T tin ) {
  return {std::move(fin), std::move(tin)};
}
Run Code Online (Sandbox Code Playgroud)

现在我们连接它们.

auto adder = folder(add, 0);
std::cout << adder(2)(3)(4) << "\n";
Run Code Online (Sandbox Code Playgroud)

我们还可以folder用于其他操作:

auto append = [](auto vec, auto element){
  vec.push_back(std::move(element));
  return vec;
};
Run Code Online (Sandbox Code Playgroud)

使用:

auto appender = folder(append, std::vector<int>{});
for (int x : appender(1)(2)(3).get())
    std::cout << x << "\n";
Run Code Online (Sandbox Code Playgroud)

实例.

我们必须在.get()这里调用因为for(:)循环不理解我们的文件夹operator T().我们可以通过一些工作来解决这个问题,但.get()更容易.


Jus*_*tin 10

如果您愿意使用库,那么在Boost的Hana中这很容易:

double plus4_impl(double a, double b, double c, double d) {
    return a + b + c + d;
}

constexpr auto plus4 = boost::hana::curry<4>(plus4_impl);
Run Code Online (Sandbox Code Playgroud)

然后使用它就像你想要的那样:

int main() {
    std::cout << plus4(1)(1.0)(3)(4.3f) << '\n';
    std::cout << plus4(1, 1.0)(3)(4.3f) << '\n'; // you can also do up to 4 args at a time
}
Run Code Online (Sandbox Code Playgroud)