溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

功能樣式:Lambda函數(shù)和映射

發(fā)布時(shí)間:2020-07-19 16:43:12 來源:網(wǎng)絡(luò) 閱讀:449 作者:二哈少爺 欄目:安全技術(shù)

一等函數(shù):Lambda函數(shù)和映射

什么是一流的功能?

您之前可能已經(jīng)聽過它說某種特定的語(yǔ)言是有用的,因?yàn)樗哂小耙涣鞯墓δ堋?。正如我在本系列關(guān)于函數(shù)式編程的第一篇文章中所說,我不同意這種流行的看法。我同意一流函數(shù)是任何函數(shù)式語(yǔ)言的基本特性,但我不認(rèn)為這是語(yǔ)言功能的充分條件。有很多命令式語(yǔ)言也有此功能。但是,什么是一流的功能?當(dāng)函數(shù)可以被視為任何其他值時(shí),函數(shù)被描述為第一類 - 也就是說,它們可以在運(yùn)行時(shí)動(dòng)態(tài)分配給名稱或符號(hào)。它們可以存儲(chǔ)在數(shù)據(jù)結(jié)構(gòu)中,通過函數(shù)參數(shù)傳入,并作為函數(shù)返回值返回。

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

這些也不是模糊的功能。在C中,我們通常使用函數(shù)作為第一類對(duì)象。例如,排序時(shí):

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中的庫(kù)具有針對(duì)不同類型的排序例程的函數(shù)集合。所有的人都能夠分揀任何種類的數(shù)據(jù)的:它們從編程器需要的唯一的協(xié)助將被提供,用于比較數(shù)據(jù)集的兩個(gè)元素并返回的功能-1,1或者0,指示哪個(gè)元件比其它或更大他們是平等的。

這基本上是戰(zhàn)略模式!

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

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


并且,我們將它傳遞給排序函數(shù),如下所示:

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


compare函數(shù)名稱上沒有括號(hào)使編譯器發(fā)出函數(shù)指針而不是函數(shù)調(diào)用。因此,將函數(shù)視為C中的第一類對(duì)象非常容易,盡管接受函數(shù)指針的函數(shù)的簽名非常難看:

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


函數(shù)指針不僅用于排序。早在.NET發(fā)明之前,就有用于編寫Microsoft Windows應(yīng)用程序的Win32 API。在此之前,有Win16 API。它使得函數(shù)指針的自由使用可以用作回調(diào)。當(dāng)應(yīng)用程序需要通知已發(fā)生的某些事件時(shí),應(yīng)用程序在調(diào)用窗口管理器時(shí)由窗口管理器調(diào)用它時(shí)提供了指向其自身功能的指針。您可以將此視為應(yīng)用程序(觀察者)與其窗口(可觀察對(duì)象)之間的觀察者模式關(guān)系 - 應(yīng)用程序接收到諸如鼠標(biāo)點(diǎn)擊和其窗口上發(fā)生的鍵盤按壓等事件的通知。管理窗戶的工作 - 移動(dòng)它們,將它們堆疊在一起,決定哪個(gè)應(yīng)用程序是用戶操作的接收者 - 在窗口管理器中抽象。應(yīng)用程序?qū)εc其共享環(huán)境的其他應(yīng)用程序一無(wú)所知。在面向?qū)ο蟮木幊讨?,我們通常通過抽象類和接口實(shí)現(xiàn)這種解耦,但也可以使用第一類函數(shù)來實(shí)現(xiàn)。

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

Lambda表達(dá)式

在Javascript中,將函數(shù)傳遞給用作回調(diào)的其他函數(shù)一直是標(biāo)準(zhǔn)做法,就像在Win32 API中一樣。這個(gè)想法是HTML DOM的組成部分,其中第一類函數(shù)可以作為事件偵聽器添加到DOM元素:

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


就像在C中一樣,myEventListener在調(diào)用中引用函數(shù)名稱時(shí)缺少括號(hào)addEventListener意味著它不會(huì)立即執(zhí)行。相反,該函數(shù)與所click討論的DOM元素上的事件相關(guān)聯(lián)。單擊該元素時(shí),調(diào)用該函數(shù)并發(fā)出警報(bào)。

流行的jQuery庫(kù)通過提供一個(gè)函數(shù)來簡(jiǎn)化流程,該函數(shù)通過查詢字符串選擇DOM元素,并提供有用的函數(shù)來操作元素并向它們添加事件監(jiān)聽器:

$(“#myBtn”)。click(function(){
    警報(bào)(“我被點(diǎn)擊了!”)
})


第一類函數(shù)也是實(shí)現(xiàn)異步I / O的手段,用于XMLHttpRequest作為Ajax基礎(chǔ)的對(duì)象。同樣的想法在Node.js中也無(wú)處不在。當(dāng)你想進(jìn)行非阻塞函數(shù)調(diào)用時(shí),你傳遞一個(gè)函數(shù)引用,讓它在完成后重新打電話給你。

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

function(){
    警報(bào)(“我被點(diǎn)擊了!”);
}


lambda函數(shù)(通常稱為lambda)是一個(gè)未命名的函數(shù)。他們本來可以稱他們?yōu)槟涿瘮?shù),然后每個(gè)人都會(huì)立即知道它們是什么。但是,這聽起來并不令人印象深刻,所以lambda的功能就是它!lambda函數(shù)的關(guān)鍵是你需要在那個(gè)地方只有那里的函數(shù); 因?yàn)樵谄渌胤讲恍枰?,你只需在那里定義它。它不需要名字。如果您確實(shí)需要在其他地方重用它,那么您可以考慮將其定義為命名函數(shù)并通過名稱引用它,就像我在第一個(gè)Javascript示例中所做的那樣。沒有l(wèi)ambda函數(shù),使用jQuery和Node編程確實(shí)非常煩人。

Lambda函數(shù)以不同的方式用不同的語(yǔ)言定義:

在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”語(yǔ)法: -> (a, b) { return a + b }

正如我們所看到的,大多數(shù)語(yǔ)言都比Javascript更簡(jiǎn)潔地表達(dá)lambda。

地圖

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

假設(shè)你有一個(gè)函數(shù)f和一個(gè)值數(shù)組A = [ a1,a2a3,a4 ]。要映射?F超過意味著應(yīng)用?F在每個(gè)元件

  • a1 → fa1)= a1'

  • a2 → fa2)= a2'

  • a3 → fa3)= a3'

  • a4 → fa4)= a4'

然后,按照與輸入相同的順序組合結(jié)果數(shù)組:

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

按示例地圖

好的,所以這很有趣但有點(diǎn)數(shù)學(xué)。你多久會(huì)這樣做?實(shí)際上,它比你想象的要頻繁得多。像往常一樣,一個(gè)例子最好地解釋了事情,所以讓我們來看看我在學(xué)習(xí)Clojure時(shí)從exercism.io中提取的一個(gè)簡(jiǎn)單的練習(xí)。這項(xiàng)運(yùn)動(dòng)被稱為“RNA轉(zhuǎn)錄”,它非常簡(jiǎn)單。我們將看一下需要轉(zhuǎn)換為輸出字符串的輸入字符串?;胤g如下:

  • C→G

  • G→C

  • A→U

  • T→A

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

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();
        斷言(轉(zhuǎn)錄者。轉(zhuǎn)錄(dna),是(rna));
    }

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


而且,我們可以通過這個(gè)Java實(shí)現(xiàn)來完成測(cè)試:

class  Transcriber {

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

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

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


不出所料,將功能風(fēng)格編程的關(guān)鍵是將可能表達(dá)為函數(shù)的所有內(nèi)容轉(zhuǎn)換為一個(gè)函數(shù)。所以,讓我們這樣做:

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

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


現(xiàn)在,我們可以將地圖用作動(dòng)詞。在Java中,Streams API中提供了一個(gè)函數(shù):

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

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


Hmmmm

所以,讓我們批評(píng)這個(gè)解決方案。關(guān)于它的最好的事情是循環(huán)已經(jīng)消失了。如果你考慮一下,循環(huán)是一種文書活動(dòng),我們真的不應(yīng)該在大多數(shù)時(shí)候關(guān)注它。通常,我們循環(huán)是因?yàn)槲覀兿霝榧现械拿總€(gè)元素做一些事情。我們真正想要做的是獲取此輸入序列并從中生成輸出序列。Streaming負(fù)責(zé)為我們迭代的基本管理工作。事實(shí)上,它是一種設(shè)計(jì)模式 - 一種功能性的設(shè)計(jì)模式 - 但是,我還沒有提到它的名字。我還不想嚇唬你。

我不得不承認(rèn)代碼的其余部分并不是那么好,主要是因?yàn)镴ava中的原語(yǔ)不是對(duì)象。第一點(diǎn)非偉大是這樣的:

mapToObj(c  - >(char)c)


我們必須這樣做,因?yàn)镴ava以不同的方式處理原語(yǔ)和對(duì)象,雖然該語(yǔ)言確實(shí)具有基元的包裝類,但是無(wú)法直接從String獲取Character對(duì)象的集合。

另一點(diǎn)不那么令人敬畏的是:

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


很明顯為什么有必要再打append兩次電話。我稍后會(huì)解釋,但現(xiàn)在時(shí)間不對(duì)。

我不會(huì)試圖捍衛(wèi)這個(gè)代碼 - 它很糟糕。如果有一種方便的方法從String,甚至是一個(gè)字符數(shù)組中獲取Stream of Character對(duì)象,那么就沒有問題了,但我們并沒有幸運(yùn)。處理原語(yǔ)并不是Java中FP的最佳選擇。想想看,它對(duì)OO編程來說甚至都不好。所以,也許我們不應(yīng)該如此著迷原始人。如果我們從代碼中設(shè)計(jì)出來怎么辦?我們可以為基數(shù)創(chuàng)建一個(gè)枚舉:

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

而且,我們有一個(gè)類作為一個(gè)包含一系列基礎(chǔ)的一流集合:

class  Sequence {

    列出< 基地>  基地 ;

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

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


現(xiàn)在,  Transcriber 看起來像這樣:

class  Transcriber {

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

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

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


這要好得多。這pairs::get是一個(gè)方法參考; 它指的是get分配給pairs變量的實(shí)例的方法。通過為基礎(chǔ)創(chuàng)建類型,我們?cè)O(shè)計(jì)了無(wú)效輸入的可能性,因此對(duì)該basePair方法的需求消失,異常也是如此。這是Java對(duì)Clojure的一個(gè)優(yōu)勢(shì),它本身不能在函數(shù)契約中強(qiáng)制執(zhí)行類型。更重要的是,它StringBuilder也消失了。當(dāng)您需要迭代集合,以某種方式處理每個(gè)元素并構(gòu)建包含結(jié)果的新集合時(shí),Java Streams非常適合。這可能占你生活中所寫循環(huán)的很大一部分。大部分的家務(wù)管理都不是真正的工作的一部分,而是為您完成的。

在Clojure

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

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

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

(定義 轉(zhuǎn)錄 [ dna ]
  (地圖 基礎(chǔ)對(duì) dna ))


這段代碼的業(yè)務(wù)結(jié)束是最后一行(map base-pair dna)- 值得指出,因?yàn)槟憧赡苠e(cuò)過了它。它表示字符串上mapbase-pair函數(shù)dna(表現(xiàn)為序列)。如果我們希望它返回一個(gè)字符串而不是一個(gè)列表,這就是map我們所要求的,唯一需要做的改變是:

(應(yīng)用 str  (map  base-pair  dna ))


在C#中

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

命名空間 RnaTranscription
{
    公共 類 轉(zhuǎn)錄員
    {
        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#沒有向我們展示我們?cè)贘ava中遇到的問題,因?yàn)镃#中的字符串是可枚舉的,并且所有“基元”都可以被視為具有行為的對(duì)象。

我們可以用更加實(shí)用的方式重寫程序,就像這樣,并且它比Java Streams版本要簡(jiǎn)單得多。對(duì)于Java流中的“map”,請(qǐng)?jiān)贑#中讀取“select”:

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


或者,如果您愿意,可以使用LINQ作為其語(yǔ)法糖:

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


為什么我們循環(huán)?

你可能會(huì)得到這個(gè)想法。如果您想到編寫循環(huán)之前的時(shí)間,通常您會(huì)嘗試完成以下任一操作:

  • 將一種類型的數(shù)組映射到另一種類型的數(shù)組。

  • 通過查找滿足某個(gè)謂詞的數(shù)組中的所有項(xiàng)來進(jìn)行過濾。

  • 確定數(shù)組中的任何項(xiàng)目是否滿足某些謂詞。

  • 累積數(shù)組中的計(jì)數(shù),總和或其他類型的累積結(jié)果。

  • 將數(shù)組的元素排序?yàn)樘囟樞颉?/p>

大多數(shù)現(xiàn)代語(yǔ)言中提供的函數(shù)式編程功能使您無(wú)需編寫循環(huán)或創(chuàng)建集合來存儲(chǔ)結(jié)果即可完成所有這些操作。功能樣式允許您省去這些內(nèi)務(wù)操作并專注于實(shí)際工作。更重要的是,功能樣式允許您將操作鏈接在一起,例如,如果您需要:

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

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

  3. 對(duì)過濾的元素進(jìn)行排序

在命令式樣式中,這需要多個(gè)循環(huán)或一個(gè)循環(huán),其中包含很多代碼。無(wú)論哪種方式,它涉及許多模糊程序真正目的的管理工作。在功能風(fēng)格中,您可以免除管理工作并直接表達(dá)您的意思。稍后,我們將看到更多功能樣式如何讓您的生活更輕松的例子。


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI