这个元组创作成语有没有名字?
在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 bind
, selectmany
)
平面图的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道模式(例如filter
, reduce
)现在可以应用于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
—-异构元组—-
到目前为止,讨论关于同构元组。 现在让我们把它推广到真正的元组。 但是, fmap
, flatmap
只需要一个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,以便每个调用都是一个尾调用(然后不需要堆栈来运行程序)。