这个元组创作成语有没有名字?

Boost邮件列表中 ,@LouisDionne最近发布了下面这个聪明的技巧来创build一个元组实体:

#include <iostream> auto list = [](auto ...xs) { return [=](auto access) { return access(xs...); }; }; auto length = [](auto xs) { return xs([](auto ...z) { return sizeof...(z); }); }; int main() { std::cout << length(list(1, '2', "3")); // 3 } 

现场示例

巧妙的是, list是一个lambda采用可变参数列表作为input,并返回一个lambda作为输出,将采取另一个lambda作用于其input。 类似地, length是一个lambdaexpression式,它将向列表的原始input参数提供variadic sizeof...操作符。 sizeof...运算符被封装在一个lambda中,以便它可以传递给list

问题 :这个元组创作习惯有没有名字? 也许从函数式编程语言中,更高阶的函数更常用。

我认为这是一个Monad类事物的微妙实现,具体来说就是继续monad的精神。

Monad是一个函数式编程结构,用于模拟计算的不同步骤之间的状态(请记住,函数式语言是无状态的)。
monad做的是链接不同的函数,创build一个“计算stream水线” ,每个步骤都知道计算的当前状态。

Monad有两个主要的柱子:

  • 一个返回函数,它接受一个值并以Monad就绪的forms返回它。
  • 一个绑定函数,它需要一个Monad-ready值(来自上一个pipe道步骤)并将其解开为原始值,以将值传递给下一步。

维基百科有关单子的很好的例子和解释。

让我重写给定的C ++ 14代码:

 auto list = []( auto... xs ) { return [=]( auto access ) { return access(xs...); }; }; 

我认为在这里我们确定monad的return函数:获取值并以单向方式返回。 具体来说,该返回返回从“元组”类别到可变包类别的函子(从math意义上讲,不是C ++函子)。

 auto pack_size = [](auto... xs ) { return sizeof...(xs); }; 

pack_size只是一个正常的function。 它将被用来做一些工作。

 auto bind = []( auto xs , auto op ) { return xs(op); }; 

length只是monad bind操作符的一个非通用版本,一个操作符从前一个stream水线步骤中取一个monadic值,并绕过它到指定的函数(Function真正做这个工作)。 该function是由这个计算步骤完成的function。

最后你的电话可以改写为:

 auto result = bind(list(1,'2',"3"), pack_size); 

那么, 这个元组创作成语的名字是什么呢? 那么,我认为这可以称为“ monad-like元组 ”,因为它不完全是一个monad,但是元组的表示和扩展以类似的方式工作,只剩下Haskell延续monad。

编辑:更有趣

仅仅为了搞笑C ++编程,我跟着探索这个monad-like的东西。 你可以在这里find一些例子。

我会称这个成语的元组连续 或者更一般的单子连续符 。 这绝对是一个延续单子的例子。 inheritancemonad for C ++程序员的一个很好的介绍就在这里 。 本质上,上面的lambdaexpression式接受一个值(一个可变参数包)并返回一个简单的“continuator”(内部闭包)。 这个continuator在给定可调用(称为access )的情况下,将参数包传递给它,并返回可调用的返回值。

从FPComplete博客中借用,一个连续性或多或less如下。

 template<class R, class A> struct Continuator { virtual ~Continuator() {} virtual R andThen(function<R(A)> access) = 0; }; 

上面的Continuator是抽象的 – 不提供实现。 所以,这是一个简单的。

 template<class R, class A> struct SimpleContinuator : Continuator<R, A> { SimpleContinuator (A x) : _x(x) {} R andThen(function<R(A)> access) { return access(_x); } A _x; }; 

SimpleContinuator接受一个types为A值,并在调用andThen时传递它。 上面的lambda list本质上是一样的。 这是更一般的。 内部闭包捕获一个参数包,并将其传递给access函数。 整齐!

希望能够解释成为一名继任者意味着什么。 但是做一个单子是什么意思? 这是一个很好的介绍使用图片。

我认为list lambda也是一个列表单子,它是作为一个延续单子实现的。 请注意, 继续monad是所有monad的母亲 。 也就是说,你可以用一个连续monad来实现任何monad。 当然,monad列表并不是遥不可及的。

作为一个参数包自然是一个“列表”(通常是异构types),它有意义的工作就像一个列表/序列monad。 上面的lambdaexpression式是将C ++参数包转换为一元结构的一种非常有趣的方式。 因此,可以将操作相继链接起来。

然而,上面的length lambda有点令人失望,因为它打破了monad,而嵌套的lambda里面只是返回一个整数。 如下所示,可以说有更好的方法来写出长度“getter”。

—- —-函子

在我们可以说列表lambda是monad之前,我们必须certificate它是一个函子。 也就是说,fmap必须写成列表。

上面的lambda表单用作参数包中函子的创build者 – 实际上它是作为return 。 创build的函子保持与自己的参数包(捕获),它允许“访问”它提供了一个可接受的,可接受的variables数量的参数。 请注意,可调用函数被称为“完全一次”。

让我们写一个这样的函子的fmap。

 auto fmap = [](auto func) { return [=](auto ...z) { return list(func(z)...); }; }; 

func的types必须是(a – > b)。 也就是说,在C ++中,

 template <class a, class b> b func(a); 

fmap的types是fmap: (a -> b) -> list[a] -> list[b] Ie,用C ++说,

 template <class a, class b, class Func> list<b> fmap(Func, list<a>); 

也就是说,fmap只是将list-of-a映射到b的list-of。

现在你可以做了

 auto twice = [](auto i) { return 2*i; }; auto print = [](auto i) { std::cout << i << " "; return i;}; list(1, 2, 3, 4) (fmap(twice)) (fmap(print)); // prints 2 4 6 8 on clang (g++ in reverse) 

因此,它是一个函子。

– – 单子 – –

现在,让我们试着写一个flatmap (aka bindselectmany

平面图的types是flatmap: (a -> list[b]) -> list[a] -> list[b].

也就是说,给定一个将a映射到b列表和a列表的函数,flatmap返回一个b列表。 从本质上讲,它从a的列表中的每个元素,调用func,逐个接收(可能是空的)list-of-b,然后连接所有的b列表,最后返回最后的列表-of-b。

以下是列表的平面图实现。

 auto concat = [](auto l1, auto l2) { auto access1 = [=](auto... p) { auto access2 = [=](auto... q) { return list(p..., q...); }; return l2(access2); }; return l1(access1); }; template <class Func> auto flatten(Func) { return list(); } template <class Func, class A> auto flatten(Func f, A a) { return f(a); } template <class Func, class A, class... B> auto flatten(Func f, A a, B... b) { return concat(f(a), flatten(f, b...)); } auto flatmap = [](auto func) { return [func](auto... a) { return flatten(func, a...); }; }; 

现在你可以用列表做很多强大的事情。 例如,

 auto pair = [](auto i) { return list(-i, i); }; auto count = [](auto... a) { return list(sizeof...(a)); }; list(10, 20, 30) (flatmap(pair)) (count) (fmap(print)); // prints 6. 

count函数是一个monad-perserving操作,因为它返回单个元素的列表。 如果你真的想得到长度(不包含在列表中),你必须终止monadic链,并得到如下的值。

 auto len = [](auto ...z) { return sizeof...(z); }; std::cout << list(10, 20, 30) (flatmap(pair)) (len); 

如果正确的话, 集合pipe道模式(例如filterreduce )现在可以应用于C ++参数包。 甜!

—- Monad Laws —-

让我们确保list monad满足所有三个monad法则 。

 auto to_vector = [](auto... a) { return std::vector<int> { a... }; }; auto M = list(11); std::cout << "Monad law (left identity)\n"; assert(M(flatmap(pair))(to_vector) == pair(11)(to_vector)); std::cout << "Monad law (right identity)\n"; assert(M(flatmap(list))(to_vector) == M(to_vector)); std::cout << "Monad law (associativity)\n"; assert(M(flatmap(pair))(flatmap(pair))(to_vector) == M(flatmap([=](auto x) { return pair(x)(flatmap(pair)); }))(to_vector)); 

所有断言都满意。

—-集合pipe道—-

虽然上面的'list'lambda可以certificate是monad和monter共有的特征,但是这是非常不愉快的。 尤其是,由于常见的集合pipe道组合filter的行为,如filter (又名where )不符合公众期望。

原因就是C ++ lambdaexpression式的工作原理。 每个lambdaexpression式产生一个唯一types的函数对象。 因此, list(1,2,3)产生一个与list(1)无关的types和一个空列表,在这种情况下它将是list()

直接执行的where编译失败,因为在C ++中函数不能返回两种不同的types。

 auto where_broken = [](auto func) { return flatmap([func](auto i) { return func(i)? list(i) : list(); // broken :-( }); }; 

在上面的实现中,func返回一个布尔值。 这是一个谓词,对每个元素说真或假。 ?:操作符不能编译。

所以,可以使用不同的技巧来继续收集pipe道。 而不是实际过滤元素,他们只是简单地标记 – 这是什么让它不愉快。

 auto where_unpleasant = [](auto func) { return [=](auto... i) { return list(std::make_pair(func(i), i)...); }; }; 

where_unpleasant完成工作,但不愉快…

例如,这是如何过滤负面的元素。

 auto positive = [](auto i) { return i >= 0; }; auto pair_print = [](auto pair) { if(pair.first) std::cout << pair.second << " "; return pair; }; list(10, 20) (flatmap(pair)) (where_unpleasant(positive)) (fmap(pair_print)); // prints 10 and 20 in some order 

—-异构元组—-

到目前为止,讨论关于同构元组。 现在让我们把它推广到真正的元组。 但是, fmapflatmap只需要一个callbacklambda。 为了提供多个lambda,每个都可以在一个types上工作,我们可以重载它们。 例如,

 template <class A, class... B> struct overload : overload<A>, overload<B...> { overload(A a, B... b) : overload<A>(a), overload<B...>(b...) {} using overload<A>::operator (); using overload<B...>::operator (); }; template <class A> struct overload<A> : A{ overload(A a) : A(a) {} using A::operator(); }; template <class... F> auto make_overload(F... f) { return overload<F...>(f...); } auto test = make_overload([](int i) { std::cout << "int = " << i << std::endl; }, [](double d) { std::cout << "double = " << d << std::endl; }); test(10); // int test(9.99); // double 

让我们使用重载的lambda技术来处理一个异构的元组连续器。

 auto int_or_string = make_overload([](int i) { return 5*i; }, [](std::string s) { return s+s; }); list(10, "20") (fmap(int_or_string)) (fmap(print)); // prints 2020 and 50 in some order 

最后, 现场示例

这看起来像是一种延续传球风格 。

CPS的粗略思想是这样的:不是有一个函数(比如说f )返回某个值,而是给另一个参数,这个函数叫做continuation 。 然后, f用返回值而不是返回来调用这个延续。 举个例子:

 int f (int x) { return x + 42; } 

 void f (int x, auto cont) { cont (x + 42); } 

这个调用是一个尾部调用,可以优化为一个跳转(这就是为什么TCO需要在某些语言中使用,如Scheme,其语义依赖于某种forms的CPS转换)。

另一个例子:

 void get_int (auto cont) { cont (10); } void print_int (int x) { printf ("%d", x), } 

您现在可以执行get_int (std::bind (f, _1, print_int))来打印54.请注意,所有继续调用始终是尾调用(对printf的调用也是继续调用)。

一个众所周知的例子是asynchronouscallback(例如,在JavaScript中的AJAX调用):您将延续传递给并行执行的例程。

如上例所示,继续可以被组合(并且形成一个monad ,如果你感兴趣的话)。 实际上,可以将一个(function性)程序完全转换为CPS,以便每个调用都是一个尾调用(然后不需要堆栈来运行程序)。