功能樣式:Lambda函數和映射

一等函數:Lambda函數和映射

什麼是一流的功能?

您之前可能已經聽過它說某種特定的語言是有用的,因爲它具有“一流的功能”。正如我在本系列關於函數式編程的第一篇文章中所說,我不同意這種流行的看法。我同意一流函數是任何函數式語言的基本特性,但我不認爲這是語言功能的充分條件。有很多命令式語言也有此功能。但是,什麼是一流的功能?當函數可以被視爲任何其他值時,函數被描述爲第一類 - 也就是說,它們可以在運行時動態分配給名稱或符號。它們可以存儲在數據結構中,通過函數參數傳入,並作爲函數返回值返回。

這實際上並不是一個新穎的想法。函數指針自1972年開始就成爲C的一個特性。在此之前,過程引用是Algol 68的一個特性,在1970年實現,當時,它們被認爲是一個過程編程特性。回到過去,Lisp(首次在1963年實現)建立在程序代碼和數據可互換的概念之上。

這些也不是模糊的功能。在C中,我們通常使用函數作爲第一類對象。例如,排序時:

char  ** array  =  randomStrings();

printf(“排序前:\ n”);
for(int  s  =  0 ; s  <  NO_OF_STRINGS ; s ++)
    printf(“%s \ n”,array [ s ]);

qsort(array,NO_OF_STRINGS,sizeof(char  *),compare);

printf(“排序後:\ n”);
for(int  s  =  0 ; s  <  NO_OF_STRINGS ; s ++)
    printf(“%s \ n”,array [ s ]);


stdlibC中的庫具有針對不同類型的排序例程的函數集合。所有的人都能夠分揀任何種類的數據的:它們從編程器需要的唯一的協助將被提供,用於比較數據集的兩個元素並返回的功能-11或者0,指示哪個元件比其它或更大他們是平等的。

這基本上是戰略模式!

我們的字符串指針數組的比較器函數可以是:

int  compare(const  void  * a,const  void  * b)
{
    char  * str_a  =  *(char  **)a ;
    char  * str_b  =  *(char  **)b ;
    return  strcmp(str_a,str_b);
}


並且,我們將它傳遞給排序函數,如下所示:

qsort(array,NO_OF_STRINGS,sizeof(char  *),compare);


compare函數名稱上沒有括號使編譯器發出函數指針而不是函數調用。因此,將函數視爲C中的第一類對象非常容易,儘管接受函數指針的函數的簽名非常難看:

qsort(void  * base,size_t  nel,size_t  width,int(* compar)(const  void  *,const  void  *));


函數指針不僅用於排序。早在.NET發明之前,就有用於編寫Microsoft Windows應用程序的Win32 API。在此之前,有Win16 API。它使得函數指針的自由使用可以用作回調。當應用程序需要通知已發生的某些事件時,應用程序在調用窗口管理器時由窗口管理器調用它時提供了指向其自身功能的指針。您可以將此視爲應用程序(觀察者)與其窗口(可觀察對象)之間的觀察者模式關係 - 應用程序接收到諸如鼠標點擊和其窗口上發生的鍵盤按壓等事件的通知。管理窗戶的工作 - 移動它們,將它們堆疊在一起,決定哪個應用程序是用戶操作的接收者 - 在窗口管理器中抽象。應用程序對與其共享環境的其他應用程序一無所知。在面向對象的編程中,我們通常通過抽象類和接口實現這種解耦,但也可以使用第一類函數來實現。

所以,我們一直在使用一流的功能。但是,可以公平地說,沒有任何語言能夠廣泛宣傳作爲一等公民的功能而不是簡單的Javascript。

Lambda表達式

在Javascript中,將函數傳遞給用作回調的其他函數一直是標準做法,就像在Win32 API中一樣。這個想法是HTML DOM的組成部分,其中第一類函數可以作爲事件偵聽器添加到DOM元素:

function  myEventListener(){
    警報(“我被點擊了!”)
}
...
var  myBtn  =  document。getElementById(“myBtn”)
myBtn。addEventListener(“click”,myEventListener)


就像在C中一樣,myEventListener在調用中引用函數名稱時缺少括號addEventListener意味着它不會立即執行。相反,該函數與所click討論的DOM元素上的事件相關聯。單擊該元素時,調用該函數併發出警報。

流行的jQuery庫通過提供一個函數來簡化流程,該函數通過查詢字符串選擇DOM元素,並提供有用的函數來操作元素並向它們添加事件監聽器:

$(“#myBtn”)。click(function(){
    警報(“我被點擊了!”)
})


第一類函數也是實現異步I / O的手段,用於XMLHttpRequest作爲Ajax基礎的對象。同樣的想法在Node.js中也無處不在。當你想進行非阻塞函數調用時,你傳遞一個函數引用,讓它在完成後重新打電話給你。

但是,這裏還有其他的東西。其中第二個不僅僅是一流功能的例子。它也是lambda函數的一個例子。具體來說,這部分:

function(){
    警報(“我被點擊了!”);
}


lambda函數(通常稱爲lambda)是一個未命名的函數。他們本來可以稱他們爲匿名函數,然後每個人都會立即知道它們是什麼。但是,這聽起來並不令人印象深刻,所以lambda的功能就是它!lambda函數的關鍵是你需要在那個地方只有那裏的函數; 因爲在其他地方不需要它,你只需在那裏定義它。它不需要名字。如果您確實需要在其他地方重用它,那麼您可以考慮將其定義爲命名函數並通過名稱引用它,就像我在第一個Javascript示例中所做的那樣。沒有lambda函數,使用jQuery和Node編程確實非常煩人。

Lambda函數以不同的方式用不同的語言定義:

在Javascript中: function(a, b) { return a + b }

在Java中: (a, b) -> a + b

在C#中: (a, b) => a + b

在Clojure中: (fn [a b] (+ a b))

在Clojure中 - 速記版本: #(+ %1 %2)

在Groovy中: { a, b -> a + b }

在F#中: fun a b -> a + b

在Ruby中,所謂的“stabby”語法: -> (a, b) { return a + b }

正如我們所看到的,大多數語言都比Javascript更簡潔地表達lambda。

地圖

您可能已經在編程中使用術語“map”來表示將對象存儲爲鍵值對的數據結構(如果您的語言將其稱爲“字典”,那麼很好 - 沒問題)。在函數式編程中,該術語具有另外的含義。實際上,基本概念實際上是一樣的。在這兩種情況下,一組事物被映射到另一組事物。在數據結構的意義上,地圖是名詞 - 鍵被映射到值。在編程意義上,映射是動詞 - 函數將值數組映射到另一個值數組。

假設你有一個函數f和一個值數組A = [ a1a2a3a4 ]。要映射˚F超過意味着應用˚F在每個元件

  • a1 → fa1)= a1'

  • a2 → fa2)= a2'

  • a3 → fa3)= a3'

  • a4 → fa4)= a4'

然後,按照與輸入相同的順序組合結果數組:

A' = map(fA)= [ a1'a2'a3'a4' ]

按示例地圖

好的,所以這很有趣但有點數學。你多久會這樣做?實際上,它比你想象的要頻繁得多。像往常一樣,一個例子最好地解釋了事情,所以讓我們來看看我在學習Clojure時從exercism.io中提取的一個簡單的練習。這項運動被稱爲“RNA轉錄”,它非常簡單。我們將看一下需要轉換爲輸出字符串的輸入字符串。基地翻譯如下:

  • C→G

  • G→C

  • A→U

  • T→A

除C,G,A,T以外的任何輸入均無效。JUnit5中的測試可能如下所示:

class  TranscriberShould {

    @ParameterizedTest
    @CsvSource({
            “C,G”,
            “G,C”,
            “A,U”,
            “T,A”,
            “ACGTGGTCTTAA,UGCACCAGAAUU”
    })
    void  transcribe_dna_to_rna(String  dna,String  rna){
        var  transcriber  =  new  Transcriber();
        斷言(轉錄者。轉錄(dna),是(rna));
    }

    @測試
    void  reject_invalid_bases(){
        var  transcriber  =  new  Transcriber();
        assertThrows(
                IllegalArgumentException。上課,
                ()- >  抄寫員。轉錄(“XCGFGGTDTTAA”));
    }
}


而且,我們可以通過這個Java實現來完成測試:

class  Transcriber {

    private  Map < Character,Character >  pairs  =  new  HashMap <>();

    Transcriber(){
        對。放('C','G');
        對。put('G','C');
        對。放('A','U');
        對。put('T','A');
    }

    String  transcribe(String  dna){
        var  rna  =  new  StringBuilder();
        對於(VAR  基:DNA。toCharArray()){
            如果(對。的containsKey(基)){
                var  pair  =  pair。得到(基礎);
                rna。追加(對);
            } 其他
                拋出 新的 IllegalArgumentException(“不是基數:”  +  基數);
        }
        返回 rna。toString();
    }
}


不出所料,將功能風格編程的關鍵是將可能表達爲函數的所有內容轉換爲一個函數。所以,讓我們這樣做:

char  basePair(char  base){
    if(pairs。包含Key(base))
        迴歸 對。得到(基礎);
    其他
        拋出 新的 IllegalArgumentException(“不是基礎”  +  基礎);
}

String  transcribe(String  dna){
    var  rna  =  new  StringBuilder();
    對於(VAR  基:DNA。toCharArray()){
        var  pair  =  basePair(base);
        rna。追加(對);
    }
    返回 rna。toString();
}


現在,我們可以將地圖用作動詞。在Java中,Streams API中提供了一個函數:

char  basePair(char  base){
    if(pairs。包含Key(base))
        迴歸 對。得到(基礎);
    其他
        拋出 新的 IllegalArgumentException(“不是基礎”  +  基礎);
}

String  transcribe(String  dna){
    返回 dna。codePoints()
            。mapToObj(c  - >(char)c)
            。地圖(基地 - >  basePair(基地))
            。收集(
                    StringBuilder :: new,
                    StringBuilder :: append,
                    StringBuilder :: append)
            。toString();
}


Hmmmm

所以,讓我們批評這個解決方案。關於它的最好的事情是循環已經消失了。如果你考慮一下,循環是一種文書活動,我們真的不應該在大多數時候關注它。通常,我們循環是因爲我們想爲集合中的每個元素做一些事情。我們真正想要做的是獲取此輸入序列並從中生成輸出序列。Streaming負責爲我們迭代的基本管理工作。事實上,它是一種設計模式 - 一種功能性的設計模式 - 但是,我還沒有提到它的名字。我還不想嚇唬你。

我不得不承認代碼的其餘部分並不是那麼好,主要是因爲Java中的原語不是對象。第一點非偉大是這樣的:

mapToObj(c  - >(char)c)


我們必須這樣做,因爲Java以不同的方式處理原語和對象,雖然該語言確實具有基元的包裝類,但是無法直接從String獲取Character對象的集合。

另一點不那麼令人敬畏的是:

。收集(
        StringBuilder :: new,
        StringBuilder :: append,
        StringBuilder :: append)


很明顯爲什麼有必要再打append兩次電話。我稍後會解釋,但現在時間不對。

我不會試圖捍衛這個代碼 - 它很糟糕。如果有一種方便的方法從String,甚至是一個字符數組中獲取Stream of Character對象,那麼就沒有問題了,但我們並沒有幸運。處理原語並不是Java中FP的最佳選擇。想想看,它對OO編程來說甚至都不好。所以,也許我們不應該如此着迷原始人。如果我們從代碼中設計出來怎麼辦?我們可以爲基數創建一個枚舉:

enum  Base {
    C,G,A,T,U ;
}

而且,我們有一個類作爲一個包含一系列基礎的一流集合:

class  Sequence {

    列出< 基地>  基地 ;

    序列(List < Base >  bases){
        這個。鹼 =  鹼 ;
    }

    Stream < Base >  bases(){
        返回 基地。stream();
    }
}


現在,  Transcriber 看起來像這樣:

class  Transcriber {

    private  Map < Base,Base >  pairs  =  new  HashMap <>();

    Transcriber(){
        對。放(C,G);
        對。放(G,C);
        對。放(A,U);
        對。put(T,A);
    }

    序列 轉錄(序列 dna){
        返回 新的 序列(DNA。基地()
                。map(pairs :: get)
                。collect(toList()));
    }
}


這要好得多。這pairs::get是一個方法參考; 它指的是get分配給pairs變量的實例的方法。通過爲基礎創建類型,我們設計了無效輸入的可能性,因此對該basePair方法的需求消失,異常也是如此。這是Java對Clojure的一個優勢,它本身不能在函數契約中強制執行類型。更重要的是,它StringBuilder也消失了。當您需要迭代集合,以某種方式處理每個元素並構建包含結果的新集合時,Java Streams非常適合。這可能佔你生活中所寫循環的很大一部分。大部分的家務管理都不是真正的工作的一部分,而是爲您完成的。

在Clojure

缺少打字,Clojure比Java版本更簡潔,它給我們映射字符串字符沒有任何困難。Clojure中最重要的抽象是序列; 所有集合類型都可以視爲序列,字符串也不例外:

(def  對 { \ C , “ G” ,
            \ G , “ C” ,
            \ A , “ U” ,
            \ T , “ A” } )

(defn  -base-pair  [ base ]
  (if-let  [ pair  (get  pairs  base )]
    對
    (throw  (IllegalArgumentException。 (str  “ not base:”  base )))))

(定義 轉錄 [ dna ]
  (地圖 基礎對 dna ))


這段代碼的業務結束是最後一行(map base-pair dna)- 值得指出,因爲你可能錯過了它。它表示字符串上mapbase-pair函數dna(表現爲序列)。如果我們希望它返回一個字符串而不是一個列表,這就是map我們所要求的,唯一需要做的改變是:

(應用 str  (map  base-pair  dna ))


在C#中

我們來試試另一種語言。C#中解決方案的必要方法如下所示:

命名空間 RnaTranscription
{
    公共 類 轉錄員
    {
        private  readonly  Dictionary < char,char >  _pairs  =  new  Dictionary < char,char >
        {
            { 'C','G' },
            { 'G','C' },
            { 'A','U' },
            { 'T','A' }
        };

        public  string  Transcribe(string  dna)
        {
            var  rna  =  new  StringBuilder();
            的foreach(炭 b  中 的DNA)
                rna。追加(_pairs [ b ]);
            返回 rna。ToString();
        }
    }
}


同樣,C#沒有向我們展示我們在Java中遇到的問題,因爲C#中的字符串是可枚舉的,並且所有“基元”都可以被視爲具有行爲的對象。

我們可以用更加實用的方式重寫程序,就像這樣,並且它比Java Streams版本要簡單得多。對於Java流中的“map”,請在C#中讀取“select”:

public  string  Transcribe(string  dna)
{
    return  String。加入(“”,dna。選擇(b  =>  _pairs [ b ]));
}


或者,如果您願意,可以使用LINQ作爲其語法糖:

public  string  Transcribe(string  dna)
{
    return  String。加入(“” ,從 b  中 的DNA  選擇 _pairs [ b ]);
}


爲什麼我們循環?

你可能會得到這個想法。如果您想到編寫循環之前的時間,通常您會嘗試完成以下任一操作:

  • 將一種類型的數組映射到另一種類型的數組。

  • 通過查找滿足某個謂詞的數組中的所有項來進行過濾。

  • 確定數組中的任何項目是否滿足某些謂詞。

  • 累積數組中的計數,總和或其他類型的累積結果。

  • 將數組的元素排序爲特定順序。

大多數現代語言中提供的函數式編程功能使您無需編寫循環或創建集合來存儲結果即可完成所有這些操作。功能樣式允許您省去這些內務操作並專注於實際工作。更重要的是,功能樣式允許您將操作鏈接在一起,例如,如果您需要:

  1. 將數組的元素映射到另一種類型。

  2. 過濾掉一些映射的元素。

  3. 對過濾的元素進行排序

在命令式樣式中,這需要多個循環或一個循環,其中包含很多代碼。無論哪種方式,它涉及許多模糊程序真正目的的管理工作。在功能風格中,您可以免除管理工作並直接表達您的意思。稍後,我們將看到更多功能樣式如何讓您的生活更輕鬆的例子。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章