您好,登錄后才能下訂單哦!
您之前可能已經(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 ]);
stdlib
C中的庫(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。
在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,a2,a3,a4 ]。要映射?F超過甲意味著應(yīng)用?F在每個(gè)元件甲:
a1 → f(a1)= a1'
a2 → f(a2)= a2'
a3 → f(a3)= a3'
a4 → f(a4)= a4'
然后,按照與輸入相同的順序組合結(jié)果數(shù)組:
A' = map(f,A)= [ 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();
}
所以,讓我們批評(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比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ò)過了它。它表示字符串上map
的base-pair
函數(shù)dna
(表現(xiàn)為序列)。如果我們希望它返回一個(gè)字符串而不是一個(gè)列表,這就是map
我們所要求的,唯一需要做的改變是:
(應(yīng)用 str (map base-pair dna ))
我們來試試另一種語(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ì)得到這個(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í)際工作。更重要的是,功能樣式允許您將操作鏈接在一起,例如,如果您需要:
將數(shù)組的元素映射到另一種類型。
過濾掉一些映射的元素。
對(duì)過濾的元素進(jìn)行排序
在命令式樣式中,這需要多個(gè)循環(huán)或一個(gè)循環(huán),其中包含很多代碼。無(wú)論哪種方式,它涉及許多模糊程序真正目的的管理工作。在功能風(fēng)格中,您可以免除管理工作并直接表達(dá)您的意思。稍后,我們將看到更多功能樣式如何讓您的生活更輕松的例子。
免責(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)容。