C++中的函数式编程

54

有人可以指导我如何在C++中进行函数式编程吗?是否有一些好的在线材料可以供我参考?

请注意,我知道FC++库。我想知道如何仅使用C++标准库来完成。

谢谢。


20
最好使用函数式编程语言(如LISP,Haskell,Scheme等)。这样你就可以确信自己所做的确实是函数式编程。 - Brian R. Bondy
4
你需要哪些FP功能?Boost提供了一些类似FP的库(mpl、function、lambda等),其中一些将在C++0x中出现,已经在TR1中使用。 - Macke
10
@Jacob:你很可能无法通过在C ++中“尝试”来学习这个。这就像说,“我想知道面向对象编程有什么好处。我该如何在VAX汇编中实现OOP?” - Chuck
4
我建议你学习OCaml或F#。对大多数人来说,理解函数式编程是最困难的部分。学习一个能帮助你做到这一点的语言会使学习不那么费力,而不是更费力。 - Chuck
7
虽然我总体上同意Chuck的观点,但我不同意具体的语言建议。如果你想学习函数式编程,请选择一种专门为函数式编程设计的语言。OCaml和F#是混合语言,拥有许多面向对象编程特性。这意味着熟悉面向对象编程的人会倾向于继续使用熟悉的面向对象编程风格。我建议直接尝试SML或Haskell等语言,这样你就被迫只能使用FP,而没有其他选择。 - jalf
显示剩余7条评论
6个回答

49
你可以使用现代C++实现出人意料的“函数式编程”风格。事实上,自从C++标准化以来,该语言一直在这个方向上发展。
标准库包含类似于map、reduce等算法(如for_each、transform、adjacent_sum等)。下一个版本,C++0x,包含许多功能,旨在让程序员以更加函数式的方式处理它们(如lambda表达式等)。
探索各种Boost库以获得更多乐趣。只是为了说明标准C++包含了大量的函数式特性,这里提供了一个延续传递风格的阶乘函数的标准C++实现。
#include <iostream>

// abstract base class for a continuation functor
struct continuation {
    virtual void operator() (unsigned) const = 0;
};

// accumulating continuation functor
struct accum_cont: public continuation {
    private:
        unsigned accumulator_;
        const continuation &enclosing_;
    public:
        accum_cont(unsigned accumulator, const continuation &enclosing)
            : accumulator_(accumulator), enclosing_(enclosing) {}; 
        virtual void operator() (unsigned n) const {
            enclosing_(accumulator_ * n);
        };
};

void fact_cps (unsigned n, const continuation &c)
{
    if (n == 0)
        c(1);
    else
        fact_cps(n - 1, accum_cont(n, c));
}

int main ()
{
    // continuation which displays its' argument when called
    struct disp_cont: public continuation {
        virtual void operator() (unsigned n) const {
            std::cout << n << std::endl;
        };
    } dc;

    // continuation which multiplies its' argument by 2
    // and displays it when called
    struct mult_cont: public continuation {
        virtual void operator() (unsigned n) const {
            std::cout << n * 2 << std::endl;
        };
    } mc;

    fact_cps(4, dc); // prints 24
    fact_cps(5, mc); // prints 240

    return 0;
}

好吧,我说了一点谎,它是一个阶乘函数对象。毕竟,闭包是穷人的对象...反之亦然。C++中使用的大多数函数式技术都依赖于使用函数对象(即函数对象)---您将在STL中广泛看到这一点。


1
不错的例子。但是我必须说标识符名称的选择非常糟糕,如果你能编辑你的帖子并使用一些更好的标识符名称,那就太好了。顺便说一句,我已经给你点赞了。谢谢! :) - Red Hyena
2
毕竟,闭包是穷人的对象...反之亦然。我不同意。是的,你可以使用其中一个来实现另一个...但是如果你想基于函数对象重新实现对象会发生什么?这将是一个丑陋的混乱。如果你在基于闭包的对象上重新实现闭包呢?它(几乎)解开了自己,回到了“本地”概念。因此,我认为闭包比对象更适合作为“原语”(别让我开始谈论基于类的对象!)。 - Javier
谢谢,Jacob!我稍微整理了一下这个示例。它从来没有被意图公开展示; 我只是某一天读了Lambda: The Ultimate Imperative,想在C++中尝试使用CPS。但后来我看到了这个问题,就不得不分享… - Derrick Turk
1
@Javier:我知道这是一种在实践中并不十分重要的等价关系(参见图灵等价性)。然而,标准C++实际上正在朝着使“闭包”更加透明化的方向发展(据我所知,使用对象作为幕后机制)。即使在当前语言环境下,函数对象也广泛用于实现与“一级”闭包相同的目的。 - Derrick Turk
作为一种函数式语言,我在C++中所缺少的是柯里化和函数组合的简单表示法、尾调用优化、不会因为捕获变量超出作用域而失效的闭包等。 - Giorgio
我很惊讶没有人提到C++03+TR1或C++11的std::functionstd::bindstd::ref。结合起来,您可以获得“函数作为参数”。再加上#include <algorithm>以及编写没有副作用的函数,您就可以进行函数式编程了。 - Charles L Wilcox

26

更新于 2014 年 8 月:此回答发布于 2009 年。C++11 在 C++ 函数式编程方面有了相当的改进,因此本回答已不再准确。我将其保留在下方作为历史记录。

由于此回答被选中为最佳答案,我将其转换为社区 Wiki。欢迎协作改进,以添加关于现代 C++ 中函数式编程的实际提示。


你不能真正使用 C++ 进行函数式编程,你只能通过大量的痛苦和复杂性来近似实现它(虽然在 C++11 中这变得稍微容易了一些)。因此,不建议采用这种方法。C++ 相对较好地支持其他编程范例,并且在我看来,不应弯曲其支持较差的范例 - 最终将使只有作者理解的代码难以阅读。


18
用C++编程函数式编程并不有趣。如果想要进行有趣的函数式编程,请使用Lisp或Haskell。 - Eli Bendersky
13
我必须坚持一点:)这将比学习如何使用 C++ 实现要花费更少的时间。 - Eli Bendersky
9
我不认为这是公平的。当然,在C++中无法完全避免副作用,但你可以采用函数式编程方法来解决该问题。像《Higher Order Perl》这样的书籍表明,函数式方法可以在命令式语言中使用。此外,C++的各种特性使得采用函数式风格比如C语言更容易。 - daf
1
请注意,“Lisp”通常指的是“Common Lisp”,它不是功能性语言 - 它是多范式的,甚至不能保证TCO。另一方面,像Scheme和Clojure这样的其他Lisp方言是功能性的。 - yati sagade
1
尽管C++11已经出现,但在我看来,原始答案仍然准确。我在C++和函数式语言方面都有广泛的工作经验,可以证明它们仍然是两个完全不同的领域。C++的语法、类型系统和低级语义仍然是非常大的障碍——这些根本问题不会因为向语言中添加更多功能而消失。是的,你可以模拟一些函数式技术,但这非常冗长、非常痛苦(特别是出现错误时),并且受到缺乏GC、缺乏通用尾调用、缺乏模式匹配和缺乏多态类型检查的限制。 - Andreas Rossberg
显示剩余10条评论

14

更新于2018年12月

我已经创建了一个全面的材料列表,您可以在其中找到文章、讲座、屏幕录像、论文、库和展示:

C++中的函数式编程


考虑我的四个研究项目:

该项目是“Amber”游戏的工作原型。 代码演示了许多主要的功能概念:不可变性lambda表达式monad组合子纯函数声明式代码设计。它使用Qt C ++和C ++ 11功能。

例如,可以看到如何将任务链接成一个大任务,当应用时会修改Amber的世界:

const AmberTask tickOneAmberHour = [](const amber::Amber& amber)
{
    auto action1Res = magic::anyway(inflateShadowStorms, magic::wrap(amber));
    auto action2Res = magic::anyway(affectShadowStorms, action1Res);
    auto action3Res = magic::onFail(shadowStabilization, action2Res);
    auto action4Res = magic::anyway(tickWorldTime, action3Res);
    return action4Res.amber;
};

这是一个展示C++中通用函数lenses的示例。使用了Variadic Templates,一些有趣(且有效)的C++技巧来使lenses可以组合并且看起来简洁。该库仅为演示目的,因此仅提供了一些最重要的组合器,即:set()view()traverse()bind()、中缀字面量组合器toover()等。

(请注意,存在“C++ Lenses”项目project:但它不是关于真正的“镜头”,而是关于类属性,具有C#或Java属性的getter和setter。)
快速示例
Car car1 = {"x555xx", "Ford Focus", 0, {}};
Car car2 = {"y555yy", "Toyota Corolla", 10000, {}};

std::vector<Car> cars = {car1, car2};

auto zoomer = traversed<Car>() to modelL();

std::function<std::string(std::string)> variator = [](std::string) { return std::string("BMW x6"); };
std::vector<Car> result = over(zoomer, cars, variator);

QVERIFY(result.size() == 2);
QVERIFY(result[0].model == "BMW x6");
QVERIFY(result[1].model == "BMW x6");

你可能听说过单子(monads)。如今在函数编程的谈话中无处不在地都能听到单子这个词。但是什么是共单子(comonads)呢?我介绍了带有共单子概念的一维和二维细胞自动机。目的是展示如何轻松地使用 std::future 作为 Par monad 从单流代码转移到并行代码。该项目还对两种方法进行了基准测试和比较。

快速示例

template <typename A, typename B>
UUB fmap(
    const func<B(UUA)>& f,
    const UUUUA& uuu)
{
    const func<UB(UUUA)> f2 = [=](const UUUA& uuu2)
    {
        UB newUt;
        newUt.position = uuu2.position;
        newUt.field = fp::map(f, uuu2.field);
        return newUt;
    };

    return { fp::map(f2, uuu.field), uuu.position };
}

这个库基于自由单子(Free monad)和其他一些函数式编程的高级思想。其接口类似于Haskell的本地STM库。事务可以在单子中组合,是纯函数式的,还有很多有用的单子组合器,使设计并发模型更加方便和强大。我使用这个库实现了哲学家就餐问题(Dining Philosophers problem),并且它运行良好。这是一些哲学家取叉子的交易示例:

STML<Unit> takeFork(const TFork& tFork) {
    return withTVar<Fork, Unit>(tFork, [=](const Fork& fork) {
       if (fork.state == ForkState::Free) {
           return writeTVar<Fork>(tFork, Fork {fork.name, ForkState:Taken});
       }
       else {
           return retry<Unit>();
       }
    });
}

STML<Unit> takeForks(const TForkPair& forks) {
    STML<Unit> lm = takeFork(forks.left);
    STML<Unit> rm = takeFork(forks.right);
    return sequence(lm, rm);
}

5
我认为在C++中实现真正的函数式编程并不是不可能的,但它肯定不是最容易或自然的方式。此外,您可能只需要使用一些类似函数式的习惯用法而不是整个思维方式(即“流畅风格”)。
我的建议是学习一门函数式语言,也许从Scheme开始,然后再转向Haskell。然后在C++编程时应用所学知识。也许您不会使用明显的函数式风格,但您可能会获得最大的优势(即使用不可变结构)。

作为一个学习 Haskell 的人,我很想知道你为什么建议先学 Scheme。 - Evan Carroll
我以前看过一些Scheme代码。对我来说,它看起来像是中日文一样。:| - Red Hyena
好的,我会尝试一些函数式编程语言。谢谢你的回复! - Red Hyena
@EvanCarroll:可能只是我学习它们的方式不同,但我认为Scheme更加符合预期。此外,有一些好的(但有点老旧)Scheme入门教材,其他函数式编程语言的入门教材就不太多了(或者不太知名)。 - Javier
@Jacon Johnson:是的,第一眼看到新语法(或缺乏语法)可能会让人不安,但这只是表面现象。真正的精华在于底层(但某些东西由于缺乏语法而更容易)。尽管如此,Scheme并没有什么神奇之处,只要选择任何看起来可行的FP语言即可。 - Javier

1
有一本名为《Functional C》的书,作者是Pieter Hartel和Henk Muller,可能会有所帮助。如果还可以得到的话,这里是一些信息的链接此处。如果我没记错的话,这本书还不错。

根据描述,那本书教授命令式编程给那些来自函数式语言的人。而不是如何在C中进行函数式编程(我想这比在C++中进行要痛苦得多)。 - sepp2k
表面上是这样,但没有什么阻止你朝相反的方向使用它。它在这方面运作得非常好,并且确实为您提供了一些指示如何以函数式风格编写C程序。 - rvirding

0

可能有点晚了,但对于其他寻找答案的人 - 我使用Lua作为C ++的函数式编程扩展,它非常棒。 lua


1
这真的是建议改用另一种语言吗? - CyberFox
这篇回答应该详细阐述Lua如何与C++紧密集成(不仅仅是调用子例程/函数/方法/别名),特别是在使用Lua进行FP的同时,以某种协同作用使用C++进行非FP的操作。 - Andreas ZUERCHER

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