理解面向對象與函數式

編寫程序是每個程序員的事,如何編寫出高效的,易用的,健壯的程序呢?跟編程方式有很大關系,過程式,對象式和函數式,哪一個適合你?

面向對象

面向對象是一種設計思想,面向對象的核心是 類 (class)對象 (object),通過類來抽象現實世界,通過對象來模擬現實世界。 面向對象的難點在于抽象,抽象的好壞很大程度決定了整個程序設計的好壞。

面向對象的設計中會有很多的層次結構,然后現實世界很多時候并沒有那么多層次結構, 這時,如果強行用面向對象的設計方式,反而會把問題復雜化,也會讓應對變化沒那么容易,所以產生了 設計模式 這種概念。 設計模式被很多人推崇,個人感覺設計模式進一步讓設計遠離現實世界,把對解決實際問題的關注變為對重構代碼的關注。

當然,面向對象也有它明顯的優勢,在代碼組織上結構清晰,有嚴格的訪問控制,同時簡單易懂,相對于函數式編程,更容易上手。

面向對象核心概念

  • 抽象
  • 多態
  • 繼承
  • 封裝

函數式編程

函數式編程其實是比面向對象更早的編程方式,但是由于其對使用者有更高的要求(主要在代碼組織方面,將實際問題轉換為函數方面), 所有面向對象設計方式出現之后,一度被忽略,成為一種小眾的編程方式。

函數式的編程范式使得它更加適用于復雜數據處理,高并發的環境,這也是函數式編程最近又興起的原因之一。

  • 函數是一等公民,也就是函數和變量等其他數據類型一樣使用
  • 沒有副作用,函數保持獨立,和外部的交互僅限于 函數參數 和 返回值

函數式核心概念

  • 不變性
  • 純函數
  • 高階函數

函數式 和 面向對象 比較

面向對象核心是狀態,函數式核心是數據

所以面向對象更適合對業務(復雜的狀態變化)的設計,而函數式適合對功能(復雜的數據變化)的設計, 我想,這也是面向對象應用廣泛的原因之一,畢竟大部分人接觸的都是業務開發。

隨著面向對象設計方式的發展,理論是越來越完善,復雜度也越來越高,面向對象的設計方式很多時候不再把目光投向實際的問題, 而是追求所謂的設計技巧。 函數式編程則更加直接,將問題轉化為對數據的處理,關注點更容易集中在問題本身。

函數式 和 AI

函數式編程能夠再度火起來,和 AI 也有一定的關系,機器學習本身就是對大量數據的學習和處理,通過數據來訓練出算法。 這種模式更加適合函數式編程,而面向對象面對這種未知結果的學習,抽象會非常困難。

理解函數式編程

看了上面的介紹,估計大家對函數式編程還是不清楚,下面來一一舉例說明(來自阮老師博客)
函數式編程的起源,是一門叫做范疇論(Category Theory)的數學分支,理解函數式編程的關鍵,就是理解范疇論。它是一門很復雜的數學,認為世界上所有的概念體系,都可以抽象成一個個的"范疇"(category)。

范疇論與函數式編程

范疇論使用函數,表達范疇之間的關系。

伴隨著范疇論的發展,就發展出一整套函數的運算方法。這套方法起初只用于數學運算,后來有人將它在計算機上實現了,就變成了今天的"函數式編程"。

本質上,函數式編程只是范疇論的運算方法,跟數理邏輯、微積分、行列式是同一類東西,都是數學方法,只是碰巧它能用來寫程序。

函數的合成與柯里化

函數式編程有兩個最基本的運算:合成和柯里化。

  1. 函數的合成
    如果一個值要經過多個函數,才能變成另外一個值,就可以把所有中間步驟合并成一個函數,這叫做"函數的合成"(compose)。
    合成兩個函數的簡單代碼如下。
const compose = function (f, g) {
  return function (x) {
    return f(g(x));
  };
}
  1. 柯里化
    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)。

  1. 函子的概念
    函子是函數式編程里面最重要的數據類型,也是基本的運算單位和功能單位。
    它首先是一種范疇,也就是說,是一個容器,包含了值和變形關系。比較特殊的是,它的變形關系可以依次作用于每一個值,將當前容器變形成另一個容器。

  2. 函子的實現
    任何具有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方法),各種函數就是運算符,通過接口接入容器,引發容器里面的值的變形。

因此,學習函數式編程,實際上就是學習函子的各種運算。由于可以把運算方法封裝在函子里面,所以又衍生出各種不同類型的函子,有多少種運算,就有多少種函子。函數式編程就變成了運用不同的函子,解決實際問題。

  1. 函子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)

這就更像函數式編程了。

  1. 函子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)
  1. 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 函子。

  1. 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));

  1. 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)。

  1. 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,然后選取最后一行輸出。

參考

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容