我将此成语称为tuple-continuator或更一般地称为monadic-continuator。它绝对是延续单子的一个实例。对于 C++ 程序员的 continuation monad 的一个很好的介绍在这里。本质上,list
上面的 lambda 接受一个值(可变参数包)并返回一个简单的“延续器”(内部闭包)。当给定一个可调用对象(被称为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
函数。整洁的!
希望这能解释成为延续者的意义。但是成为一个单子意味着什么?这是一个很好的使用图片的介绍。
我认为list
lambda 也是一个列表 monad,它被实现为一个 continuation monad。请注意,continuation monad 是所有 monad 之母。即,您可以使用延续单子实现任何单子。当然,list monad 也不是遥不可及。
由于参数包很自然地是一个“列表”(通常是异构类型),因此它像列表/序列单子一样工作是有意义的。上面的list
lambda 是将 C++ 参数包转换为一元结构的一种非常有趣的方式。因此,操作可以一个接一个地链接起来。
然而,上面的length
lambda 有点令人失望,因为它破坏了 monad,并且里面的嵌套 lambda 只返回一个整数。可以说有一种更好的方法来编写长度“getter”,如下所示。
----函子----
在我们说列表 lambda 是一个 monad 之前,我们必须证明它是一个函子。即fmap必须写成list。
上面的列表 lambda 用作参数包中仿函数的创建者——本质上它用作return
. 创建的仿函数将参数包与自身保持在一起(捕获),并且它允许“访问”它,前提是您提供了一个接受可变数量参数的可调用对象。请注意,可调用对象称为 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]
即,在 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
(aka bind
, selectmany
)
平面图的类型是flatmap: (a -> list[b]) -> list[a] -> list[b].
即,给定一个将 a 映射到 b 列表和 a 列表的函数,flatmap 返回 b 列表。本质上,它从 list-of-a 中获取每个元素,在其上调用 func,一个接一个地接收(可能为空的)list-of-b,然后连接所有 list-of-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 操作,因为它返回单个元素的列表。如果您真的想获得长度(不包含在列表中),您必须终止单子链并获得如下值。
auto len = [](auto ...z) { return sizeof...(z); };
std::cout << list(10, 20, 30)
(flatmap(pair))
(len);
如果操作正确,现在可以将集合管道模式(例如 , filter
)reduce
应用于 C++ 参数包。甜的!
----单子定律----
让我们确保list
单子满足所有三个单子定律。
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));
所有断言都满足。
----采集管道----
尽管上面的 'list' lambda 可以证明是一个 monad 并且具有众所周知的 'list-monad' 的特征,但它是非常令人不快的。特别是,因为常见的收集管道组合器的行为,例如filter
(aka where
) 不符合常见的期望。
原因就是 C++ lambda 是如何工作的。每个 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。为了提供多个 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
最后,现场示例