編寫程序是每個程序員的事,如何編寫出高效的,易用的,健壯的程序呢?跟編程方式有很大關系,過程式,對象式和函數式,哪一個適合你?
面向對象
面向對象是一種設計思想,面向對象的核心是 類 (class)和 對象 (object),通過類來抽象現實世界,通過對象來模擬現實世界。 面向對象的難點在于抽象,抽象的好壞很大程度決定了整個程序設計的好壞。
面向對象的設計中會有很多的層次結構,然后現實世界很多時候并沒有那么多層次結構, 這時,如果強行用面向對象的設計方式,反而會把問題復雜化,也會讓應對變化沒那么容易,所以產生了 設計模式 這種概念。 設計模式被很多人推崇,個人感覺設計模式進一步讓設計遠離現實世界,把對解決實際問題的關注變為對重構代碼的關注。
當然,面向對象也有它明顯的優勢,在代碼組織上結構清晰,有嚴格的訪問控制,同時簡單易懂,相對于函數式編程,更容易上手。
面向對象核心概念
- 抽象
- 多態
- 繼承
- 封裝
函數式編程
函數式編程其實是比面向對象更早的編程方式,但是由于其對使用者有更高的要求(主要在代碼組織方面,將實際問題轉換為函數方面), 所有面向對象設計方式出現之后,一度被忽略,成為一種小眾的編程方式。
函數式的編程范式使得它更加適用于復雜數據處理,高并發的環境,這也是函數式編程最近又興起的原因之一。
- 函數是一等公民,也就是函數和變量等其他數據類型一樣使用
- 沒有副作用,函數保持獨立,和外部的交互僅限于 函數參數 和 返回值
函數式核心概念
- 不變性
- 純函數
- 高階函數
函數式 和 面向對象 比較
面向對象核心是狀態,函數式核心是數據
所以面向對象更適合對業務(復雜的狀態變化)的設計,而函數式適合對功能(復雜的數據變化)的設計, 我想,這也是面向對象應用廣泛的原因之一,畢竟大部分人接觸的都是業務開發。
隨著面向對象設計方式的發展,理論是越來越完善,復雜度也越來越高,面向對象的設計方式很多時候不再把目光投向實際的問題, 而是追求所謂的設計技巧。 函數式編程則更加直接,將問題轉化為對數據的處理,關注點更容易集中在問題本身。
函數式 和 AI
函數式編程能夠再度火起來,和 AI 也有一定的關系,機器學習本身就是對大量數據的學習和處理,通過數據來訓練出算法。 這種模式更加適合函數式編程,而面向對象面對這種未知結果的學習,抽象會非常困難。
理解函數式編程
看了上面的介紹,估計大家對函數式編程還是不清楚,下面來一一舉例說明(來自阮老師博客)
函數式編程的起源,是一門叫做范疇論(Category Theory)的數學分支,理解函數式編程的關鍵,就是理解范疇論。它是一門很復雜的數學,認為世界上所有的概念體系,都可以抽象成一個個的"范疇"(category)。
范疇論與函數式編程
范疇論使用函數,表達范疇之間的關系。
伴隨著范疇論的發展,就發展出一整套函數的運算方法。這套方法起初只用于數學運算,后來有人將它在計算機上實現了,就變成了今天的"函數式編程"。
本質上,函數式編程只是范疇論的運算方法,跟數理邏輯、微積分、行列式是同一類東西,都是數學方法,只是碰巧它能用來寫程序。
函數的合成與柯里化
函數式編程有兩個最基本的運算:合成和柯里化。
- 函數的合成
如果一個值要經過多個函數,才能變成另外一個值,就可以把所有中間步驟合并成一個函數,這叫做"函數的合成"(compose)。
合成兩個函數的簡單代碼如下。
const compose = function (f, g) {
return function (x) {
return f(g(x));
};
}
- 柯里化
f(x)和g(x)合成為f(g(x)),有一個隱藏的前提,就是f和g都只能接受一個參數。如果可以接受多個參數,比如f(x, y)和g(a, b, c),函數合成就非常麻煩。
這時就需要函數柯里化了。所謂"柯里化",就是把一個多參數的函數,轉化為單參數函數。
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
有了柯里化以后,我們就能做到,所有函數只接受一個參數。后文的內容除非另有說明,都默認函數只有一個參數,就是所要處理的那個值。
函數式之函子
函數不僅可以用于同一個范疇之中值的轉換,還可以用于將一個范疇轉成另一個范疇。這就涉及到了函子(Functor)。
函子的概念
函子是函數式編程里面最重要的數據類型,也是基本的運算單位和功能單位。
它首先是一種范疇,也就是說,是一個容器,包含了值和變形關系。比較特殊的是,它的變形關系可以依次作用于每一個值,將當前容器變形成另一個容器。函子的實現
任何具有map方法的數據結構,都可以當作函子的實現。
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
上面代碼中,Functor是一個函子,它的map方法接受函數f作為參數,然后返回一個新的函子,里面包含的值是被f處理過的(f(this.val))。
一般約定,函子的標志就是容器具有map方法。該方法將容器里面的每一個值,映射到另一個容器。
下面是一些用法的示例。
(new Functor(2)).map(function (two) {
return two + 2;
});
// Functor(4)
(new Functor('flamethrowers')).map(function(s) {
return s.toUpperCase();
});
// Functor('FLAMETHROWERS')
(new Functor('bombs')).map(_.concat(' away')).map(_.prop('length'));
// Functor(10)
上面的例子說明,函數式編程里面的運算,都是通過函子完成,即運算不直接針對值,而是針對這個值的容器----函子。函子本身具有對外接口(map方法),各種函數就是運算符,通過接口接入容器,引發容器里面的值的變形。
因此,學習函數式編程,實際上就是學習函子的各種運算。由于可以把運算方法封裝在函子里面,所以又衍生出各種不同類型的函子,有多少種運算,就有多少種函子。函數式編程就變成了運用不同的函子,解決實際問題。
-
函子of
上面生成新的函子的時候,用了new命令。這實在太不像函數式編程了,因為new命令是面向對象編程的標志。
函數式編程一般約定,函子有一個of方法,用來生成新的容器。
下面就用of方法替換掉new。
Functor.of = function(val) {
return new Functor(val);
};
然后,前面的例子就可以改成下面這樣。
Functor.of(2).map(function (two) {
return two + 2;
});
// Functor(4)
這就更像函數式編程了。
-
函子maybe
函子接受各種函數,處理容器內部的值。這里就有一個問題,容器內部的值可能是一個空值(比如null),而外部函數未必有處理空值的機制,如果傳入空值,很可能就會出錯。
Functor.of(null).map(function (s) {
return s.toUpperCase();
});
// TypeError
上面代碼中,函子里面的值是null,結果小寫變成大寫的時候就出錯了。
Maybe 函子就是為了解決這一類問題而設計的。簡單說,它的map方法里面設置了空值檢查。
class Maybe extends Functor {
map(f) {
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
}
有了 Maybe 函子,處理空值就不會出錯了。
Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
// Maybe(null)
-
Either 函子
條件運算if...else是最常見的運算之一,函數式編程里面,使用 Either 函子表達。
Either 函子內部有兩個值:左值(Left)和右值(Right)。右值是正常情況下使用的值,左值是右值不存在時使用的默認值。
class Either extends Functor {
constructor(left, right) {
this.left = left;
this.right = right;
}
map(f) {
return this.right ?
Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right);
}
}
Either.of = function (left, right) {
return new Either(left, right);
};
下面是用法。
var addOne = function (x) {
return x + 1;
};
Either.of(5, 6).map(addOne);
// Either(5, 7);
Either.of(1, null).map(addOne);
// Either(2, null);
上面代碼中,如果右值有值,就使用右值,否則使用左值。通過這種方式,Either 函子表達了條件運算。
Either 函子的常見用途是提供默認值。下面是一個例子。
Either
.of({address: 'xxx'}, currentUser.address)
.map(updateField);
上面代碼中,如果用戶沒有提供地址,Either 函子就會使用左值的默認地址。
Either 函子的另一個用途是代替try...catch,使用左值表示錯誤。
function parseJSON(json) {
try {
return Either.of(null, JSON.parse(json));
} catch (e: Error) {
return Either.of(e, null);
}
}
上面代碼中,左值為空,就表示沒有出錯,否則左值會包含一個錯誤對象e。一般來說,所有可能出錯的運算,都可以返回一個 Either 函子。
-
ap 函子
函子里面包含的值,完全可能是函數。我們可以想象這樣一種情況,一個函子的值是數值,另一個函子的值是函數。
function addTwo(x) {
return x + 2;
}
const A = Functor.of(2);
const B = Functor.of(addTwo)
上面代碼中,函子A內部的值是2,函子B內部的值是函數addTwo。
有時,我們想讓函子B內部的函數,可以使用函子A內部的值進行運算。這時就需要用到 ap 函子。
ap 是 applicative(應用)的縮寫。凡是部署了ap方法的函子,就是 ap 函子。
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val));
}
}
注意,ap方法的參數不是函數,而是另一個函子。
因此,前面例子可以寫成下面的形式。
Ap.of(addTwo).ap(Functor.of(2))
// Ap(4)
ap 函子的意義在于,對于那些多參數的函數,就可以從多個容器之中取值,實現函子的鏈式操作。
function add(x) {
return function (y) {
return x + y;
};
}
Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
// Ap(5)
上面代碼中,函數add是柯里化以后的形式,一共需要兩個參數。通過 ap 函子,我們就可以實現從兩個容器之中取值。它還有另外一種寫法。
Ap.of(add(2)).ap(Maybe.of(3));
-
Monad 函子
函子是一個容器,可以包含任何值。函子之中再包含一個函子,也是完全合法的。但是,這樣就會出現多層嵌套的函子。
Maybe.of(
Maybe.of(
Maybe.of({name: 'Mulburry', number: 8402})
)
)
上面這個函子,一共有三個Maybe嵌套。如果要取出內部的值,就要連續取三次this.val。這當然很不方便,因此就出現了 Monad 函子。
Monad(單子) 函子的作用是,總是返回一個單層的函子。它有一個flatMap方法,與map方法作用相同,唯一的區別是如果生成了一個嵌套函子,它會取出后者內部的值,保證返回的永遠是一個單層的容器,不會出現嵌套的情況。
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
return this.map(f).join();
}
}
上面代碼中,如果函數f返回的是一個函子,那么this.map(f)就會生成一個嵌套的函子。所以,join方法保證了flatMap方法總是返回一個單層的函子。這意味著嵌套的函子會被鋪平(flatten)。
-
Monad 函子之IO操作
Monad 函子的重要應用,就是實現 I/O (輸入輸出)操作。
I/O 是不純的操作,普通的函數式編程沒法做,這時就需要把 IO 操作寫成Monad函子,通過它來完成。
var fs = require('fs');
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
});
};
var print = function(x) {
return new IO(function() {
console.log(x);
return x;
});
}
上面代碼中,讀取文件和打印本身都是不純的操作,但是readFile和print卻是純函數,因為它們總是返回 IO 函子。
如果 IO 函子是一個Monad,具有flatMap方法,那么我們就可以像下面這樣調用這兩個函數。
readFile('./user.txt')
.flatMap(print)
這就是神奇的地方,上面的代碼完成了不純的操作,但是因為flatMap返回的還是一個 IO 函子,所以這個表達式是純的。我們通過一個純的表達式,完成帶有副作用的操作,這就是 Monad 的作用。
由于返回還是 IO 函子,所以可以實現鏈式操作。因此,在大多數庫里面,flatMap方法被改名成chain。
var tail = function(x) {
return new IO(function() {
return x[x.length - 1];
});
}
readFile('./user.txt')
.flatMap(tail)
.flatMap(print)
// 等同于
readFile('./user.txt')
.chain(tail)
.chain(print)
上面代碼讀取了文件user.txt,然后選取最后一行輸出。