從Haskell、JavaScript、Go看函數語言程式設計

NO IMAGE

引言

本文就是我在學習函數語言程式設計的過程當中自己體悟到的一些東西,這裡將用go,JavaScript以及Haskell三種語言來分析函數語言程式設計的一些奧祕。JavaScript由於具有的一些優勢能夠讓我們可以實現函數語言程式設計,而go作為一種強型別語言,雖然靈活性又稍有欠缺,但是也能夠完成一些高階函式的實現,Haskell語言作為正統的函數語言程式設計語言,為了解釋說明問題,作為對比參照。

正文

函數語言程式設計也算是經常看到了,它的一些優勢包括:

不包括賦值語句(assignment statement),一個變數一旦初始化,就無法被修改(immutable)
無副作用,函式除了計算結果,將不會產生任何副作用
因為無副作用,所以任何表示式在任何時候都能夠evaluate

雖然上面的優勢看看上去好像很厲害的樣子,但是,到底厲害在哪裡呢?我們可以通過下面的例子進行說明:

求和函式

Haskell

sum [1,2,3]
-- 6
-- sum 的實現其實是
foldr ( ) 0 [1,2,3]

在Haskell中flodr

函式實現是:

-- 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

然後我們再實現js版本的( )

那麼我們的sum

最後我們的js版的sum

像js這樣的弱型別的語言較為靈活,函式f

go因為有陣列切片,所以使用起來較為簡單,但是go又是強型別的語言,所以在宣告函式的時候必須要把型別宣告清楚。

再實現一下f

依葫蘆畫瓢我們可以得到go版本的sum

可以看出來好像套路都差不多,真正在呼叫的時候是這樣的:

func main(){
a := []int{1,2,3}
sum(a) // 6
}

在Haskell中是沒有迴圈的,因為迴圈可以通過遞迴實現,在上文我們實現的sum

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

呼叫:

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

lodash

好吧,我欺騙了你們,其實foldr

為什麼兩個輸出是不同的呢?這個和結合方向有關:

foldr (-) 0 [1,2,3]

這個和說好的-6

我們看到這個結果是0

如果對列表[3,4,5,6]

從上面的例子可以看出,左摺疊(foldl

上面的flip'

那麼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

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

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

然後我們再實現同上面相同的邏輯:

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

這裡用了Haskell的lambda表示式,其實就是f

這些需要柯里化的go我都不實現了,因為go實現柯里化比較複雜。

最後我們再看看map

這裡一定要顯式宣告 引數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

結語

在學習函數語言程式設計的過程中,我感受到了一種新的思維模式的衝擊,彷彿開啟了一種全新的世界,沒有迴圈,甚至沒有分支,語法簡潔優雅。我認為作為一名計算機從業人員都應該去接觸一下函數語言程式設計,能夠讓你的視野更加開闊,能夠從另一個角度去思考。

原文釋出於本人個人部落格,保留文章所有權利,未經允許不得轉載。