并置函数

其实很多函数没有必要完全自定义的去实现。让我们先来看一个使用Haskell实现的在文本中查找单一单词的例子。第一行定义了一个unique_words函数,在第二行中传入一个字符串:

并置函数 - 图1

Wow,就是这么简单!这里不对Haskell的语法做过多的解释,让我们来看一下代码。其定义了一个unique_words的函数,该函数对其传入的参数进行了一系列的处理。首先,使用map toLower将所有字符都小写化。然后,将句子用逗号进行分割,比如"foo bar baz"就会已变成["foo", "bar","baz"]。接下来,将单词列表进行排序。这样,["a", "b", "a"]就会变为["a", "a", "b"]。现在,使用group函数,其会将相同的词组放到一个列表中,也就是["a", "a", "b"]成为[ ["a", "a"], ["b"] ]。现在就差不多快完事了,接下来就让我们数一下列表中一共有多少个组,这个工作由length函数完成。

多么完美的编程方式呀!我们可以从右往左看,来了解这段代码是如何工作的。这里我就不需要关心每个细节是如何进行实现(除非其性能很差,或者有Bug)。

我们不是来赞美Haskell的,而是来提升我们自己C++技能的,这样的方式在C++中同样奏效。本节的例子会展示如何使用Lambda表达式来模仿并置函数。

How to do it…

本节中定义了一些函数对象,并将它们串联起来,也就是将一个函数的输出作为另一个函数的输入,以此类推。为了很好的展示这个例子,我们编写了一些串联辅助函数:

  1. 包含必要的头文件

    1. #include <iostream>
    2. #include <functional>
  2. 然后,我们实现一个辅助函数concat,其可以去任意多的参数。这些参数都是函数,比如f,g和h。并且一个函数的结果是另一个函数的输入,可以写成f(g(h(...))):

    1. template <typename T, typename ...Ts>
    2. auto concat(T t, Ts ...ts)
    3. {
  3. 现在,代码就会变有些复杂了。当用户提供函数f,g和h时,我们现将其转换为f( concat(g,h)),然后再是f(g(concat(h))),类似这样进行递归,直到得到f(g(h(...)))为止。用户提供的这些函数都可以由Lambda表达式进行捕获,并且Lambda表达式将在之后获得相应的参数p,然后前向执行这些函数f(g(h(p)))。这个Lambda表达式就是我们要返回的。if constexpr结构会检查在递归步骤中,当前函数是否串联了多个函数:

    1. if constexpr (sizeof...(ts) > 0) {
    2. return [=](auto ...parameters) {
    3. return t(concat(ts...)(parameters...));
    4. };
    5. }
  4. 当我们到达递归的末尾,编译器会选择if constexpr的另一分支。这个例子中,我们只是返回函数t,因为其传入的只有参数了:

    1. else {
    2. return t;
    3. }
    4. }
  5. 现在,让我们使用刚创建的函数连接器对函数进行串联。我们先在主函数的起始位置定义两个简单的函数对象:

    1. int main()
    2. {
    3. auto twice ([] (int i) { return i * 2; });
    4. auto thrice ([] (int i) { return i * 3; });
  6. 现在,来串联他们。这里我们将两个乘法器函数和一个STL函数std::plus<int>放在一起,STL的这个函数可以接受两个参数,并返回其加和。这样我们就得到了函数twice(thrice(plus( a, b ))):

    1. auto combined (
    2. concat(twice, thrice, std::plus<int>{})
    3. );
  7. 我们来应用一下。combined函数现在看起来和一般函数一样,并且编译器会将这些函数连接在一起,且不产生任何不必要的开销:

    1. std::cout << combined(2, 3) << '\n';
    2. }
  8. 编译运行这个例子就会得到如下的结果,和我们的期望一致,因为2 * 3 * (2 + 3)为30:

    1. $ ./concatenation
    2. 30

How it works…

concat函数是本节的重点。其函数体看起来非常的复杂,因为其要对另一个Lambda表达式传过来ts参数包进行解析,concat会递归多次调用自己,每次调用参数都会减少:

  1. template <typename T, typename ...Ts>
  2. auto concat(T t, Ts ...ts)
  3. {
  4. if constexpr (sizeof...(ts) > 0) {
  5. return [=](auto ...parameters) {
  6. return t(concat(ts...)(parameters...));
  7. };
  8. } else {
  9. return [=](auto ...parameters) {
  10. return t(parameters...);
  11. };
  12. }
  13. }

让我们写一个简单点的版本,这次串联了三个函数:

  1. template <typename F, typename G, typename H>
  2. auto concat(F f, G g, H h)
  3. {
  4. return [=](auto ... params) {
  5. return f( g( h( params... ) ) );
  6. };
  7. }

这个例子看起来应该很简单了吧。返回的Lambda表达式可以对f,g和h函数进行捕获。这个Lambda表达式可以接受任意多的参数传入,然后在调用f,g和h函数。我们先定义auto combined (concat(f, g, h)),并在之后传入两个参数,例如combined(2, 3),这里的2和3就为concat函数的参数包。

看起来很复杂,但concat却很通用,有别与f(g(h( params... )))式的串联。我们完成的是f(concat(g, h))(params...)的串联,f(g(concat(h)))(params...)为其下一次递归调用的结果,最终会的结果为f(g(h( params...)))