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

66

我一直在学习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; };
}

我只是无法弄清楚如何实现函数f(a)(b)(c),正如我之前所说,它应该返回a + b + c


38
我可以问一下,为什么你要在C++中尝试函数式编程(而不是任何本质上的函数式语言)? - Ben Steffan
12
如果只使用f(a)(b)(c),不确定能否完成此操作。如果你想使用f(a)(b)(c)(),应该很容易让它正常工作。 - NathanOliver
6
C++在许多主流函数式编程语言出现之前就提供了函数对象。迭代器和算法只是从C++转移到函数式语言的一个特性。它 已经 支持Lambda表达式。它 已经 允许将函数作为参数传递给迭代器等等等等。 - Panagiotis Kanavos
7
@am.rez 这是lambda表达式的捕获列表 - Quentin
9
请注意,您可以在函数的返回类型中使用auto,从而避免使用std::function所带来的类型抹除代价。 - Quentin
显示剩余14条评论
9个回答

115

您可以通过让您的函数 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;
}

Example

Link

int main()
{
    std::cout << f(1)(2)(3)(4) << std::endl;
}

模板版本

你甚至可以写一个模板版本,让编译器推断类型。在这里试试

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;
}

例子

在这个例子中,T 最终会被解析为 double

std::cout << f(1)(2.5)(3.1f)(4) << std::endl;

46
@PanagiotisKanavos,你在说什么...标准算法期望使用函数或函数对象。Lambda表达式是函数对象的语法糖。这不会有任何问题,因为它实际上就是同一件事,并且在标准中也是如此定义的。此外,std::function与讨论无关--它是用于类型抹除的。 - Quentin
16
Lambda函数的问题在于它们是匿名的。但正如您在这里看到的,sum::operator()返回sum。由于缺乏名称,这种自我引用对于Lambda函数来说是不可能的。 - MSalters
26
首先,你应该意识到在C++中,lambda表达式只是定义一个重载operator()类的另一种语法形式。使用“正常”的语法来创建这样一个类并没有任何问题(特别是对于某些不适合使用lambda表达式语法的情况)。 - Jerry Coffin
14
请注意,不同语言对于“函数”的定义可能有所不同。C++已经从C中继承了“函数”的定义,因此它需要一个新术语来表示更广泛的概念。这种名义上的差异并不意味着在C++中,“可调用对象”与函数式语言中的“函数”在结构上有所不同。如果它走起路来像鸭子,叫起来像鸭子,那它就是一只鸭子。 - MSalters
15
按照这个论点,你排除了类似函数的lambda表达式,它们也是像函数一样运作的类,而不是实际的函数。 - Mooing Duck
显示剩余19条评论

60

只需将您的2元解决方案扩展,通过使用另一个lambda将其包装。

由于您想返回一个获取double并返回double加法lambda的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;
        };
    };
}

正如 @Ðаn 所指出的,您可以跳过 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; };
    };
}
你可以使用更深层次的嵌套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;
                };
            };
        };
    }
    

  • 11
    不错,能够满足原帖作者的需求,但是没有很好地扩展。 - NathanOliver
    4
    这正是我试图实现的方式,但我没有包括std::function<double(double)>(double)。谢谢,这让很多事情变得更清晰了,现在我知道如何将它嵌套得更深(尽管我不想再深入了)。 - Gigaxel
    3
    可以泛化到更少或更多的元素吗? - Jonas
    3
    我知道可以嵌套更多的lambda表达式。你如何在不定义像 "plus2" 和 "plus4" 这样的新函数的情况下实现它? - Jonas
    8
    通常的解决方案是使用非类型模板参数,具体来说就是用 plus<N-1> 定义 plus<N>。缺点很明显,你仍然需要事先指定精确的参数数量。 - MSalters
    显示剩余9条评论

    30

    这是一种稍微不同的方法,它从operator()返回对*this的引用,这样你就不会有任何浮动的副本了。这是一个非常简单的函数对象的实现,它存储状态并在自身上递归地进行左折叠:

    #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);
    }
    

    在Coliru上实时查看


    4
    这适用于任何数量的“加数”,并且是递归的,即不需要为每个级别定义另一个函数/lambda。 - Walter
    请原谅我的无知,但编译器如何知道调用您的强制类型转换运算符?我知道它实际上没有其他选择,因为没有定义插入运算符,但我没有意识到这样的东西可以在不显式使用强制类型转换的情况下工作。 - Dave Branton
    1
    @DaveBranton 语言规则规定编译器最多可以尝试一次用户定义的转换。在我们的情况下,我们有这样一个转换运算符,因此在 cout << s(1)(2)(3) 中,编译器首先看到它不能简单地显示它,然后搜索用户定义的转换运算符,并找到 Sum::operator T() const。然后将转换应用于 T,如果可以显示 T(在我们的情况下,它是一个 int),则显示它。 - vsoftco
    这就是函数链式调用的实现方式。 - KeyC0de

    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...);
                    });
            }
        };  
    }
    

    出于简洁起见,我省略了转发引用。例如使用方法如下:

    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
    

    4
    C++17使这更加整洁。在C++14中编写我的函数柯里化(curry)更加痛苦。需要注意的是,为了达到最高效率,可能需要一个函数对象,根据调用上下文有条件地移动其状态的元组。这可能需要解耦tup和可能从lambda捕获列表中解耦f,以便根据 () 的调用方式传入不同的rvalue。然而,这样的解决方案超出了此问题的范围。 - Yakk - Adam Nevraumont
    @Yakk 希望让它更整洁!需要找时间修改那篇论文... - Barry
    我在其中没有看到任何允许完美转发lambda本身的内容,所以如果在可能是右值的上下文中调用,你可以完美地转发按值捕获的东西。这可能需要一个不同的提案。(顺便说一句:你如何区分lambda的*this上下文和封闭方法的*this上下文?嗯。) - Yakk - Adam Nevraumont
    @Yakk 不,我只是试图让简单的lambda表达式更短。在std-proposals中有一些关于这样做的东西,比如auto fact = [] self (int i) { return (i < = 1) ? 1 : i * self(i-1); };,但我找不到相关文件。 - Barry

    12
    我能想到的最简单的方法是通过使用plus2()来定义plus3()
    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); };
    }
    

    这将第一个和第二个参数列表合并为单个arglist,用于调用plus2()。 这样做可以最小限度地重复使用我们现有的代码,并且可以很容易地在未来进行扩展; plusN()只需要返回调用plusN-1()的lambda函数,该函数将依次将调用传递给以前的函数,直到达到plus2()。 它可以像这样使用:

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

    考虑到我们只是顺序调用,我们可以轻松将其转换为函数模板,这消除了创建额外参数版本的需要。

    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
    

    在此处可以同时查看两者的效果 here


    2
    太好了!它可以很好地扩展,实现也非常简单。 - Jonas
    @Jonas 谢谢。当您尝试扩展此类现有框架时,最简单的方法可能是找到一种定义扩展版本的方式,以当前版本为基础,除非这样做会过于复杂。我也不认为原型对于模板化版本是严格必要的,但在某些情况下它可以增加清晰度。 - Justin Time - Reinstate Monica
    它确实存在在运行时执行的缺陷,但是一旦更多的编译器正确支持constexpr lambda,这个问题就可以得到缓解。 - Justin Time - Reinstate Monica

    11

    我将要玩。

    您想进行柯里化折叠加法。我们可以解决这一个问题,也可以解决包括此在内的一类问题。

    首先是加法:

    auto add = [](auto lhs, auto rhs){ return std::move(lhs)+std::move(rhs); };
    

    这很好地表达了加法的概念。

    现在,折叠:

    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;
      }
    };
    

    它需要一个种子值和一个T,然后允许链接。
    template<class F, class T>
    folder_t<F, T> folder( F fin, T tin ) {
      return {std::move(fin), std::move(tin)};
    }
    

    现在我们将它们连接起来。
    auto adder = folder(add, 0);
    std::cout << adder(2)(3)(4) << "\n";
    

    我们也可以使用 folder 来进行其他操作:

    auto append = [](auto vec, auto element){
      vec.push_back(std::move(element));
      return vec;
    };
    

    使用:

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

    实际演示

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


    这很酷!我认为它可以在某些情况下用作std::bind的替代品 :) - Slava
    有趣。你写了fold,我写了curry。不知道OP实际想要什么。 - Barry
    1
    @Barry 显然,这是“一个临时的、非正式规定的、有错误、速度慢的实现Common Lisp的一半”。 - Yakk - Adam Nevraumont
    @Barry 其实,我的设计(不会终止)是一个问题。"operator T"是一个hack。如果我将curry和folding混合在一起,我们可以提供一个二元操作和一个参数大小来生成对"+"的柯里化fold。这让我们可以做到auto f = fold<4>(add);,这可能比我们任何一个答案更符合OP的要求。 - Yakk - Adam Nevraumont
    1
    @Yakk 说实话,我大多数时候只是对这些顶部答案获得的赞数感到惊讶。 - Barry
    1
    @Barry 它在侧边栏中链接。/耸肩。 - Yakk - Adam Nevraumont

    10
    如果您愿意使用库,那么在Boost的Hana中这非常容易:Boost's 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);
    

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

    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
    }
    

    4
    所有这些答案看起来都非常复杂。
    auto f = [] (double a) {
        return [=] (double b) {
            return [=] (double c) {
                return a + b + c;
            };
        };
    };
    

    这段代码可以完全满足您的需求,并且它适用于C++11,而不像其他答案那样可能大多数都无法使用。

    请注意,它不使用std::function,因为这会导致性能下降,实际上,在许多情况下它可以被内联。


    -8
    这是一个基于状态模式单例的方法,使用operator()来改变状态。 编辑: 将不必要的赋值替换为初始化。
    #include<iostream>
    class adder{
    private:
      adder(double a)val(a){}
      double val = 0.0;
      static adder* mInstance;
    public:
      adder operator()(double a){
        val += a;
        return *this;}
      static adder add(double a){
        if(mInstance) delete mInstance;
        mInstance = new adder(a);
        return *mInstance;}
      double get(){return val;}
    };
    adder* adder::mInstance = 0;
    int main(){
      adder a = adder::add(1.0)(2.0)(1.0);
      std::cout<<a.get()<<std::endl;
      std::cout<<adder::add(1.0)(2.0)(3.0).get()<<std::endl;
      return 0;
    }
    

    3
    我并不是其中一个给这篇文章点踩的人,但很可能是因为代码格式糟糕,离零成本抽象化相距甚远(与其他更干净的C++解决方案相比,它非常缓慢且难以阅读),而且原帖作者暗示想要一种函数式编程的解决方案,然而这个代码与函数式编程几乎没有任何关系。 - rationalcoder
    我不明白静态的 add 函数的意义。在内部成员中存储实例指针有什么意义呢?你好像也没有问题返回 - JDługosz
    1
    ":val(0)"仍然是不必要的。使用"mInstance"太荒谬了,它掩盖了其他所有东西。根据最初的评论,您应该在[codereview.se]上发布一个问题,以便了解其中的可怕之处。 - JDługosz
    1
    代码甚至无法编译(缺少冒号和分号)。一旦您修复了这个问题并将构造函数设置为公共的,您就可以完全消除“mInstance”和“add”方法,并简单地执行“adder(1.0)(2.0)(1.0)”操作。你所说的这种模式,如果我们“学习了一些面向对象的设计模式”,是完全没有用的。 - DarkDust
    5
    天啊,那太可怕了。无论如何,移除mInstance,用return {a};替换add的整个函数体,你的代码就不会再浪费资源了。堆分配和静态状态什么也没做。虽然这并没有解决原问题,因为OP不想要.get(),但至少不会太糟糕。 - Yakk - Adam Nevraumont
    显示剩余5条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接