【问题标题】:Functional programming in C++. Implementing f(a)(b)(c)C++中的函数式编程。实施 f(a)(b)(c)
【发布时间】:2017-10-02 16:05:45
【问题描述】:

我一直在学习使用 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

【问题讨论】:

  • 请问您究竟为什么要尝试在 c++ 中进行函数式编程(而不是任何固有的函数式语言)?
  • 不确定您是否可以仅使用 f(a)(b)(c)。如果你想使用f(a)(b)(c)(),你应该可以很容易地让它工作。
  • @BenSteffan C++ 在一些主流函数式语言出现之前就提供了函数对象。迭代器和算法只是从 C++ 转移到函数式语言的一个特性。它已经支持 lambdas。它已经允许将函数作为参数传递给迭代器。等等等等等等
  • 请注意,您可以使用auto作为函数的返回类型,并避免std::function的类型擦除成本。

标签: c++ c++11 functional-programming currying std-function


【解决方案1】:

你可以通过让你的函数f返回一个functor来做到这一点,即一个实现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;
}

示例

Link

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

模板版本

您甚至可以编写一个模板版本,让编译器推断类型。试试看here

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;

【讨论】:

  • @PanagiotisKanavos 你在说什么……标准算法需要函数或函数对象。 lambda 表达式是函数对象的语法糖。不会有任何问题,因为它是完全相同的东西,甚至在标准中也是这样定义的。此外,std::function 与讨论相切——用于类型擦除。
  • @PanagiotisKanavos:lambda 的问题在于它们是匿名的。但正如您在此处看到的,sum::operator() 返回sum。没有名字,这种自我引用对于 lambda 是不可能的。
  • @PanagiotisKanavos:首先,您应该意识到,在 C++ 中,lambda 表达式只是定义重载 operator() 的类的另一种语法。使用“普通”语法来创建这样的类在世界上没有任何问题(尤其是对于 lambda 表达式语法不能很好地工作的情况)。
  • @PanagiotisKanavos:请注意,不同的语言对函数的确切定义有不同的定义。 C++ 已经从 C 继承了 function 的定义,因此它需要一个新术语来表示更广泛的概念。名义上的差异并不意味着 C++ 中的 callable objects 在结构上与函数式语言中的 functions 不同。如果它走路像鸭子,说话像鸭子,那就是鸭子。
  • @PanagiotisKanavos:根据这个论点,你已经排除了 lambda,它们也是像函数一样起作用的类,而不是实际的函数。
【解决方案2】:

只需使用您的 2 元素解决方案并通过用另一个 lambda 包装它来扩展它。

由于您要返回一个 lambda,它得到一个 double 并返回一个 doubles 的附加 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&lt;std::function&lt;double(double)&gt;(double)&gt;并与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;
                };
            };
        };
    }
    

【讨论】:

  • 很好,得到了 OP 想要的东西,但扩展得不好。
  • 这正是我试图实现它的方式,但我没有包含 std::function(double)。谢谢,它让很多事情变得更清楚了,现在我现在如何将它嵌套得更深(尽管我不想更进一步)。
  • 很好,是否可以概括为,例如,越来越少的元素?
  • @Uriel 我知道嵌套更多 lambdas 的可能性。如果不定义像 plus2plus4 这样的新函数,你将如何实现它?
  • @Jonas:通常的解决方案是非类型模板参数,特别是根据plus&lt;N-1&gt; 定义plus&lt;N&gt;。缺点很明显;您仍然需要预先指定确切的参数数量。
【解决方案3】:

这是一种稍微不同的方法,它从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);
}

Live on Coliru

【讨论】:

  • 这适用于任意数量的(summand) 并且是递归的,即不需要为每个级别定义另一个函数/lambda。
  • 请原谅我的无知,但是编译器怎么知道调用你的强制转换运算符呢?我看到它实际上不能做任何其他事情,因为没有定义插入运算符,但我没有意识到这样的事情在没有明确的演员表的情况下也可以工作。
  • @DaveBranton 该语言的规则说编译器最多可以尝试一次用户定义的转换。在我们的例子中,我们有这样一个转换运算符,所以在cout &lt;&lt; s(1)(2)(3) 编译器首先看到它不能简单地显示它,然后它搜索用户定义的转换运算符,并找到Sum::operator T() const。然后它将转换应用到T,然后如果可以显示T(在我们的例子中它可以,因为它是int)。
  • 这就是函数链的实现方式。
【解决方案4】:

这不是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

【讨论】:

  • C++17 让它变得如此整洁。用 C++14 编写我的函数 curry 要痛苦得多。请注意,为了提高效率,您可能需要一个函数对象,根据调用上下文有条件地移动其状态元组。这可能需要从 lambda 捕获列表中解耦 tupf,以便可以根据 () 的调用方式以不同的右值传递它。然而,这样的解决方案超出了这个问题的范围。
  • @Yakk 希望能成功even tidier!需要找时间修改那篇论文...
  • 我看不到任何允许完美转发 lambda 本身的“this”的东西,因此如果在可能右值上下文中调用,您可以完美地转发按值捕获的东西。那可能必须是一个不同的提议。 (顺便说一句:你如何区分 lambda 的 *this 上下文和封闭方法的 *this 上下文?嗯。)
  • @Yakk 不,我只是想让简单的 lambda 更短。标准提案中出现了一些类似auto fact = [] self (int i) { return (i &lt; = 1) ? 1 : i * self(i-1); }; 的内容,但我找不到论文。
【解决方案5】:

我能想到的最简单的方法是用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

【讨论】:

  • 这太棒了!它可以很好地扩展,实现起来也很简单。
  • @Jonas 谢谢。当您尝试像这样扩展现有框架时,最简单的方法可能是找到一种根据当前版本定义扩展版本的方法,除非这太复杂了。我也不认为原型对于模板版本是绝对必要的,但在某些情况下它可以增加清晰度。
  • 虽然它确实存在在运行时执行的缺陷,但是一旦更多的编译器正确支持constexpr lambdas,就可以减轻这种缺陷。
【解决方案6】:

我要去玩了。

你想做一个咖喱折叠加法。我们可以解决这个问题,或者我们可以解决包括这个问题的一类问题。

所以,首先,加法:

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

Live example.

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

【讨论】:

  • 这太酷了!而且我认为在某些情况下它可以用作std::bind 的替代品:)
  • 有趣。你写了折叠,我写了咖喱。想知道 OP 到底想要什么。
  • @Barry 显然是“Common Lisp 一半的临时、非正式指定、漏洞百出、缓慢的实现”
  • @Yakk TBH 我对其中一些热门答案有多少赞成票感到困惑。
  • @Barry 已在侧边栏中链接。 /耸耸肩。
【解决方案7】:

如果你愿意使用库,这在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
}

【讨论】:

    【解决方案8】:

    所有这些答案似乎都非常复杂。

    auto f = [] (double a) {
        return [=] (double b) {
            return [=] (double c) {
                return a + b + c;
            };
        };
    };
    

    完全符合您的要求,并且可以在 C++11 中使用,这与此处的许多或大多数其他答案不同。

    请注意,它不使用会导致性能损失的std::function,事实上,在许多情况下它可能会被内联。

    【讨论】:

      【解决方案9】:

      这是一个使用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;
      }
      

      【讨论】:

      • 我不是反对者之一,但这可能是因为代码格式不正确,它尽可能远离零成本抽象(也就是说,与其他更清洁的 C++解决方案,看起来非常缓慢和痛苦),并且OP暗示他想要一个FP风格的解决方案,而这与FP尽可能远。
      • 您的构造函数分配给成员(而不是初始化),但您val成员上指定了隐式初始化,所以这是不必要的。
      • :val(0)仍然没有必要mInstance 的使用是如此荒谬,以至于它掩盖了其他一切。根据最初的评论,您应该在Code Review 上发布一个问题,以便了解它的可怕之处。
      • 代码甚至无法编译(缺少冒号和分号)。一旦你修复了这个问题并公开了构造函数,你就可以完全消除mInstanceadd 方法,而只需执行adder(1.0)(2.0)(1.0);。你的这种模式,你说如果我们“学习了一些面向对象的设计模式”就会使用它是完全没用的。
      • 天哪,这太可怕了。在任何情况下,删除mInstance,将add 的整个主体替换为return {a};,您的代码就不会那么荒谬地浪费了。堆分配和静态状态什么都不做。仍然没有解决 OP 的问题,因为 OP 不想要 .get(),但至少不会很糟糕。
      猜你喜欢
      • 1970-01-01
      • 2020-12-25
      • 2016-07-06
      • 1970-01-01
      • 2016-09-16
      • 2017-04-19
      • 2015-06-12
      • 1970-01-01
      相关资源
      最近更新 更多