你可能聽(tīng)過(guò)它之前說(shuō)過(guò),一種特定的語(yǔ)言是功能性的,因?yàn)樗小耙涣鞯墓δ堋?。正如我在本系列關(guān)于函數(shù)式編程的第一篇文章中所說(shuō)的那樣,我不贊同這種流行的觀點(diǎn)。我同意一流的函數(shù)是任何函數(shù)式語(yǔ)言的基本特征,但我不認(rèn)為這是語(yǔ)言具有功能性的充分條件。有許多命令式語(yǔ)言也具有這一特性。但是,什么是一流的函數(shù)呢?功能描述為頭等艙當(dāng)它們可以像任何其他值一樣處理時(shí)-也就是說(shuō),它們可以在運(yùn)行時(shí)被動(dòng)態(tài)地分配給一個(gè)名稱或符號(hào)。它們可以存儲(chǔ)在數(shù)據(jù)結(jié)構(gòu)中,通過(guò)函數(shù)參數(shù)傳入,并作為函數(shù)返回值返回。
作為一家“創(chuàng)意+整合+營(yíng)銷”的成都網(wǎng)站建設(shè)機(jī)構(gòu),我們?cè)跇I(yè)內(nèi)良好的客戶口碑。創(chuàng)新互聯(lián)公司提供從前期的網(wǎng)站品牌分析策劃、網(wǎng)站設(shè)計(jì)、成都做網(wǎng)站、成都網(wǎng)站建設(shè)、成都外貿(mào)網(wǎng)站建設(shè)、創(chuàng)意表現(xiàn)、網(wǎng)頁(yè)制作、系統(tǒng)開(kāi)發(fā)以及后續(xù)網(wǎng)站營(yíng)銷運(yùn)營(yíng)等一系列服務(wù),幫助企業(yè)打造創(chuàng)新的互聯(lián)網(wǎng)品牌經(jīng)營(yíng)模式與有效的網(wǎng)絡(luò)營(yíng)銷方法,創(chuàng)造更大的價(jià)值。
這其實(shí)不是一個(gè)新奇的想法。函數(shù)指針從1972年開(kāi)始就一直是C的一個(gè)特性。在此之前,過(guò)程引用是Algol 68的一個(gè)特性,于1970年實(shí)現(xiàn),當(dāng)時(shí),它們被認(rèn)為是程序性編程特性追溯到更久以前,Lisp(首次實(shí)現(xiàn)于1963年)是建立在程序代碼和數(shù)據(jù)是可互換的概念之上的。
這些也不是模糊的特性。在C語(yǔ)言中,我們通常使用函數(shù)作為一流的對(duì)象。例如,在排序時(shí):
char **array = randomStrings();
printf("Before sorting:\n");
for (int s = 0; s < NO_OF_STRINGS; s++)
printf("%s\n", array[s]);
qsort(array, NO_OF_STRINGS, sizeof(char *), compare);
printf("After sorting:\n");
for (int s = 0; s < NO_OF_STRINGS; s++)
printf("%s\n", array[s]);
這,這個(gè),那,那個(gè)stdlib
C中的庫(kù)為不同類型的排序例程提供了一組函數(shù)。它們都能夠?qū)θ魏晤愋偷臄?shù)據(jù)進(jìn)行排序:程序員所需要的唯一幫助就是提供一個(gè)比較數(shù)據(jù)集的兩個(gè)元素并返回的函數(shù)。-1
, 1
,或0
,指示哪個(gè)元素大于另一個(gè)元素或它們相等。
這本質(zhì)上就是戰(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);
控件上沒(méi)有括號(hào)。compare
函數(shù)名使編譯器發(fā)出函數(shù)指針,而不是函數(shù)調(diào)用。因此,在C中將函數(shù)視為頭等對(duì)象是非常容易的,盡管接受函數(shù)指針的函數(shù)的簽名非常難看:
qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
函數(shù)指針不僅用于排序。早在.NET發(fā)明之前,就有用于編寫(xiě)MicrosoftWindows應(yīng)用程序的Win 32 API。在此之前,有Win16API。它自由地使用函數(shù)指針作為回調(diào)。應(yīng)用程序在調(diào)用窗口管理器時(shí)提供了指向其自身函數(shù)的指針,當(dāng)應(yīng)用程序需要通知某個(gè)已經(jīng)發(fā)生的事件時(shí),窗口管理器將調(diào)用該窗口管理器。您可以認(rèn)為這是應(yīng)用程序(觀察者)與其窗口(可觀察的)之間的一個(gè)觀察者模式關(guān)系-應(yīng)用程序接收到了發(fā)生在其窗口上的事件的通知,例如鼠標(biāo)單擊和按鍵盤。在窗口管理器中抽象了管理窗口的工作-移動(dòng)窗口,將它們堆疊在一起,決定哪個(gè)應(yīng)用程序是用戶操作的接收者。這些應(yīng)用程序?qū)λ鼈児蚕憝h(huán)境的其他應(yīng)用程序一無(wú)所知。在面向?qū)ο蟮木幊讨?,我們通常通過(guò)抽象類和接口來(lái)實(shí)現(xiàn)這種解耦,但也可以使用一流的函數(shù)來(lái)實(shí)現(xiàn)。
所以,我們使用一流的函數(shù)已經(jīng)有很長(zhǎng)時(shí)間了。但是,公平地說(shuō),沒(méi)有一種語(yǔ)言比簡(jiǎn)陋的Javascript更能廣泛地推廣作為一流公民的功能。
在Javascript中,將函數(shù)傳遞給用作回調(diào)的其他函數(shù)一直是一種標(biāo)準(zhǔn)做法,就像在Win 32 API中一樣。這個(gè)想法是HTML DOM的一個(gè)組成部分,其中第一類函數(shù)可以作為事件偵聽(tīng)器添加到DOM元素中:
function myEventListener() {
alert("I was clicked!")
}
...
var myBtn = document.getElementById("myBtn")
myBtn.addEventListener("click", myEventListener)
就像在C中一樣,myEventListener
函數(shù)名時(shí),在調(diào)用addEventListener
意味著它不會(huì)立即執(zhí)行。相反,該函數(shù)與click
事件中的DOM元素。當(dāng)單擊元素時(shí),然后將調(diào)用該函數(shù)并發(fā)生警報(bào)。
流行的jQuery庫(kù)通過(guò)證明通過(guò)查詢字符串選擇DOM元素的函數(shù)簡(jiǎn)化了流程,并提供了操作元素和向元素添加事件偵聽(tīng)器的有用函數(shù):
$("#myBtn").click(function() {
alert("I was clicked!")
})
類中使用的第一類函數(shù)也是實(shí)現(xiàn)異步I/O的方法。XMLHttpRequest
對(duì)象,它是Ajax的基礎(chǔ)。同樣的想法在Node.js中也很普遍。當(dāng)您想要進(jìn)行一個(gè)非阻塞函數(shù)調(diào)用時(shí),將它傳遞給一個(gè)函數(shù)的引用,以便在它完成時(shí)調(diào)用您。
但是,這里還有別的東西。第二個(gè)例子不僅僅是一個(gè)一流函數(shù)的例子。它也是Lambda函數(shù)。具體而言,本部分:
function() {
alert("I was clicked!");
}
lambda函數(shù)(通常被稱為蘭卜達(dá))是一個(gè)未命名的函數(shù)。他們本可以叫他們匿名函數(shù),這樣每個(gè)人都會(huì)立刻知道他們是什么。但是,這聽(tīng)起來(lái)不那么令人印象深刻,所以lambda函數(shù)就是!lambda函數(shù)的要點(diǎn)是在那里只需要一個(gè)函數(shù);因?yàn)樗谌魏蔚胤蕉疾恍枰?,所以您只需要在那里定義它。不需要名字。如果你做需要在其他地方重用它,然后考慮將它定義為一個(gè)命名函數(shù),然后按名稱引用它,就像我在第一個(gè)Javascript示例中所做的那樣。如果沒(méi)有l(wèi)ambda函數(shù),使用jQuery和Node進(jìn)行編程確實(shí)會(huì)令人厭煩。
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中,所謂的“穩(wěn)定”語(yǔ)法:->
(a, b) { return a + b }
正如我們所看到的,大多數(shù)語(yǔ)言都傾向于一種比Javascript更簡(jiǎn)潔的表達(dá)lambdas的方式。
您可能已經(jīng)在編程中使用了“map”一詞來(lái)表示將對(duì)象存儲(chǔ)為鍵值對(duì)的數(shù)據(jù)結(jié)構(gòu)(如果您的語(yǔ)言稱它為“字典”,那么就沒(méi)有問(wèn)題了)。在函數(shù)式編程中,這個(gè)詞還有一個(gè)額外的含義。事實(shí)上,基本概念是一樣的。在這兩種情況下,一組事物被映射到另一組事物。在數(shù)據(jù)結(jié)構(gòu)的意義上,映射是一個(gè)名詞-鍵被映射到值。在編程意義上,map是一個(gè)動(dòng)詞-一個(gè)函數(shù)將一個(gè)值數(shù)組映射到另一個(gè)值數(shù)組。
假設(shè)你有一個(gè)功能f以及一系列的值A = [A1, A2, A3, A4]地圖f過(guò)關(guān)A手段應(yīng)用f中的每一個(gè)元素A:
A1 → f (A1) = a1‘
A2 → f (A2) = a2‘
A3 → f (A3) = A3‘
A4 → f (A4) = A4‘
然后,按照與輸入相同的順序組裝結(jié)果數(shù)組:
A‘=地圖(f, A ) = [a1‘, a2‘, A3‘, A4‘]
好吧,這很有趣,但是位數(shù)學(xué)。你多久會(huì)這么做一次?實(shí)際上,這比你想象的要頻繁得多。像往常一樣,有一個(gè)例子最能解釋事情,所以讓我們看一看我從下面舉出來(lái)的一個(gè)簡(jiǎn)單的練習(xí)exercism.io當(dāng)我學(xué)習(xí)Clojure的時(shí)候。這項(xiàng)運(yùn)動(dòng)被稱為“RNA轉(zhuǎn)錄”,它非常簡(jiǎn)單。我們將看一看需要轉(zhuǎn)錄成輸出字符串的輸入字符串。這些基礎(chǔ)是這樣翻譯的:
C→G
G→C
→U
T→A
除C、G、A、T以外的任何輸入都是無(wú)效的。JUnit 5中的測(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();
assertThat(transcriber.transcribe(dna), is(rna));
}
@Test
void reject_invalid_bases() {
var transcriber = new Transcriber();
assertThrows(
IllegalArgumentException.class,
() -> transcriber.transcribe("XCGFGGTDTTAA"));
}
}
而且,我們可以通過(guò)這個(gè)Java實(shí)現(xiàn)通過(guò)測(cè)試:
class Transcriber {
private Mappairs = new HashMap<>();
Transcriber() {
pairs.put('C', 'G');
pairs.put('G', 'C');
pairs.put('A', 'U');
pairs.put('T', 'A');
}
String transcribe(String dna) {
var rna = new StringBuilder();
for (var base: dna.toCharArray()) {
if (pairs.containsKey(base)) {
var pair = pairs.get(base);
rna.append(pair);
} else
throw new IllegalArgumentException("Not a base: " + base);
}
return rna.toString();
}
}
用函數(shù)樣式編程的關(guān)鍵是,毫不奇怪地,將可能表示為函數(shù)的所有內(nèi)容轉(zhuǎn)換為一個(gè)函數(shù)。所以,讓我們這樣做:
char basePair(char base) {
if (pairs.containsKey(base))
return pairs.get(base);
else
throw new IllegalArgumentException("Not a base " + base);
}
String transcribe(String dna) {
var rna = new StringBuilder();
for (var base : dna.toCharArray()) {
var pair = basePair(base);
rna.append(pair);
}
return rna.toString();
}
現(xiàn)在,我們可以用地圖作為動(dòng)詞了。在Java中,在Streams API中提供了一個(gè)函數(shù):
char basePair(char base) {
if (pairs.containsKey(base))
return pairs.get(base);
else
throw new IllegalArgumentException("Not a base " + base);
}
String transcribe(String dna) {
return dna.codePoints()
.mapToObj(c -> (char) c)
.map(base -> basePair(base))
.collect(
StringBuilder::new,
StringBuilder::append,
StringBuilder::append)
.toString();
}
那么,讓我們批評(píng)一下這個(gè)解決方案。可以說(shuō)的最好的事情就是循環(huán)已經(jīng)消失了。如果你想一想,循環(huán)是一種文書(shū)活動(dòng),我們真的不應(yīng)該去關(guān)注大部分時(shí)間。通常,我們循環(huán)是因?yàn)槲覀兿霝榧现械拿總€(gè)元素做一些事情。我們真的這里要做的是獲取這個(gè)輸入序列并從它生成一個(gè)輸出序列。流為我們處理迭代的基本管理工作。事實(shí)上,它是一種設(shè)計(jì)模式-一種功能性設(shè)計(jì)模式-但是,我現(xiàn)在還不想提它的名字。我還不想把你嚇跑。
我不得不承認(rèn),代碼的其余部分并沒(méi)有那么好,這主要是因?yàn)镴ava中的原語(yǔ)不是對(duì)象。第一點(diǎn)不偉大的地方是:
mapToObj(c -> (char) c)
我們必須這樣做,因?yàn)镴ava對(duì)原語(yǔ)和對(duì)象的處理方式不同,而且盡管語(yǔ)言確實(shí)為原語(yǔ)設(shè)置了包裝類,但是無(wú)法直接從字符串中獲取字符對(duì)象的集合。
另一個(gè)不那么令人敬畏的地方是:
.collect(
StringBuilder::new,
StringBuilder::append,
StringBuilder::append)
還不清楚為什么要打電話append
兩次。我稍后會(huì)解釋,但現(xiàn)在時(shí)機(jī)不對(duì)。
我不打算為這個(gè)密碼辯護(hù)-這太糟糕了。如果有一種方便的方法從字符串中獲取一個(gè)字符流對(duì)象,甚至是一個(gè)字符數(shù)組,那么就沒(méi)有問(wèn)題了,但我們還沒(méi)有得到一個(gè)。在Java中,處理原語(yǔ)不是FP的亮點(diǎn)。想想看,它甚至對(duì)OO編程都沒(méi)有好處。所以,也許我們不應(yīng)該那么癡迷于原語(yǔ)。如果我們把它們?cè)O(shè)計(jì)在代碼之外呢?我們可以為基礎(chǔ)創(chuàng)建一個(gè)枚舉:
enum Base {
C, G, A, T, U;
}
而且,我們有一個(gè)類作為一個(gè)包含一系列堿基的一流集合:
class Sequence {
Listbases;
Sequence(Listbases) {
this.bases = bases;
}
Streambases() {
return bases.stream();
}
}
現(xiàn)在,Transcriber
看起來(lái)是這樣的:
class Transcriber {
private Mappairs = new HashMap<>();
Transcriber() {
pairs.put(C, G);
pairs.put(G, C);
pairs.put(A, U);
pairs.put(T, A);
}
Sequence transcribe(Sequence dna) {
return new Sequence(dna.bases()
.map(pairs::get)
.collect(toList()));
}
}
這樣好多了。這,這個(gè),那,那個(gè)pairs::get
是方法引用;它引用get
方法的實(shí)例分配給pairs
變量。通過(guò)為基創(chuàng)建類型,我們?cè)O(shè)計(jì)了無(wú)效輸入的可能性,因此需要basePair
方法消失,異常也會(huì)消失。這是Java的一個(gè)優(yōu)勢(shì),它本身不能在函數(shù)契約中強(qiáng)制執(zhí)行類型。更重要的是,StringBuilder
也消失了。當(dāng)您需要迭代一個(gè)集合、以某種方式處理每個(gè)元素以及構(gòu)建一個(gè)包含結(jié)果的新集合時(shí),Java流是很好的。這可能在你生命中寫(xiě)的循環(huán)中占了相當(dāng)大的比例。大部分家務(wù)活,不是手頭真正工作的一部分,都是為你做的。
撇開(kāi)輸入不足不談,Clojure比Java版本要簡(jiǎn)潔一些,并且它給我們提供了在字符串的字符上進(jìn)行映射的難度。Clojure中最重要的抽象是序列;所有集合類型都可以視為序列,字符串也不例外:
(def pairs {\C, "G",
\G, "C",
\A, "U",
\T, "A"})
(defn- base-pair [base]
(if-let [pair (get pairs base)]
pair
(throw (IllegalArgumentException. (str "Not a base: " base)))))
(defn transcribe [dna]
(map base-pair dna))
此代碼的業(yè)務(wù)端是最后一行。(map
base-pair dna)
-這是值得指出的,因?yàn)槟憧赡芤呀?jīng)錯(cuò)過(guò)了。意思是map
這,這個(gè),那,那個(gè)base-pair
函數(shù)對(duì)dna
字符串(表現(xiàn)為序列)。如果我們希望它返回一個(gè)字符串而不是一個(gè)列表,這就是map
給我們,唯一需要的改變是:
(apply str (map base-pair dna))
讓我們?cè)囋嚵硪环N語(yǔ)言。C#中解決方案的命令式方法如下所示:
namespace RnaTranscription
{
public class Transcriber
{
private readonly Dictionary_pairs = new Dictionary
{
{'C', 'G'},
{'G', 'C'},
{'A', 'U'},
{'T', 'A'}
};
public string Transcribe(string dna)
{
var rna = new StringBuilder();
foreach (char b in dna)
rna.Append(_pairs[b]);
return rna.ToString();
}
}
}
同樣,C#沒(méi)有向我們介紹我們?cè)贘ava中遇到的問(wèn)題,因?yàn)镃#中的字符串是可枚舉的,而且所有的“原語(yǔ)”都可以被視為具有行為的對(duì)象。
我們可以一種更實(shí)用的方式重寫(xiě)程序,就像這樣,結(jié)果顯示它比JavaStreams版本要少得多。對(duì)于Java流中的“map”,請(qǐng)改為C#中的“select”:
public string Transcribe(string dna)
{
return String.Join("", dna.Select(b => _pairs[b]));
}
或者,如果您愿意,可以使用LINQ作為其語(yǔ)法糖:
public string Transcribe(string dna)
{
return String.Join("", from b in dna select _pairs[b]);
}
你可能知道這個(gè)主意。如果您想到以前編寫(xiě)循環(huán)的時(shí)間,通常您會(huì)嘗試完成以下工作之一:
將一種類型的數(shù)組映射為另一種類型的數(shù)組。
通過(guò)查找滿足某種謂詞的數(shù)組中的所有項(xiàng)進(jìn)行篩選。
確定數(shù)組中的任何項(xiàng)是否滿足某些謂詞。
從數(shù)組中累積計(jì)數(shù)、和或其他類型的累積結(jié)果。
將數(shù)組的元素按特定順序排序。
大多數(shù)現(xiàn)代語(yǔ)言中可用的函數(shù)式編程特性允許您完成所有這些功能,而無(wú)需編寫(xiě)循環(huán)或創(chuàng)建集合來(lái)存儲(chǔ)結(jié)果。功能風(fēng)格可以讓你省去那些家務(wù)工作,專注于真正的工作。此外,功能樣式允許您將操作鏈接在一起,例如,如果需要的話:
將數(shù)組的元素映射到另一種類型。
過(guò)濾掉一些映射的元素。
對(duì)過(guò)濾過(guò)的元素進(jìn)行排序。
在命令式風(fēng)格中,這需要多個(gè)循環(huán)或一個(gè)循環(huán),其中包含大量代碼。不管是哪種方式,它都涉及大量的行政工作,這些工作掩蓋了項(xiàng)目的真正目的。在功能風(fēng)格中,您可以分發(fā)管理工作,并直接表達(dá)您的意思。稍后,我們將看到更多的例子,功能風(fēng)格可以使您的生活更輕松。
當(dāng)我學(xué)習(xí)函數(shù)式編程和習(xí)慣JavaStreams
API時(shí),每次我寫(xiě)一個(gè)循環(huán)時(shí),我會(huì)做的下一件事就是考慮如何將它重寫(xiě)為流。這通常是可能的。在C#中,ReSharper
VisualStudio插件自動(dòng)建議您進(jìn)行這種重構(gòu)。既然我已經(jīng)內(nèi)化了功能風(fēng)格,我就直奔流程,除非我真的需要一個(gè)循環(huán),否則就不需要循環(huán)了。在下一篇文章中,我們將繼續(xù)探索一流的函數(shù),以及如何使用函數(shù)樣式使代碼更具表現(xiàn)力。filter
和reduce
。繼續(xù)關(guān)注!