本文就是我在學習函數式編程的過程中本身體悟到的一些東西,這裏將用go,JavaScript以及Haskell三種語言來分析函數式編程的一些奧祕。JavaScript因爲具備的一些優點可以讓咱們能夠實現函數式編程,而go做爲一種強類型語言,雖然靈活性又稍有欠缺,可是也可以完成一些高階函數的實現,Haskell語言做爲正統的函數式編程語言,爲了解釋說明問題,做爲對比參照。javascript
函數式編程也算是常常看到了,它的一些優點包括:java
雖然上面的優點看看上去好像很厲害的樣子,可是,到底厲害在哪裏呢?咱們能夠經過下面的例子進行說明:編程
求和函數數組
Haskellapp
sum [1,2,3] -- 6 -- sum 的實現實際上是 foldr (+) 0 [1,2,3]
在Haskell中flodr
的函數定義是:編程語言
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
函數實現是:函數式編程
-- if the list is empty, the result is the initial value z; else -- apply f to the first element and the result of folding the rest foldr f z [] = z foldr f z (x:xs) = f x (foldr f z xs)
這是一個遞歸實現,在函數式編程中,遞歸定義是十分常見的。函數
foldr
函數其實作了這樣的事情:foldr
接受三個參數,第一個參數是函數f
,第二個參數是初始值z
,第三個參數是一個列表。若是列表爲空則返回初始化值z
,不然遞歸調用 foldr
,須要說明的是函數f
的類型是接受兩個參數,返回一個值,兩個參數類型都應該和z
相同(強類型語言中)。學習
在Haskell中咱們可以看到一個列表可以這樣被求和,那麼在JavaScript中,咱們是如何實現sum
函數的呢?ui
JavaScript
首先咱們實現js版本的foldr
function foldr(f,z,list){ //爲了簡潔起見,把類型判斷省略了 // Object.prototype,toString.call(list) === '[object Array]' if(list === null || list.length == 0){ return z; } //這裏的shift會改變參數的狀態,會形成反作用 //return f(list.shift(),foldr(f,z,list)); //改用以下寫法 return f(list[0],foldr(f,z,list.slice(1))); }
而後咱們再實現js版本的(+)
:
function add(a,b){ return a+b; }
那麼咱們的sum
就變成:
function sum(list){ return foldr(add,0,list); }
最後咱們的js版的sum
也能夠這樣用了:
let a = [1,2,3]; sum(a); // 6
像js這樣的弱類型的語言較爲靈活,函數f
能夠任意實現,對於foldr
函數也可以在多種數據類型之間複用,那麼對於像go這樣的強類型語言,結果又是怎麼樣的呢?
go
一樣地,咱們實現如下go版本的foldr
:
func foldr(f func(a,b int) int,z int,list []int)int{ if len(list) == 0{ return z } return f(list[0],foldr(f,z,list[1:])) }
go由於有數組切片,因此使用起來較爲簡單,可是go又是強類型的語言,因此在聲明函數的時候必需要把類型聲明清楚。
再實現一下f
函數:
func add(a,b int) int{ return a+b; }
依葫蘆畫瓢咱們能夠獲得go版本的sum
函數:
func sum(list []int) int{ return foldr(add,0,list) }
能夠看出來好像套路都差很少,真正在調用的時候是這樣的:
func main(){ a := []int{1,2,3} sum(a) // 6 }
在Haskell中是沒有循環的,由於循環能夠經過遞歸實現,在上文咱們實現的sum
函數中,也沒有用到任何循環語句,這和咱們原來的編程思惟有所不一樣,剛開始我學寫求和函數的時候,都是從for
,while
開始的,可是函數式給我打開了新世界的大門。
有了上面的基礎,咱們發如今函數式編程中,代碼的重用很是便利:
求積函數
javaScript
function muti(a,b){ return a*b; } function product(list){ return foldr(muti,1,list); }
go
func muti(a,b int) int{ return a*b; } func product(list []int) int{ return foldr(muti,1,list) }
Haskell
foldr (*) 1 [1,2,3,4] -- 24 -- or -- product 是Haskell預約義的函數 myproduct xs = foldr (*) 1 xs -- myproduct [1,2,3,4]
還有不少例如 anyTrue
、allTrue
的例子,如下僅給出js實現:
anyTure
JavaScript
function or(a,b){ return a || b; } function anyTrue(list){ return foldr(or,false,list); }
調用:
let b = [true,false,true]; console.log(anyTrue(b)); // true
allTure
JavaScript
function and(a,b){ return a && b; } function allTrue(list){ return foldr(and,true,list); }
調用:
let b = [true,false,true]; console.log(allTrue(b)); // false
固然咱們能夠看出來這個flodr
函數賊好用,可是好像仍是有點疑惑,它是怎麼工做的呢?看了一圈,flodr
就是一個遞歸函數,但其實在編程世界,它還有一個更加出名的名字——reduce
。咱們看看在js中是如何使用reduce
實現sum函數的:
求和函數reduce版
const _ = require("lodash"); _.reduce([1,2,3],function(sum,n){ return sum+n; });
在lodash
官方文檔是這麼定義的:
_.reduce alias _.foldl _.reduceRight alias _.foldr
好吧,我欺騙了大家,其實foldr
應該對應reduceRight
。
那麼foldl
和foldr
到底有什麼不一樣呢?
其實這兩個函數的不一樣之處在於結合的方式不一樣,以求差爲例:
Haskell
foldr (-) 0 [1,2,3] -- 輸出: 2 foldl (-) 0 [1,2,3] -- 輸出: -6
爲何兩個輸出是不一樣的呢?這個和結合方向有關:
foldr (-) 0 [1,2,3]
至關於:
1-(2-(3-0)) = 2
而
foldl (-) 0 [1,2,3]
至關於:
((0-1)-2)-3) = -6
結合方向對於求和結果而言是沒有區別的,可是對於求差,就有影響了:
JavaScript
const _ = require("lodash"); //reduce 至關於 foldl _.reduce([1,2,3],function(sum,n){ return sum-n; }); // 輸出 -4
這個和說好的-6
好像又不同了,坑爹呢麼不是?!這是由於,在lodash
的實現中,reduce
的初始值爲數組的第一個元素,因此結果是1-2-3 = -4
。
那麼咱們看看reduceRight == foldr
的結果:
JavaScript
const _ = require("lodash"); //reduceRight 至關於 foldr _.reduceRight([1,2,3],function(sum,n){ return sum-n; }); // 輸出 0
咱們看到這個結果是0
也算是預期結果,由於3-2-1=0
。
注:上文爲了易於理解和行文連貫,加入了一些我本身的理解。須要說明的是,在Haskell中,
foldl1
函數應該是和JavaScript的reduce
(lodash)函數是一致的,foldl1
函數將會把列表的第一個元素做爲初始值。
如今咱們總結一下foldr
和foldl
的一些思路:
若是對列表[3,4,5,6]
應用函數f
初始值爲z
進行foldr
的話,應該理解爲:
f 3 (f 4 (f 5 ( f 6 z))) -- 當 f 爲 +, z = 0 上式就變爲: 3 + (4 + (5 + (6 + 0))) -- 前綴(+)形式則爲: (+)3 ((+)4 ((+)5 ((+)6 0)))
若是對列表[3,4,5,6]
應用函數g
初始值爲z
進行foldl
的話,應該理解爲:
g(g (g (g z 3) 4) 5) 6 -- 固然咱們也能夠相似地把 g 設爲 +, z = 0, 上式就變爲: (((0 + 3) + 4) + 5) + 6 -- 改爲前綴形式 (+)((+)((+)((+)0 3) 4) 5) 6
從上面的例子能夠看出,左摺疊(foldl
)和右摺疊(foldr
)二者有一個很關鍵的區別,就是,左摺疊沒法處理無限列表,可是右摺疊能夠。
前面咱們說的都是用預約義的函數+
,-
,*
…,(在函數式編程裏,這些運算符其實也都是函數)用這些函數是爲了可以讓咱們更加便於理解,如今咱們看看用咱們本身定義的函數呢?試試逆轉一個列表:
reverse
Haskell
flip' :: (a -> b -> c) -> b -> a -> c flip' f x y= f y x
上面的flip'
函數的做用就是傳入第一個參數是一個函數,而後將兩個參數的順序調換一下(flip
是預約義函數)。
Hasekll
foldr flip' [] [1,2,3]
那麼JavaScript的實現呢?
JavaScript
function flip(f, a, b){ return f(b,a); } //這個函數須要進行柯里化,不然沒法在foldr中做爲參數傳入 var flip_ = _.curry(flip); function cons(a,b){ return a.concat(b); } function reverse(list){ return foldr(flip_(cons),[],list); }
調用結果又是怎麼樣的呢?
console.log(reverse([1,2,3])) // [ 3, 2, 1 ]
好了,如今咱們好像又看到了一個新東西——curry
,柯里化。簡單地說,柯里化就是一個函數能夠先接受一部分參數,而後返回一個接受剩下參數的函數。用上面的例子來講,flip
函數在被柯里化以後獲得的函數flip_
,能夠先接受第一個參數cons
而後返回一個接受兩個參數a,b
的函數,也就是咱們須要的鏈接函數。
在go語言裏面,實現curry是一個很麻煩的事情,所以go的函數式編程支持仍是比較有限的。
接着咱們試試如何取得一個列表的長度,實現一個length
函數:
length
Haskell
-- 先定義實現一個count 函數 count :: a -> b ->c count a n = n + 1 -- 再實現一個length函數 length' = foldr (count) 0 -- 再調用 length' [1,2,3,4] -- 4
JavaScript
//先定義一個count函數 function count(a,n){ return n + 1; } //再實現length函數 function length(list){ return foldr(count,0,list); } //調用 console.log(length([1,2,3,4])); // 4
就是這麼簡單,好了,reduce
咱們講完了,而後咱們看看map
,要知道map
函數是怎麼來的,咱們要從一個比較簡單的函數先入手,這個函數的功能是把整個列表的全部元素乘以2:
doubleall
haskell
-- 定義一個乘以2,並鏈接的函數 doubleandcons :: a -> [a] -> [a] doubleandcons x y = 2 * x : y doubleall x = foldr doubleandcons [] -- 調用 doubleall [1,2,3] -- 輸出 -- [2,4,6]
JavaScript
function doubleandcons(a,list){ return [a * 2].concat(list) } function doubleall(list){ return foldr(doubleandcons,[],list) } //調用 console.log(doubleall([1,2,3])); // [2,4,6]
再來看看go怎麼寫:
go
go 的尷尬之處在於,須要很是明確的函數定義,因此咱們要從新寫一個foldr
函數,來接受第二個參數爲列表的f
。
func foldr2(f func(a int,b []int) []int,z []int,list []int)[]int{ if len(list) == 0{ return z } return f(list[0],foldr2(f,z,list[1:])) }
而後咱們再實現同上面相同的邏輯:
func doubleandcons(n int,list []int) []int{ return append([]int{n * 2},list...) } func doubleall(list []int) []int{ return foldr2(doubleandcons,make([]int,0),list) } // doubleall([]int{1,2,3,4}) //[2 4 6 8]
go這門強類型編譯語言雖然支持必定的函數式編程,可是使用起來仍是有必定侷限性的,起碼代碼複用上仍是不如js的。
接下來咱們關注一下其中的doubleandcons
函數,這個函數其實能夠轉換爲這樣的一個函數:
fandcons f el [a]= (f el) : [a] double el = el * 2 -- 只傳入部分參數,柯里化 doubleandcons = fandcons double
如今咱們關注一下這裏的fandcons
,其實這裏能夠通用表述爲Cons·f
,這裏的·
稱爲函數組合。而函數組合有這樣的操做:
$$
(f. g) (h) = f (g(h))
$$
那麼上面的咱們的函數就能夠表述爲:
$$
fandcons(f(el)) = (Cons.f)(el)= Cons (f(el))
$$
因此:
$$
fandcons(f(el),list) = (Cons.f) ( el , list) = Cons ((f(el)) ,list)
$$
最終版本就是:
$$
doubleall = foldr((Cons . double),Nil)
$$
這裏的foldr(Cons.double)
其實就是咱們要的map double
,那麼咱們的map
的原本面目就是:
$$
map = foldr((Cons.f), Nil)
$$
這裏的
Nil
是foldr
函數的初始值。
好了map
已經現身了,讓咱們再仔細看看一個map
函數應該怎麼實現:
map
Haskell
fandcons :: (a->b) ->a -> [b] -> [b] fandcons f x y= (f x):y map' :: (a->b) -> [a] -> [b] map' f x = foldr (fandcons f) [] x -- 調用 map' (\x -> 2 * x) [1,2,3] -- 輸出 [2,4,6]
這裏用了Haskell的lambda表達式,其實就是f
的double
實現。
咱們也看看js版本的實現:
JavaScript
function fandcons(f, el, list){ return [f(el)].concat(list); } //須要柯里化 var fandcons_ = _.curry(fandcons); function map(f, list){ return foldr(fandcons_(f),[],list); } //調用 console.log(map(function(x){return 2*x},[1,2,3,4])); // 輸出[ 2, 4, 6, 8 ]
這些須要柯里化的go我都不實現了,由於go實現柯里化比較複雜。
最後咱們再看看map
的一些神奇的操做:
矩陣求和
summatrix
Haskell
summatrix :: Num a => [[a]] -> a summatrix x = sum (map sum x) -- 調用 summatrix [[1,2,3],[4,5,6]] -- 21
這裏必定要顯式聲明 參數a的類型,由於sum函數要求Num類型的參數
JavaScript
function sum(list){ return foldr(add,0,list); } function summatrix(matrix){ return sum(map(sum,matrix)); } //調用 mat = [[1,2,3],[4,5,6]]; console.log(summatrix(mat)); //輸出 21
在學習函數式編程的過程當中,我感覺到了一種新的思惟模式的衝擊,彷彿打開了一種全新的世界,沒有循環,甚至沒有分支,語法簡潔優雅。我認爲做爲一名計算機從業人員都應該去接觸一下函數式編程,可以讓你的視野更加開闊,可以從另外一個角度去思考。
原文發佈於本人我的博客,保留文章全部權利,未經容許不得轉載。