【问题标题】:Is there a name for this tuple-creation idiom?这个创建元组的习语有名字吗?
【发布时间】:2014-10-09 22:04:22
【问题描述】:

@LouisDionne 最近在Boost mailinglist 上发布了以下创建类元组实体的巧妙技巧:

#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    
}

Live Example.

聪明之处在于list 是一个将可变参数列表作为输入的 lambda,并返回一个 lambda 作为输出,该输出将采用另一个 lambda 对其输入进行操作。类似地,length 是一个 lambda,它采用类似列表的实体,它将可变参数 sizeof... 运算符提供给列表的原始输入参数。 sizeof... 运算符被包装在一个 lambda 中,以便可以将其传递给 list

问题:这个创建元组的习语有名字吗?也许来自更常用高阶函数的函数式编程语言。

【问题讨论】:

  • 我认为没有具体的名称,但您可能想查看 lambda 演算。他们只使用 lambdas 定义了各种各样的东西,但那里没有可变参数。你的例子有点像 Church Pair (en.wikipedia.org/wiki/Church_encoding#Church_pairs),所以我猜你可以称它为 Church tuple?
  • @zch 我有点想起你的tmp cartesian product技巧
  • 我又在读代码了。你的 list 是一个单子,不是吗?期望其他函数完成计算的函数haskell.org/haskellwiki/Monad
  • 暂时忽略元组。然后List:X-&gt;(X-&gt;Y)-&gt;Y。哪个应该更容易找到。
  • 您能否展示这个成语的更有用的应用?对我来说,这看起来完全没有意义/没用,对我来说,一个有用的例子可能是什么并不明显。

标签: c++ tuples variadic-templates c++14 generic-lambda


【解决方案1】:

我认为这是一个类似 Monad 的东西的微妙实现,特别是与延续 monad 相同的东西。

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

Monad 有两个主要支柱:

  • 一个返回函数,它接受一个值并以 Monad 就绪的形式返回它。
  • 一个绑定函数,它采用 Monad 就绪值(来自上一个管道步骤)并将其解包到其原始 from 以将值传递到下一步。

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 运算符附近的某个东西的非泛型版本,该运算符从前一个管道步骤中获取一个单子值,并将其绕过到指定的函数(真正的函数工作)。该函数就是这个计算步骤完成的功能。

最后你的调用可以改写为:

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

那么,这个元组创建习语叫什么名字?好吧,我想这可以称为“monad-like tuples”,因为它不完全是 monad,但元组表示和扩展以类似的方式工作,保留到 Haskell 延续 monad。

编辑:更有趣

只是为了让有趣的 C++ 编程振作起来,我一直在探索这个类似 monad 的东西。你可以找到一些例子here

【讨论】:

  • 这个单子有特殊的名字吗?
  • 虽然在提供绑定函数时这可能是巧合的一个 monad 实例,但它肯定不是唯一的 monad,也不以任何方式代表所有 monad,所以我不认为 'monad ' 是这个成语的正确名称。其次,任何列表实现都可以用适当的bindreturn 做成一个monad。最后,您的 bind 似乎连类型都不正确。例如,它似乎不能被链接。
  • @zch 当然,这不完全是一个 monad,我在这里试图解释的是,这个东西具有相同的 monad 微妙形式,并且(作为 monad)可以完全用于通用方式。这里的 gem 是“monad”的返回函数,它允许创建一个元组,传递它,直接提取它的内容,没有索引技巧。这个不错
  • @zch 是延续单子的体现。因此,它绝对是“所有单子”
  • 如果length 不返回相同monad 的实例,bind 的非泛型版本如何?
【解决方案2】:

我将这个成语称为 tuple-continuator 或更一般地称为 monadic-continuator。它绝对是延续单子的一个实例。对于 C++ 程序员的 continuation monad 的一个很好的介绍是 here。本质上,上面的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 函数。整洁!

希望这能解释成为延续者的意义。但是成为一个单子意味着什么?这是一个很好的introduction使用图片。

我认为list lambda 也是一个列表单子,它被实现为一个延续单子。请注意continuation monad is the mother of all monads。即,您可以使用 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 -&gt; b) -&gt; list[a] -&gt; 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)

因此,它是一个函子。

----Monad----

现在,让我们尝试写一个flatmap(又名bindselectmany

平面图类型为flatmap: (a -&gt; list[b]) -&gt; list[a] -&gt; 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);

如果操作正确,collection pipeline 模式(例如,filterreduce)现在可以应用于 C++ 参数包。甜蜜!

----单子定律----

让我们确保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));

所有断言都满足。

----收集管道----

虽然上面的 'list' lambda 可以证明是一个 monad 并且具有众所周知的 'list-monad' 的特征,但它是相当不愉快的。特别是,因为常见的collection pipeline 组合子的行为,例如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(); // 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

----异构元组----

到目前为止,讨论的是同质元组。现在让我们将其推广到真正的元组。但是,fmapflatmapwhere 只接受一个回调 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

最后,Live Example

【讨论】:

  • "然后合并所有列表[b];"这里的“合并”到底是什么意思?加入?联盟?还是别的什么?
  • 要么你想写SimpleContinuator 代替Return ,要么Return 代替SimpleContinuator。换句话说,一个名为SimpleContinuator 的类不能有一个名为Return 的构造函数。或者构造函数为Return 的类不能命名为SimpleContinuator。请解决此问题以避免混淆。
  • 这个答案已经过时了,只有大约 75% 的正确率。如需正确、更完整的答案,请访问:cpptruths.blogspot.com/2014/08/…
【解决方案3】:

这看起来像是continuation passing style 的一种形式。

CPS 的大致思路是这样的:不是让函数(比如f)返回一些值,而是给f 另一个参数,它是一个函数,称为延续 .然后,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 的调用也是延续调用)。

一个众所周知的例子是异步回调(例如 javascript 中的 AJAX 调用):您将延续传递给并行执行的例程。

可以像上面的例子一样组合延续(和form a monad,如果你有兴趣的话)。事实上it is possible 将(函数式)程序完全转换为 CPS,因此每次调用都是尾调用(然后您不需要堆栈来运行程序!)。

【讨论】:

    猜你喜欢
    • 2017-07-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-30
    • 1970-01-01
    • 2011-01-05
    • 2013-04-08
    • 2011-07-27
    相关资源
    最近更新 更多