这个元组创建习惯用法有一个名称吗?

65

最近在Boost邮件列表上,@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,它将提供 variadic sizeof... 运算符到列表的原始输入参数。这个 sizeof... 运算符被包裹在一个 lambda 中,以便可以将其传递给 list

问题: 是否有一个名称来表示这种 tuple-creation 惯用语?也许源自一个更常使用高阶函数的函数式编程语言。


5
我认为这个东西没有一个特定的名称,但你可能想查看λ演算。它们使用λ来定义各种东西,但那里面没有可变参数。你的例子有点像Church对( http://en.wikipedia.org/wiki/Church_encoding#Church_pairs ),所以你可以称之为Church元组。 - zch
2
我再次阅读了代码。你的list是一个单子,对吧?一个期望其他函数完成计算的函数。http://www.haskell.org/haskellwiki/Monad - Manu343726
1
暂时忽略元组。然后是 List:X->(X->Y)->Y。这应该更容易找到。 - Yakk - Adam Nevraumont
1
你能展示一下这个习语的更有用的应用吗?对我来说,它看起来完全毫无意义和用处,我不知道一个有用的例子可能是什么。 - user541686
这是一种续延传递风格的形式(list函数返回一个带有续延参数的函数)。 - Alexandre C.
显示剩余4条评论
3个回答

31

我认为这是一种微妙的Monad-like实现,具体来说,是与continuation monad类似精神的东西。

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

Monad有两个主要的支柱:

  • return函数:将值采用Monad-ready形式返回
  • bind函数:采用Monad-ready值(从前面的流水线步骤)并解包它以将该值传递给下一步。

维基百科上有关于monads非常好的例子和解释。

让我重新编写给定的C++14代码:

auto list = []( auto... xs ) 
{ 
    return [=]( auto access ) { return access(xs...); };
};

我认为在这里我们确定了单子的return函数:以单子的方式获取值并返回它。 具体来说,这个return返回一个函子(在数学意义上,不是C ++函子),它从“元组”类别转换为可变参数包类别。

auto pack_size = [](auto... xs ) { return sizeof...(xs); };

pack_size只是一个普通函数。它将被用在一个管道中来完成一些工作。

auto bind = []( auto xs , auto op ) 
{
    return xs(op);
};

length只是某个接近单子bind操作符的非范型版本,该操作符从前一步流水线中获取单子值,并将其传递给指定函数(实际执行工作的功能)。该函数是这个计算步骤所完成的功能。

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

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

那么,这种元组创建习惯用语叫什么呢?我认为可以称之为“类单子元组”,因为它不完全是一个单子,但元组的表示和展开方式类似,并保持了对Haskell延续单子的参考。

编辑:更有趣的东西

为了有趣的C++编程体验,我一直在探索这个类单子元组。你可以在这里找到一些示例。


这个单子有特殊的名称吗? - TemplateRex
8
尽管可能是符合单子的实例,当提供绑定函数时,它肯定不是唯一的单子,也不以任何方式代表所有单子,因此我认为“单子”不是这种习语的正确名称。其次,任何列表实现都可以通过适当的bindreturn成为单子。最后,你的bind似乎甚至没有正确的类型。例如,它似乎不能被链接起来。 - zch
1
如果 length 不返回相同的单子实例,那么它如何是 bind 的非泛型版本呢? - R. Martinho Fernandes
我不知道为什么你一直没有在答案中写下这样的更正。但是,那时候,它对于问题“是否有一个名称来表示这个元组创建习惯用语?”还有用吗? - R. Martinho Fernandes
@R.MartinhoFernandes 如我之前所评论的,这个东西在技术上并不是一个Monad,但类似于Monad。具体来说,正如Sumant在他的答案中解释的那样,这件事可以被解释为一个(不完整的)Continuation Monad。 - Manu343726
显示剩余3条评论

19
我会称这个习语为元组续接器或更普遍地说,单子续接器。它绝对是一个续接单子的实例。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);

如果做得正确,集合管道模式(例如filterreduce)现在可以应用于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(); // broken :-(
  }); 
};

在上面的实现中,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

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

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

最终,实时示例

“然后合并所有列表[b];” 这里的“合并”具体指什么?连接?联合?还是其他什么? - celtschk
你要么想在 Return 的位置写入 SimpleContinuator,要么想在 SimpleContinuator 的位置写入 Return。换句话说,不能将名为 SimpleContinuator 的类具有名为 Return 的构造函数,或者名为 Return 的构造函数的类不能被命名为 SimpleContinuator。请进行修正以避免混淆。 - Nawaz
这个答案已经过时,只有大约75%的正确性。要获得正确、更完整的答案,请访问:http://cpptruths.blogspot.com/2014/08/fun-with-lambdas-c14-style-part-3.html - Sumant

3
这似乎是一种延续传递风格

CPS的基本思想是:不要让一个函数(比如说f)返回某个值,而是给f另一个参数,这个参数是一个函数,称为“延续”(continuation)。然后,f调用这个延续,并将返回值作为参数传递给它,而不是直接返回。我们来看一个例子:

int f (int x) { return x + 42; }

变成

void f (int x, auto cont) { cont (x + 42); }

这个调用是尾调用,可以被优化成跳转指令(这就是为什么某些语言,如Scheme要求TCO的原因,因为它的语义依赖于一些形式的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调用):你向在并行执行的例程中传递一个续传。

续传可以组合(并且如果你感兴趣,可以形成一个单子), 就像上面的例子一样。实际上,可能将(函数式)程序完全转换为CPS,这样每次调用都是尾调用(然后你不需要堆栈来运行程序!)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接