我会称这个习语为
元组续接器或更普遍地说,
单子续接器。它绝对是一个续接单子的实例。C++程序员的续接单子的绝佳介绍可以在
这里找到。本质上,上面的
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
的值,并在调用
andThen
时将其传递给
access
。上面的
list
lambda本质上是相同的,但更通用。内部闭包捕获参数包并将其传递给
access
函数。太棒了!
希望这解释了什么是continuator。但是什么是monad呢?这里有一篇好的
introduction使用图片。
我认为
list
lambda也是列表monad,它被实现为continuation monad。请注意,
continuation monad is the mother of all monads。也就是说,你可以用continuation monad来实现任何monad。当然,列表monad并不难实现。
作为参数包,它自然是一个“列表”(通常由异构类型组成),因此让它像列表/序列单子一样工作是有意义的。上面的
list
lambda是将C++参数包转换为单子结构的一种非常有趣的方式。因此,操作可以一个接一个地链接起来。
然而,上面的
length
lambda有点令人失望,因为它破坏了单子,内部嵌套的lambda只返回一个整数。如下所示,可能有更好的方法来编写长度“getter”。
----函子----
在我们说列表lambda是一个单子之前,我们必须证明它是一个函子。即,对于列表必须编写fmap。
上面的列表lambda充当从参数包创建函子的创建者-本质上它充当
return
。创建的函子保留了参数包并允许“访问”它,前提是您提供一个可调用的函数,该函数接受可变数量的参数。请注意,可调用函数仅被调用一次。
让我们为这样的函子编写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 仅将 a 列表映射到 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)
因此,它是一个函数子。
----Monad----
现在,让我们尝试编写flatmap(又名bind、selectmany)。
flatmap的类型为flatmap: (a -> list[b]) -> list[a] -> list[b]。
也就是说,给定一个将a映射到b列表和a列表的函数,flatmap返回一个b列表。基本上,它从a列表中取出每个元素,调用该函数,在逐个接收(可能为空的)b列表的同时,连接所有b列表,最后返回最终的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.
计数函数是一种保留单一元素列表的单子操作。如果您真的想获得长度(不包含在列表中),则必须终止单子链并按以下方式获取值。
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可证明是一种单子,并且具有俗称的“列表单子”的特征,但它相当不友好。特别是,常见的集合管道组合器的行为(例如filter
(又名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();
});
};
在上面的实现中,func返回一个布尔值。它是一个谓词,为每个元素指定true或false。?: 运算符无法编译。
因此,可以使用不同的技巧来允许集合管道的继续。而不是实际过滤元素,它们只是被标记为这样-这就是使其不愉快的原因。
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,每个 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
最终,
实时示例
list
是一个单子,对吧?一个期望其他函数完成计算的函数。http://www.haskell.org/haskellwiki/Monad - Manu343726List:X->(X->Y)->Y
。这应该更容易找到。 - Yakk - Adam Nevraumontlist
函数返回一个带有续延参数的函数)。 - Alexandre C.