什么是多态lambda?

时间:2013-11-22 19:26:03

标签: function haskell lambda functional-programming polymorphism

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

3 个答案:

答案 0 :(得分:7)

你会发现我在下面的回答中没有多谈及lambdas。请记住,在函数式语言中,任何函数都只是绑定到名称的lambda,所以我对函数的说法会转换为lambdas。

多态性

请注意,多态并不需要那种" dispatch" OO语言通过派生类覆盖虚拟方法来实现。这只是one particular kind of polymorphism, subtyping

多态性本身只是意味着函数不仅允许一种特定类型的参数,而且能够相应地为任何允许的类型采取行动。最简单的例子:你根本不关心这个类型,而只是简单地处理传入的内容。或者,为了使它不是相当如此微不足道,将它包装在单个元素中容器。您可以在C ++中实现这样的功能:

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

但你无法将它作为lambda 实现,因为C ++(写作时间:C ++ 11 )并不支持多态lambda

无类型值

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

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

引用语义的一大优点是多态变得更容易:指针总是具有相同的大小,因此相同的机器代码可以处理对任何类型的引用。这非常丑陋 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

课程&amp;继承

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

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

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

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

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

∀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之前),参数多态并没有给你带来太多好处:因为编译器故意难以定义&#34;只是一个简单的辅助函数&#34;如果只使用类方法,您可以立即从类中获取。但在函数式语言中,定义小辅助函数是最容易想象的事情:lambdas!

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

  

前奏&GT; foldr(+)0 [1,4,6]
  11个
  前奏&GT; foldr(\ x y - &gt; x + y + 1)0 [1,4,6]
  14个
  前奏&GT;让f start = foldr(\ _(xl,xr) - &gt;(xr,xl))开始
  前奏&GT; :t f
  f ::(t,t) - &gt; [a] - &gt; (t,t)
  前奏&GT; f(&#34;左&#34;,&#34;右&#34;)[1]
  (&#34;右&#34;&#34;左&#34)
  前奏&GT; f(&#34;左&#34;,&#34;右&#34;)[1,2]
  (&#34;左&#34;&#34;右&#34)

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

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

1 除了奇怪的显式malloc之外,输入安全等:这样的代码在没有垃圾收集器的语言中非常难以使用,因为有人总是需要清理记忆一旦不再需要,但如果你没有正确地注意是否有人仍然拥有对数据的引用,实际上可能仍然需要它。在Java,Lisp,Haskell中你无需担心......

2 对此有一种完全不同的方法:一种动态语言选择。在这些语言中,每个操作需要确保它适用于任何类型(或者,如果不可能,则引发明确定义的错误)。然后你可以任意组合多态操作,这一方面很好地解决了问题。 (虽然没有像Haskell那样真正聪明的类型系统那么无故障)但OTOH会产生相当大的开销,因为即使是原始操作也需要类型决策和围绕它们的安全措施。

3 我当然在这里不公平。 OO范例不仅仅是类型安全的多态性,它可以实现许多东西,例如:旧的ML与它的Hindler-Milner类型系统无法做到(ad-hoc多态:Haskell有类型类,SML有模块),甚至一些在Haskell中相当困难的东西(主要是,将不同类型的值存储在可变大小的容器中)。但是你越熟悉函数式编程,你对这些东西的需求就越少。

答案 1 :(得分:0)

您是否听说过“多态性lambda”一词?我们可能会更具体。

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

e.g。 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示例)

答案 2 :(得分:0)

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