繁体   English   中英

什么是多态lambda?

[英]What is a polymorphic lambda?

lambdas(匿名函数)的概念对我来说很清楚。 而且我知道类的多态性,运行时/动态调度用于根据实例的派生类型调用适当的方法。 但是lambda究竟能够多态化吗? 我是另一个Java程序员,试图学习更多有关函数式编程的知识。

您将在以下答案中发现我对lambda的讨论不多。 请记住,在函数式语言中,任何函数都只是一个绑定到名称的lambda,因此我对函数的说法转换为lambdas。

多态性

注意,多态实际上并不需要OO语言通过派生类覆盖虚拟方法来实现的“调度”。 这只是一种特殊的多态,即子类型

多态本身仅仅意味着函数不仅允许一种特定类型的参数,而且能够相应地对任何允许的类型起作用。 最简单的例子:你没有为类型不在乎一切,只是把手放在任何或传递,使其没有这么琐碎,在一个单一的元素容器包装它。 您可以在C ++中实现这样的功能:

template<typename T> std::vector<T> wrap1elem( T val ) {
  return std::vector(val);
}

但你不能把它作为一个lambda来实现 ,因为C ++( 写作时间:C ++ 11 )不支持多态lambda。

未输入的值

......至少不是这样,那就是。 C ++模板以一种不同寻常的方式实现多态:编译器实际上为它遇到的所有代码中的任何人传递给函数的每种类型生成一个单态函数。 这是必要的,因为C ++的值语义 :当传入一个值时,编译器需要知道确切的类型(它在内存中的大小,可能的子节点等),以便复制它。

在大多数较新的语言中,几乎所有东西都只是对某个值的引用 ,当你调用一个函数时,它不会获得参数对象的副本,而只是对已经存在的对象的引用。 较旧的语言要求您将参数显式标记为引用/指针类型。

引用语义的一大优点是多态变得更容易:指针总是具有相同的大小,因此相同的机器代码可以处理对任何类型的引用。 这非常丑陋1 ,即使在C中也可以使用多态容器包装器:

typedef struct{
  void** contents;
  int size;
} vector;

vector wrap1elem_by_voidptr(void* ptr) {
  vector v;
  v.contents = malloc(sizeof(&ptr));
  v.contents[0] = ptr;
  v.size = 1;
  return v;
}
#define wrap1elem(val) wrap1elem_by_voidptr(&(val))

在这里, void*只是指向任何未知类型的指针。 由此产生的明显问题是: vector不知道它“包含”了什么类型的元素! 所以你无法对这些对象做任何有用的事情。 除非您确实知道它是什么类型

int sum_contents_int(vector v) {
  int acc = 0, i;
  for(i=0; i<v.size; ++i) {
    acc += * (int*) (v.contents[i]);
  }
  return acc;
}

显然,这非常费力。 如果类型是双重怎么办? 如果我们想要产品而不是总和呢? 当然,我们可以手工编写每个案例。 不太好的解决方案。

如果我们有一个通用函数将指令作为一个额外的参数什么,那么我们会更好! C具有函数指针

int accum_contents_int(vector v, void* (*combine)(int*, int)) {
  int acc = 0, i;
  for(i=0; i<v.size; ++i) {
    combine(&acc, * (int*) (v.contents[i]));
  }
  return acc;
}

那可以像

void multon(int* acc, int x) {
  acc *= x;
}
int main() {
  int a = 3, b = 5;
  vector v = wrap2elems(a, b);
  printf("%i\n", accum_contents_int(v, multon));
}

除了仍然很麻烦之外,所有上述C代码都有一个巨大的问题: 如果容器元素实际上具有正确的类型,则完全不受控制 来自*void的演员阵容会在任何类型上开火,但毫无疑问,结果将是完整的垃圾2

类和继承

该问题是OO语言通过尝试将您可能会正确执行的所有操作与对象中的数据一起捆绑为方法而解决的主要问题之一。 在编译类时,类型是单态的,因此编译器可以检查操作是否有意义。 当您尝试使用这些值时,如果编译器知道如何找到方法就足够了。 特别是,如果你创建一个派生类,编译器知道“啊哈,即使在派生对象上也可以从基类调用该方法”。

不幸的是,这意味着你通过多态实现的所有东西等同于合成数据并简单地在单个字段上调用(单态)方法。 为了实际获得不同类型的不同行为(但可控制 !),OO语言需要虚拟方法。 这相当于基本上该类具有指向方法实现的指针的额外字段,非常类似于我在C示例中使用的combine函数的指针 - 区别在于您只能通过添加派生类来实现重写方法,编译器再次知道所有数据字段的类型等,你是安全的。

复杂的类型系统,检查参数多态性

虽然基于继承的多态性显然有效,但我不禁说它 只是疯了傻 3肯定有点限制。 如果您只想使用一个未实现为类方法的特定操作,则需要制作整个派生类。 即使您只是想以某种方式改变操作,您也需要派生并覆盖稍微不同的方法版本。

让我们重新审视我们的C代码。 从表面上看,我们注意到它应该完全可以使它类型安全,没有任何方法捆绑废话。 我们只需要确保没有丢失类型信息 - 至少在编译期间不会丢失。 想象一下(读∀T为“所有类型T”)

∀T: {
  typedef struct{
    T* contents;
    int size;
  } vector<T>;
}

∀T: {
  vector<T> wrap1elem(T* elem) {
    vector v;
    v.contents = malloc(sizeof(T*));
    v.contents[0] = &elem;
    v.size = 1;
    return v;
  }
}

∀T: {
  void accum_contents(vector<T> v, void* (*combine)(T*, const T*), T* acc) {
    int i;
    for(i=0; i<v.size; ++i) {
      combine(&acc, (*T) (v[i]));
    }
  }
}

观察一下,即使签名看起来很像这篇文章顶部的C ++模板(正如我说的,它实际上只是自动生成的单态代码),但实现实际上实际上只是纯C。没有那里的T值,只是指向它们的指针。 无需编译多个版本的代码:在运行时 ,不需要类型信息,我们只需处理通用指针。 在编译时 ,我们知道类型,并可以使用函数头来确保它们匹配。 即,如果你写的

void evil_sumon (int* acc, double* x) { acc += *x; }

并试图做

vector<float> v; char acc;
accum_contents(v, evil_sumon, acc);

编译器会抱怨,因为类型不匹配:在accum_contents的声明中,它表示类型可能会有所不同, 但是T所有出现都需要解析为相同的类型

这正是参数多态在ML系列语言以及Haskell中的工作原理:函数实际上并不知道他们正在处理的多态数据。 但他们被赋予了具有这种知识的专业运营商作为论据

在Java之类的语言(lambdas之前的语言)中,参数多态性不会给您带来什么好处:由于编译器故意定义“仅是一个简单的辅助函数”,而仅使用类方法,因此很难定义它,因此您可以简单地进行派生从一流的方式马上。 但是在函数式语言中,定义小的辅助函数是最容易想到的事情:lambdas!

所以你可以在Haskell中做出令人难以置信的简洁代码:

前奏>文件夹(+)0 [1,4,6]
11
前奏> foldr(\\ xy - > x + y + 1)0 [1,4,6]
14
Prelude>让f start = foldr(\\ _(xl,xr)->(xr,xl))开始
序曲>:tf
f ::(t,t)-> [a]->(t,t)
前奏> f(“左”,“右”)[1]
(“右左”)
前奏> f(“左”,“右”)[1,2]
(“左右”)

请注意我在lambda中定义为f的帮助器,我对xlxr的类型没有任何线索,我只想交换这些元素的元组,这些元素要求类型相同 所以这将是一个多态的lambda,与类型

\_ (xl, xr) -> (xr, xl)   ::  ∀ a t.  a -> (t,t) -> (t,t)

1除了奇怪的显式malloc东西,类型安全等:这样的代码在没有垃圾收集器的语言中非常难以使用,因为有人总是需要在不再需要时清理内存,但如果你不看正确确定是否有人仍然持有对数据的引用,实际上可能仍然需要它。 在Java,Lisp,Haskell中你无需担心...

2对此有一种完全不同的方法:一种动态语言选择。 在这些语言中, 每个操作都需要确保它适用于任何类型(或者,如果不可能,则提出明确定义的错误)。 然后,您可以任意地编写多态操作,一方面,它非常“麻烦”(虽然不像像Haskell这样的真正聪明的类型系统那样麻烦),但是OTOH会产生相当大的开销,因为即使原始操作也需要类型决策和围绕它们的保障措施。

3我当然在这里不公平。 OO范式不仅具有类型安全的多态性,它还具有很多其他功能,例如,使用Hindler-Milner类型系统无法做到的许多事情,例如老ML(即席多态性:Haskell具有类型类,SML具有模块),甚至在Haskell中有些非常困难的事情(主要是在可变大小的容器中存储不同类型的值)。 但是你越习惯于函数式编程,你对这些东西的需求就越少。

你有没有听过“多态性lambda”这个词的背景? 我们可能会更具体。

Lambda可以是多态的最简单方法是接受其类型与最终结果(部分不相关)的参数。

例如lambda

\(head:tail) -> tail

具有类型[a] -> [a] -例如,它在列表的内部类型中是多态的。

其他简单的例子就像是

\_ -> 5      :: Num n => a -> n
\x f -> f x  :: a -> (a -> b) -> b
\n -> n + 1  :: Num n => n -> n

等等

(请注意涉及类型类分派的Num n示例)

在C ++中,多态(或通用)lambda从C ++ 14开始是一个lambda,可以将任何类型作为参数。 基本上它是一个具有auto参数类型的auto lambda = [](auto){};auto lambda = [](auto){};

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM