函數語言程式設計
- 函數語言程式設計是範疇論的數學分支,是一門複雜的數學,認為世界上所有的概念體系都可以抽象出一個個範疇論。
- 成員彼此存某種關係概念、事物、物件等等,解構成範疇。任何事物只要找出他們之間的關係,就能定義成為範疇
- 反應範疇成員之間的關係叫做態射 ,範疇論認為,同一個範疇的所有成員通過“態射”,一個成員可以變形為另外一個成員
總結:
函數語言程式設計不是指用函式程式設計
函數語言程式設計是指用一些數學的方式加上js的語法進行的程式設計 函數語言程式設計裡面沒有if elsewhile等語句 以及變數的概念
函數語言程式設計特性
- 函式式一等公民。所謂“第一等公民”,指的式函式與其他資料型別一樣,處於平等地位,可以賦值給其他變數,也可以作為引數,傳入另外一個函式,或者作為別的函式返回值。
- 不可改變數。在函數語言程式設計中,我們通常理解的變數在函數語言程式設計中也被函式替代了;在函數語言程式設計中變數僅僅代表某個表示式。這裡所說的“變數”是不能被修改的。所有的變數只能被賦一次初值
- map & reduce 他們是常用的函數語言程式設計的方法。
總結:
- 函式是‘第一等公民’
- 只有‘表示式’,不用‘語句’
- 沒有“副作用”
- 不修改狀態
- 引用透明(函式執行只靠引數)
##純函式
對於相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,也不依賴外部環境的狀態。
例如:
let xs = [1,2,3,4,5]; //Array.slice是純函式,因為他沒有副作用,對於固定的輸入,輸出總是固定的 slice√ xs.slice(0,3); xs.slice(0,3); //總是得到的輸出是 [1,2,3,4] splice × xs.splice(0,3);//[1,2,3] xs.splice(0,3);//[4,5] 複製程式碼
優缺點
- 優點 純函式不僅可以有效的降低系統的複雜度,還有很多很棒的特性,例如可快取性
-
缺點例如
//不純的 var min = 18; var checkage = age => age > min; //純的 var checkage = age => age > 18; 複製程式碼
在不純的版本中,checkage不僅取決於引數age 還有外部依賴的變數 min。 純的checkage 把關鍵字18 硬性 編碼在函式內部,擴充套件性較差,所以就引出了函式柯里化來優化這一問題
函式的柯里化
傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數。
例 Ⅰ柯里化上面的checkage函式
var checkage = min => (age => age >min); checkage(18)(20);//true 複製程式碼
例 Ⅱ
//柯里化之前 function add(x,y){ return x + y; } add(1,2) //3 //柯里化之後 function addX(y){ return function(x){ return x + y; } } addX(2)(1) //3 複製程式碼
例 III利用bind 傳遞部分引數 實現柯里化
function foo(p1,p2){ this.val = p1 + p2; } var bar = foo.bind(null,'p1'); var baz = new bar('p2'); console.log(baz.val); 複製程式碼
函式的組合
純函式以及如何把它柯里化寫出的洋蔥程式碼 h(g(f(x))),為了解決函式巢狀的問題,我們需要用到‘函式組合’
const compose = (f,g) => f(x => f(g(x))); var first = arr => arr[0]; var reverse = arr => arr.reverse(); var last = compose(first, reverse); last([1,2,3,4,5]);//5 複製程式碼
Point Free
- 把一些物件自帶的方法轉化成純函式,不要命名轉瞬即逝的中間變數
- 例如 如下函式,我們使用str作為我們中間的變數,但是這個中間變數除了讓程式碼變得長了一點之外是毫無意義的。
const f = str => str.toUpperCase().split(' '); 複製程式碼
轉化為如下風格
var toUpperCase = word => word.UpperCase(); var split = x => (str => str.split(x)); var f = compose(split(' '),toUpperCase); f("abcd efgh"); 複製程式碼
這種風格能夠幫助我們減少不必要的命名,讓程式碼保持簡潔和通用。
宣告式與命令式程式碼
命令式程式碼的意思就是,我們通過編寫一條又一條指令去讓計算機執行一些動作,這其中一般都會涉及到很多繁雜的細節。而宣告式就要優雅很多了,我們通過寫表示式的方式來宣告我們想幹什麼,而不是通過一步一步的指示。
//命令式: let CEOs = []; for(var i = 0; i < companies.length;i++){ CEOs.push(companies[i].CEO); } //宣告式 let CEOs = companies.map(c => c.CEO); 複製程式碼
優缺點
函數語言程式設計的一個明顯的好處就是這種宣告式的程式碼,對於無副作用的純函式,我們完全可以不考慮函式內部式如何實現的,專注於編寫業務程式碼。優化程式碼時,目光只需要集中在這些穩定堅固的函式內部即可。 複製程式碼
相反,不純的函式式的程式碼會產生副作用或者依賴外部系統環境,使用它們的時候總要考慮這些 不乾淨的副作用。在複雜的系統種,這對於程式猿的心智來說是極大的負擔。
惰性求值、惰性函式
在指令式語言中以下程式碼會按順序執行,由於每個函式都有可能改動或者依賴於其外部的狀態,因此必須順序執行。
function somewhatlongOperation(){somewhatlongOperation} 複製程式碼
高階函式
函式當引數,把傳入的函式作為一個封裝,然後返回這個封裝函式,達到更高程度的抽象。
//命令式 var add = function(a,b){ return a + b; }; function math(func,array){ return func(array[0],array[1]); }; math(add,[1,2]);//3 複製程式碼
尾呼叫優化
指函式內部的最後一個動作是函式呼叫。該呼叫的返回值,直接返回給函式。函式呼叫自身,稱為遞迴。如果尾呼叫自身,就稱為尾遞迴。遞迴需要儲存大量的呼叫紀錄,很容易發生棧溢位錯誤,如果使用尾遞迴優化,將遞迴變為迴圈,那麼只需要儲存一個呼叫紀錄,這樣就不會發生棧溢位錯誤了。
function factorial(n){ if(m === 1) return 1; return n * factorial(n -1); } function factorial(n,total){ if(n === 1) return total; return factorial(n-1,n * total) } //ES6強制使用尾遞迴 複製程式碼
console.trace() 檢視呼叫幀
閉包
快取了當前上下文執行環境的詞法作用域
範疇與容器
-
函式不僅可以用於統一範疇之中值的轉換,還可以用於將一個範疇轉成另一個範疇。這就涉及到了函子(Functor )
-
函子是函數語言程式設計裡面最重要的資料型別,也是基本的運算單位和功能單位。它首先是一種範疇,也就是說,是一個容器,包含了值和變形關係。比較特殊的是,它的變形關係可以依此作用於每一個值,將當前容器變形成為另一個容器。
函子 Functor
- Functor 遵守一些特定規則的容器型別。
- Functor 是一個對於函式呼叫的抽象,我們賦予容器自己去呼叫函式的能力。把東西裝進一個容器,只留出一個介面map給容器外的函式,map一個函式時,我們讓容器自己來執行這個函式,這樣容器就可以自由地選擇何時何地如何操作這個函式,以致於擁有惰性求值,錯誤處理,非同步呼叫等等非常牛掰的特性。
var Container = function(x){ this.__value = x; } //函數語言程式設計一般約定,函子有一個 of 方法 Container.of = x => new Container(x); //Container.of('abcd'); //一般約定,函子的標誌就是容器具有map方法。該方法將容器裡面的每一個值,對映到另一個容器。 Container.prototype.map = function(f){ return Container.of(f(this.__value)) } Container.of(3) .map(x => x + 1)//結果為Container(4) .map(x => 'Result is' + x); //結果為 Container('Result is 4') 複製程式碼
map
class Functor{ constructor(val){ this.val = val; } map(f){ return new Functor(f(this.val)); } } (new Functor(2)).map(function(tow){ return tow + 2; }) //Functor(4) 複製程式碼
上面程式碼中,Functor是一個函子,它的map方法接受函式f作為引數,然後返回一個新的函子,裡面包含的值是被f處理過的(f(this.val))。 一般約定,函子的標誌就是容器具有map方法。該方法將容器裡的每一個值,對映到另一個容器。 上面的例子說明,函數語言程式設計裡面的運算,都是通過函子完成,即運算不直接針對值,而是針對這個值的容器---函子。函子本身具有對外介面(map方法),各種函式就是運算子,通過介面接入容器,引發容器裡面的值的變形。
總結:
首先 函子 是一個容器 它特殊在 有map 方法 ,通過map方法 接受一個變形關係,映射出另外一個函子。程式設計面向不直接操作值,而是通過操作函式,將原來的集合 轉化為一個新的集合
of方法
函數語言程式設計一般約定,函子有一個of方法,用來生成新的容器。 所以上面程式碼改寫成
class Functor{ constructor(val){ this.val = val; } static of(val){ return new Functor(val); } map(f){ return new Functor(f(this.val)); } } Functor.of(2).map(function(tow){ return tow + 2; }) //Functor(4) 複製程式碼
maybe 函子 (函數語言程式設計中的 if else)
函子接受各種函式,處理容器內部的值。這裡就有一個問題,容器內部的值可能是一個空值(比如null), 而外部函式未必又處理空值的機制,如果傳入空值,很可能就會報錯。
例如如下程式碼 就熄火了
Functor.of(null).map(function(s){ return s.toUpperCase(); }) class Maybe extends Functor{ map(f){ return this.val?Maybe.of(f(this.val)):Maybe.of(null); } } Maybe.of(null).map(function(s){ return s.toUpperCase(); }) //Maybe(null) 複製程式碼
如何實現 maybe 函子
var Maybe = function(x){ this.__value = x; } Maybe.of = function(x){ return new Maybe(x); } Maybe.prototype.map = function(f){ return this.isNothing()?Maybe.of(null):Maybe.of(f(this.__value)); } Maybe.prototype.isNothing = function(){ return (this.__value === null || this.__value === undefined); } 複製程式碼
錯誤處理、Either
- 我們的容器能做的事情太少了,try/catch/throw 並不是“純”的,因為它從外部接管了我們的函式,並且在這個函數出錯時拋棄了它的返回值。
- Promise是可以呼叫catch來集中處理錯誤的
- 事實上Either並不只是用來做錯誤處理的,它表示了邏輯或與,範疇學裡的coproduct.
Either的實現
條件運算子if..else是最常見的運算之一,函數語言程式設計裡面,使用Either函子表達。Either函子內部又兩個值:左值(left)和右值(Right)。右值是正常情況下使用的值,左值是右值不存在的時候使用的預設值。
class Either extends Functor{ constructor(left,right){ this.left = left; this.right = right; } static of(left,right){ return new Either(left,right); } map(f){ return this.right? Either.of(this.left,f(this.right)): Either.of(f(this.left),this.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.of({address:'xxx'},currentUser.address).map(updateField); 複製程式碼
AP函子
函子裡面包含的值,完全可能是 函式。我們可以想象這樣一個情況,一個函子的值是數值,另一個函子的值是函式。
class Ap extends Functor{ ap(F){ return Ap.of(this.val(F.val)); } } Ap.of(addTwo).ap(Functor.of(2)); 複製程式碼
IO函子
-
真正的程式總要去接觸骯髒的世界。
function readLocalStorage(){ return window.localStorage; } 複製程式碼
- IO跟前面那幾個Functor不同的地方在於,它的__value是一個函式。它把不純的操作(比如IO、網路請求、DOM)包裹到一個函式內,從而延遲這個操作的執行。所以我們認為,IO包含的是被包裹的操作的返回值
- IO其實也算是惰性求值
- IO負責了呼叫鏈積累了很多很多不純的操作,帶來的複雜性和不可維護性
import _ from 'lodash'; var compose = _,flowRight; var IO = function(f){ this.__value = f; } IO.of = x => new IO(_=>x); IO.prototype.map = function(f){ return new IO(compose(f,this.__value)) }; //ES6 寫法 import _ from 'lodash'; var compose = _.flowRight; class IO extends Monad{ map(f){ return IO.of(compose(f,this.__value)) } } 複製程式碼
var fs = require('fs'); var readFile = function(filename){ return new IO(function(){ return fs.readFileSync(filename,'utf-8';) }); }; readFile('./user.txt') .flatMap(tail) .flatMap(print) //等同於 readFile('./user.txt') .chain(tail) .chain(print) 複製程式碼
Monad
- Monad就是一種設計模式,表示將一個運算過程,通過函式拆解成互相連結的多個步驟。你只要提供下一步運算所需要的函式。整個運算就會自動執行下去
- Promise就是一種Monad。
- Monad讓我們避開了巢狀式地獄,可以輕鬆地進行深度巢狀的函數語言程式設計,比如IO和其他非同步任務。
- 記得讓IO繼承Monad.
class Monad extends Functor{ join(){ return this.val; } flatMap(f){ return this.map(f).join(); } } 複製程式碼
Monad函子的作用是,總是返回一個單層的函子。它有一個flatMap方法,與map方法作用相同,唯一的區別是如果生成了一個巢狀函子,它會去除後者內部的值,保證返回的永遠是一個單層的容器,不會出現巢狀的情況。 如果 函式 f 返回的是一個函子,那麼this.map(f)就會生成一個巢狀的函子。所以,join方法保證了flatMap方法總是返回一個單層的函子。這意味著巢狀的函子會被鋪平。