您好,登錄后才能下訂單哦!
本文首發(fā)于 vivo互聯(lián)網(wǎng)技術(shù) 微信公眾號?
鏈接:https://mp.weixin.qq.com/s/gqw57pBYB4VRGKmNlkAODg
作者:張文博
比起命令式編程,函數(shù)式編程更加強調(diào)程序執(zhí)行的結(jié)果而非執(zhí)行的過程,倡導利用若干簡單的執(zhí)行單元讓計算結(jié)果不斷演進,逐層推導出復雜的運算。本文通過函數(shù)式編程的一些趣味用法來闡述學習函數(shù)式編程的奇妙之處。
編程是為了解決問題,而解決問題可以有多種視角和思路,其中普適且行之有效的模式被歸結(jié)為“編程范式”。編程語言日新月異,從匯編、Pascal、C、C++、Ruby、Python、JS,etc...其背后的編程范式其實并沒有發(fā)生太多變化。拋開各語言繁紛復雜的表象去探究其背后抽象的編程范式可以幫助我們更好地使用computer進行compute。
計算機本質(zhì)上是執(zhí)行一個個指令,因此編程人員只需要一步步寫下需要執(zhí)行的指令,比如:先算什么再算什么,怎么輸入怎么計算怎么輸出。所以編程語言大多都具備這四種類型的語句:
運算語句將結(jié)果存入存儲器中以便日后使用;
循環(huán)語句使得一些語句可以被反復運行;
條件分支語句允許僅當某些條件成立時才運行某個指令集合;
使得執(zhí)行順序能夠轉(zhuǎn)移到其他指令之處。
無論使用匯編、C、Java、JS 都可以寫出這樣的指令集合,其主要思想是關(guān)注計算機執(zhí)行的步驟,即一步一步告訴計算機先做什么再做什么。所以命令式語言特別適合解決線性的計算場景,它強調(diào)自上而下的設(shè)計方式。這種方式非常類似我們的工作、生活,因為我們的日?;顒佣际前床烤桶嗟捻樞蜻M行的,甚至你可以認為是面向過程的。也比較貼合我們的思維方式,因此我們寫出的絕大多數(shù)代碼都是這樣的。
聲明式編程是以數(shù)據(jù)結(jié)構(gòu)的形式來表達程序執(zhí)行的邏輯,它的主要思想是告訴計算機應(yīng)該做什么,但不指定具體要怎么做(當然在一些場景中,我們也還是要指定、探究其如何做)。SQL 語句就是最明顯的一種聲明式編程的例子,例如:“SELECT * FROM student WHERE age> 18”。因為我們歸納剝離了how,我們就可以專注于what,讓數(shù)據(jù)庫來幫我們執(zhí)行、優(yōu)化how。
有時候?qū)τ谀硞€業(yè)務(wù)邏輯目前沒有任何可以歸納提取的通用實現(xiàn),我們只能寫命令式編程代碼。當我們寫成以后,如果進行思考歸納抽象、進一步優(yōu)化,就為以后的聲明式做下鋪墊。
通過對比,命令式編程模擬電腦運算,是行動導向的,關(guān)鍵在于定義解法,即“怎么做”,因而算法是顯性而目標是隱性的;聲明式編程模擬人腦思維,是目標驅(qū)動的,關(guān)鍵在于描述問題,即“做什么”,因而目標是顯性而算法是隱性的。
函數(shù)式編程將計算機運算視為函數(shù)運算,并且避免使用程序狀態(tài)以及易變對象。這里的“函數(shù)”不是指計算機中的函數(shù),而是指數(shù)學中的函數(shù),即自變量的映射。也就是說一個函數(shù)的值僅決定于函數(shù)參數(shù)的值,不依賴其他狀態(tài)。比如f(x),只要x不變,不論什么時候調(diào)用,調(diào)用幾次,值都是不變的。比起命令式編程,函數(shù)式編程更加強調(diào)程序執(zhí)行的結(jié)果而非執(zhí)行的過程,倡導利用若干簡單的執(zhí)行單元讓計算結(jié)果不斷演進,逐層推導出復雜的運算,而不是設(shè)計一個復雜的執(zhí)行過程。函數(shù)作為一等公民,可以出現(xiàn)在任何地方,比如你可以把函數(shù)作為參數(shù)傳遞給另一個函數(shù)、還可以將函數(shù)作為返回值。
函數(shù)式編程的特點:
減少了可變量的聲明,程序更為安全;
相比命令式編程,少了非常多的狀態(tài)變量的聲明與維護,天然適合高并發(fā)多線程并行計算等任務(wù),我想這也是函數(shù)是編程近年又大熱的重要原因之一;
public class OutClass {
private void helloWorld() {
System.out.println("Hello World!");
}
public InnerClass getInnerClass() {
return new InnerClass();
}
public class InnerClass {
public void hello() {
helloWorld();
}
}
/**
* @param args
*/
public static void main(String[] args) {
// 在外部使用OutClass的private方法
new OutClass().getInnerClass().hello();
}
}
在Java中有很多方式實現(xiàn)上述目的,因為我們的作用域和JS有著巨大差異。但是借鑒閉包的原理,我們來看一個場景。假設(shè)接口A有一個方法m;接口B也有一個同名的方法m,兩個方法的簽名完全一樣但是功能卻不一樣。類C想要同時實現(xiàn)接口A和接口B中的方法。因為兩個接口中的方法簽名完全一致,所以C只能有一個m方法,這種情況下應(yīng)該怎么實現(xiàn)需求呢?
public class C implements A {
@Override
public void m() {
//...
}
private void o() {
//...
}
public D getD() {
return new D();
}
class D implements B {
@Override
public void m() {
o();
}
}
public static void main(String[] args) {
C c = new C();
c.m();
c.getD().m();
}
}
我對柯里化(Currying)的理解:柯里化函數(shù)可以接收一些參數(shù),接收了這些參數(shù)之后,該函數(shù)并不是立即求值,而是繼續(xù)返回另一個函數(shù),剛才傳入的參數(shù)在函數(shù)形成的閉包中被保存起來,待到函數(shù)真正需要求值的時候,之前傳入的所有參數(shù)都能用于求值。
下面先通過JS(個人感覺通過JS比較好理解)對柯里化有一個直觀的認識。
var calculator = function(x, y, z){
return(x + y)* z;
}
調(diào)用:calculator( 2, 7, 3);
柯里化寫法:
var calculator=function(x){
return function(y){
return function(z){
return(x + y)* z;
};
};
};
調(diào)用:calculator(2)(7)(3);
通過對比,我們發(fā)現(xiàn)柯里化的數(shù)學描述應(yīng)該類似這樣,calculator(2, 7, 3) ---> calculator(2)(7)(3)。
現(xiàn)在我們來回頭看看柯里化較為學術(shù)的定義,是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)的函數(shù),并且返回接受余下的參數(shù)的新函數(shù),這個新函數(shù)最后還能返回所有輸入的運算結(jié)果。
Java 中的柯里化實現(xiàn)
Function<Integer, Function<Integer, Function<Integer, Integer>>> currying =
new Function<Integer, Function<Integer, Function<Integer, Integer>>>() {
@Override
public Function<Integer, Function<Integer, Integer>> apply(Integer x) {
return new Function<Integer, Function<Integer, Integer>>() {
@Override
public Function<Integer, Integer> apply(Integer y) {
return new Function<Integer, Integer>() {
@Override
public Integer apply(Integer z) {
return (x + y) * z;
}
};
}
};
}
};
//在這里,我們可以發(fā)現(xiàn),雖然依次輸入2、7,但是我們并不會計算結(jié)果,而是等到最后輸入結(jié)束時才會返回值。
Function function1 = curryingFun().apply(2);//返回的是函數(shù)
Function function2 = curryingFun().apply(2).apply(7);//返回的是函數(shù)
Integer value = curryingFun().apply(2).apply(7).apply(3);//參數(shù)全部輸入,返回最后的值
柯里化的爭論
(1)支持的觀點
延遲計算,只有在最后的輸入結(jié)束才會進行計算;
當你發(fā)現(xiàn)你要調(diào)用一個函數(shù),并且調(diào)用參數(shù)都是一樣的情況下,這個參數(shù)就可以被柯里化,以便更好的完成任務(wù);
(2)不過也有一些人持反對觀點,參數(shù)的不確定性、排查錯誤困難。
Promise 是異步編程的一種解決方案,比傳統(tǒng)的諸如“回調(diào)函數(shù)、事件”解決方案,更合理和更強大。ES6已經(jīng)廣泛應(yīng)用。我在這里主要分析兩個最常見的用法。
Promise實例生成以后,可以用then方法分別指定resolved狀態(tài)和rejected狀態(tài)的回調(diào)函數(shù)。then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。因此可以采用鏈式寫法,即then方法后面再調(diào)用另一個then方法。
promise.then(function(value) {
// success
}, function(error) {
// failure
}).then(...);
Promise.all方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.all方法接受一個數(shù)組作為參數(shù),p1、p2、p3都是 Promise 實例,p的狀態(tài)由p1、p2、p3決定,分成兩種情況。
只有p1、p2、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled,此時p1、p2、p3的返回值組成一個數(shù)組,傳遞給p的回調(diào)函數(shù)。
下面是一個具體的例子:
// 生成一個Promise對象的數(shù)組
const promises = [1,2,3.....].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
Java的實現(xiàn)
Java中的使用方法目前確實不如js方便,可以看看CompletableFuture,給我們提供了一些方法。
其定義如下:當函數(shù)的參數(shù)個數(shù)太多,可以創(chuàng)建一個新的函數(shù),這個新函數(shù)可以固定住原函數(shù)的部分參數(shù),從而在調(diào)用時更簡單。下面是基于Python的實現(xiàn)。個人覺得,最大的便利就是避免我們再去寫一些重載的方法。不過暫時沒有看到partial的Java版本。看到這里,大家肯定認為“偏函數(shù)”這個翻譯實在是不準確,如果直譯過來叫“部分函數(shù)”好像也不怎么清晰,我們姑且還是稱其為Partial Function。
# !/usr/bin/python
# -*- coding: UTF-8 -*-
from functools import partial
def multiply(x, y):
return x * y
print(multiply(3,4))# 輸出12
multiply4 = partial(multiply, y =4)# 不需要定義重載函數(shù)
print(multiply4(3))# 輸出12
Java現(xiàn)在對map、reduce也做了支持,特別是map已經(jīng)是大家日常編碼的利器,相信大家也都不陌生了。map(flatMap)按照規(guī)則轉(zhuǎn)換輸入內(nèi)容,而reduce則是通過某個連接動作將所有元素匯總的操作。但是在這里我還是使用Python的例子來進行闡述,因為我覺得Python看起來更簡潔明了。
# !/usr/bin/python
# -*- coding: UTF-8 -*-
from functools import reduce
def addTen(x):
return x + 10
def add(x, y):
return x + y
r = map(addTen, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print r #[11, 12, 13, 14, 15, 16, 17, 18, 19]
total = reduce(add, r)
print total #[11, 12, 13, 14, 15, 16, 17, 18, 19]加和等于135
divmod是Python的函數(shù),我之所以專門來講述,是因為它所代表的思想確實新穎。函數(shù)會把除數(shù)和余數(shù)運算結(jié)果結(jié)合起來返回,如下。不過Java肯定不支持。
//把秒數(shù)轉(zhuǎn)換成時分秒結(jié)構(gòu)顯示
def parseDuration( seconds ):
m, s = divmod(int(seconds), 60)
h, m = divmod(m, 60)
return ("%02d:%02d:%02d" % (h, m, s))
上述很多特性,Scala都提供了支持,它集成了面向?qū)ο缶幊毯秃瘮?shù)式編程的一些特性,感興趣的同學可以了解一下。之前看過介紹,Twitter對于Scala的應(yīng)用比較多,推薦閱讀?Twitter Effective Scala?。
在很多時候,無可否認命令式編程很好用。當我們寫業(yè)務(wù)邏輯時會書寫大量的命令式代碼,甚至在很多時候并沒有可以歸納抽離的實現(xiàn)。但是,如果我們花時間去學習、發(fā)現(xiàn)可以歸納抽離的部分使其朝著聲明式邁進,結(jié)合函數(shù)式的思維來思考,能為我們的編程帶來巨大的便捷。
通過其他語言來觸類旁通函數(shù)式編程的奇技淫巧,確實能帶給我們新的視野。我相信隨著機器運算能力不斷提升、底層能力更加完善,我們也需要跳出如何做的思維限制,更多地站在更高的抽象層去思考做什么,方能進入一個充滿想象、神奇的computable world。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。