簡體   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