在Boost mailinglist上,@ LouisDionne最近发布了以下创建类似元组的实体的巧妙技巧:
#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,它将一个可变参数列表作为输入,并返回一个lambda作为输出,将另一个lambda作用于其输入。类似地,length
是一个lambda,它采用类似列表的实体,它将向列表的原始输入参数提供可变sizeof...
运算符。 sizeof...
运算符包含在lambda中,以便可以传递给list
。
问题:这个元组创作成语是否有名称?也许来自函数式编程语言,其中更常用的是高阶函数。
答案 0 :(得分:31)
我认为这是一个类似Monad的东西的微妙实现,特别是与延续monad相同的精神。
Monads是一种函数式编程结构,用于模拟计算的不同步骤之间的状态(请记住,函数式语言是无状态的)。
monad所做的是链接不同的函数,创建一个“计算管道”,其中每个步骤都知道计算的当前状态。
Monads有两个主要的pilars:
The Wikipedia有很好的关于monad的例子和解释。
让我重写给定的C ++ 14代码:
auto list = []( auto... xs )
{
return [=]( auto access ) { return access(xs...); };
};
我认为在这里我们确定monad的return
函数:获取值并以Monadic方式返回它。
具体来说,此返回返回一个仿函数(在数学意义上,不是C ++仿函数),它从“元组”类别变为可变参数包类别。
auto pack_size = [](auto... xs ) { return sizeof...(xs); };
pack_size
只是一个正常的功能。它将在管道中用于做一些工作。
auto bind = []( auto xs , auto op )
{
return xs(op);
};
并且length
只是monad bind
运算符附近的非泛型版本,运算符从前一个管道步骤获取monadic值,并将其绕过指定的函数(真正完成工作的功能)。该功能是此计算步骤完成的功能。
最后你的电话可以改写为:
auto result = bind(list(1,'2',"3"), pack_size);
那么, 这个元组创作成语的名称是什么? 嗯,我认为这可以称为“ monad-like tuples ”,因为它不完全是monad,但是元组表示和扩展以类似的方式工作,留在Haskell延续monad中。
为了摆脱有趣的C ++编程,我跟着探索了这个类似Monad的东西。你可以找到一些例子here。
答案 1 :(得分:18)
我会称之为成语 tuple-continuator 或更常见的是 monadic-continuator 。它绝对是一个延续monad的实例。对于C ++程序员来说,继续monad的一个很好的介绍是here。从本质上讲,上面的list
lambda取一个值(一个可变参数包)并返回一个简单的&#39;延续器&#39; (内部封闭)。当给定可调用(称为access
)时,此继承器将参数包传递给它并返回可调用返回的任何内容。
借用FPComplete博客帖子,延续器或多或少如下。
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
接受A
类型的一个值,并在调用access
时将其传递给andThen
。上面的list
lambda基本相同。它更通用。内部闭包不是单个值,而是捕获参数包并将其传递给access
函数。整齐!
希望这能解释成为延续者意味着什么。但是成为一个单子是什么意思?这是一个很好的introduction使用图片。
我认为list
lambda也是一个列表monad,它被实现为continuation monad。请注意continuation monad is the mother of all monads。即,您可以使用continuation monad实现任何monad。当然,列表monad并非遥不可及。
作为参数包,很自然地是一个列表&#39; (通常是异构类型),它就像列表/序列monad一样工作是有意义的。上面的list
lambda是一种将C ++参数包转换为monadic结构的非常有趣的方法。因此,操作可以一个接一个地链接。
然而,上面的length
lambda有点令人失望,因为它打破了monad并且嵌套的lambda里面只返回一个整数。可以说有一个更好的方法来写长度&#39; getter&#39;如下图所示。
<强> ---- ----函子强>
在我们可以说列表lambda是monad之前,我们必须证明它是一个仿函数。即,必须为列表编写fmap。
上面的列表lambda作为参数包中仿函数的创建者 - 实际上它充当return
。创建的仿函数保持参数包本身(捕获),它允许访问&#39;如果你提供一个接受可变数量的参数的callable。请注意,callable称为EXACTLY-ONCE。
让我们为这样的仿函数写fmap。
auto fmap = [](auto func) {
return [=](auto ...z) { return list(func(z)...); };
};
func的类型必须是(a - > b)。即,在C ++中说,
template <class a, class b>
b func(a);
fmap的类型是fmap: (a -> b) -> list[a] -> list[b]
即I ++,在C ++中说,
template <class a, class b, class Func>
list<b> fmap(Func, list<a>);
即,fmap只是将list-of-a映射到list-of-b。
现在你可以做到
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
(a.k.a。bind
,selectmany
)
平面地图的类型为flatmap: (a -> list[b]) -> list[a] -> list[b].
即,给定一个将a映射到list-of-b和list-of-a的函数,flatmap返回list-of-b。本质上,它从list-of-a中获取每个元素,在其上调用func,逐个接收(可能为空)list-of-b,然后连接所有list-of-b,最后返回最终列表-of-b中。
这是list的flatmap实现。
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);
如果操作正确,collection pipeline模式(例如filter
,reduce
)现在可以应用于C ++参数包。甜!
---- Monad Laws ----
让我们确保list
monad满足所有三个monad laws。
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));
所有断言都得到满足。
----收集管道----
虽然上面列出了&#39; lambda可证明是一个monad并且具有众所周知的monad&mon;这是非常不愉快的。特别是,因为常见collection pipeline组合子的行为,例如filter
(a.k.a where
)不符合共同的期望。
原因在于C ++ lambdas是如何工作的。每个lambda表达式都会生成一个唯一类型的函数对象。因此,list(1,2,3)
会生成与list(1)
无关的类型和一个空列表,在本例中为list()
。
where
的直接实现无法编译,因为在C ++中,函数不能返回两种不同的类型。
auto where_broken = [](auto func) {
return flatmap([func](auto i) {
return func(i)? list(i) : list(); // broken :-(
});
};
在上面的实现中,func返回一个布尔值。它是一个谓词,表示每个元素的真或假。 ?:运算符无法编译。
因此,可以使用不同的技巧来继续收集管道。它们不是实际过滤元素,而是简单地标记为 - 这就是令它不愉快的原因。
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
,where
只接受一个回调lambda。为了提供多个lambdas,每个lambda都处理一种类型,我们可以重载它们。例如,
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
最后, Live Example
答案 2 :(得分:3)
这看起来像是一种continuation passing style。
CPS的粗略思想是:不是让函数(比如f
)返回一些值,而是给f
另一个参数,这是一个函数,称为 continuation 。然后,f
使用返回值调用此延续而不是返回。让我们举一个例子:
int f (int x) { return x + 42; }
变为
void f (int x, auto cont) { cont (x + 42); }
调用是一个尾调用,可以优化为跳转(这就是为什么TCO在某些语言中被强制要求,比如Scheme,其语义依赖于某种形式的转换为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
的调用也是延续调用)。< / p>
一个众所周知的例子是异步回调(例如javascript中的AJAX调用):你将延续传递给并行执行的例程。
如上例所示,可以组合延续(和form a monad,以防您感兴趣)。实际上it is possible将一个(功能)程序完全转换为CPS,这样每个调用都是尾调用(然后你不需要堆栈来运行程序!)。