在淺析函數式編程以前,咱們須要明確兩個前導概念,即:編程範式(Programming Paradigm)與設計模式(Design Pattern):html
對於編程範式(Programming Paradigm),維基百科給出的定義以下:前端
Programming paradigms are a way to classify programming languages based on their features. Languages can be classified into multiple paradigms.
能夠看出,編程範式是一種組織代碼的方式,它與各大語言的特色(特別是語言設計及編譯器)息息相關;react
而設計模式(Design Pattern),維基百科給出的定義以下:jquery
In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design.
從定義能夠得出:設計模式是一種通用的解決方案,編程範式與語言的特色是強相關的,而設計模式則是任何語言均可以根據其特色進行實現的一種通用模板git
從前導的概念,咱們能夠了解,函數式編程是一種編程範式而不是一種設計模式,於是其與語言是強相關的,從上圖能夠看出,對於編程範式不能單獨經過某一屬性或某一邊界將其區分開,尤爲是現代高級語言都已基本借鑑了其餘語言的特點與方法,因此,目前大多數文章或教材中對編程方式的區分方法都是作點狀分析,由於其從全局明確區分確實比較困難,而常見的能夠泛泛的將編程範式分爲:命令式編程和聲明式編程。其中命令式編程包括面向過程編程及面向對象編程(也有說面向對象編程屬於元編程),而聲明式編程包括函數式編程、邏輯式編程、響應式編程,咱們不嚴謹的能夠簡單的將常見編程範式進行簡化爲上圖所示分類程序員
前言中介紹了編程範式是與語言強相關的,於是函數式編程也是語言強相關的,最先的函數式編程語言是LISP,Schema語言是Lisp語言的一種方言,而現代語言好比Haskell、Clean、Erlang等也前仆後繼的實現了函數式編程的特點。對於前端程序員而言,咱們使用的語言是JavaScript或TypeScript,然後者是前者的超集,於是能夠算是類JavaScript的語言使用者,對於js而言,因爲其設計者Brendan Eich自己是函數式編程的擁躉,於是其設計上借鑑了Schema的函數第一公民(First Class)的理念(ps:所謂函數第一公民,是指 they can be bound to names (including local identifiers), passed as arguments, and returned from other functions, just as any other data type can.,即函數具備能夠經過名稱綁定、傳遞參數,而且能夠返回其餘函數的特徵),這就爲js的函數式編程埋下了伏筆。既然js能夠實現函數式編程的特色,那麼函數式編程都有什麼特色,或者說怎麼樣組織代碼就是函數式編程了?es6
In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions.
維基百科中給出的定義是使用函數來組合和應用的編程範式,那麼這裏邊最核心的就是函數,那麼什麼是函數?咱們來看一下函數或者函數式編程在數學中的理論基礎github
設 F 爲二元關係,若任意 x ∈ domF 都存在惟一的 y ∈ ranF 使 xFy 成立,則稱 F 爲函數。對於函數 F,若是有 xFy,則記做 y = F(x),並稱 y 爲 F 在 x 的值。
從《離散數學》中的定義能夠看出,函數式是一種特殊的二元關係。簡單來講,函數是鏈接兩種實體的一種媒介關係,在編程中常見的就是 輸入 -> 輸出 的一種關係。從這裏咱們能夠看出,輸入什麼,輸出是有一個預期指望能夠得到的,通常來講,咱們只想處理輸入的部分,對非輸入的部分儘可能作到不影響或者說隔離,那麼當輸出不符合咱們的預期,即輸出會影響其餘輸入意外的數據時候,咱們就說產生了反作用(Side Effect);而若是輸入什麼同時可以輸出相同的結果,咱們就稱這樣的函數爲純函數(Pure Function),這時純函數對於它的執行環境不會帶來任何的改變,咱們就說這種理想狀況下函數對於環境是引用透明(Referential Transparency)的。express
Lambda calculus (also written as λ-calculus) is a formal system in mathematical logic for expressing computation based on function abstraction and application using variable binding and substitution.
λ演算是一種形式系統(ps:數理邏輯中,將形式語言及其對應的轉換規則構成的集合稱爲一種形式系統),這個系統中規定了特殊的形式,好比α轉換和β規約:α轉換(ps:[α-conversion, sometimes known as α-renaming,[21] allows bound variable names to be changed.](https://en.wikipedia.org/wiki... is defined in terms of substitution: the β-reduction of (λV.M) N is M[V := N].](https://en.wikipedia.org/wiki... Function)、組合函數(Compose)等演算形式,從而能夠看出函數式編程具備以下特色:一、管道化鏈式調用;二、惰性求值、惰性加載;三、操做符操做,隱藏內部細節;四、不可變數據。編程
設 V = < S, ο > 是代數系統,ο 爲二元運算,若 ο 是可結合的,則稱V是半羣;若 e ∈ S 是關於 ο 運算的單位元,則稱 V 爲幺半羣(monoid),也叫獨異點,也將獨異點 V 記做 V = < S, ο, e >
這裏涉及到了一些範疇論(ps:不嚴謹的,簡單來講範疇論就是將概念體系進行區分,造成一個個範疇,也就是找到對應的邊界,以及在這個範疇中有本身對應的規則和描述)的概念,這裏將以前說到的函數的概念進行了擴展,前面說到函數式一種特殊的二元關係,而羣是有着二元運算的一些對象,在這些對象中,有一個特殊的對象e,知足一些法則(ps:不嚴謹的,簡單理解爲交換律就是一種規則),則稱爲單位元,也叫幺元。幺半羣定義了函數二元運算的規則,對於元素A運算後仍會獲得A。
Let C and D be categories. A functor F from C to D is a mapping that: associates each object X in C to an Object F(X) in D; associates each morphism f: X -> Y in C to a morphism F(f): F(X) -> F(Y) in D such that the following two conditions hold: F(idx) = idF(x) for every object X in C, F(g ο f) = F(g) ο F(f) for all morphisms f: X -> Y and g: Y -> Z in C.
從定義能夠看出,函子(Functor)在範疇論進行了擴展,其本質是兩個範疇之間的映射關係
A monad on C consists of an endofunctor T:C -> C together with two natural transformations: η: 1c -> T (where 1c denotes the identify functor on C) and μ: T2 -> T (where T2 is the functor T ο T from C to C). These are required to fulfill the following conditions (sometimes called coherence conditions): μ ο Tμ = μ ο μT (as natural transformations T3 -> T); μ ο Tη = μ ο ηT = 1T (as natural transformations T -> T; here 1T denotes the identify transformation from T to T).
從定義咱們能夠簡單給出:單子就是自函子範疇上的幺半羣,其中自函子是指映射另外一個範疇也爲其自己的函子。全部函數式打散以後的核心組織就能夠基於單子形態的編程,於是也稱爲Monadic開發模型。
至此,咱們瞭解了前端函數式編程所涉及到的底層數學依據支撐,那麼接下來咱們就要看一下函數式編程在前端的一些應用狀況
應用部分挑選了幾個比較有表明性的庫,旨在展示一下函數式編程的風格及特性,不會對全部源碼進行分析,畢竟這些庫中所運用的編程方法不止一種,全部的方法都是爲人來服務的,只有思想纔是最重要的,「形而上者謂之道,形而下者謂之器」
jQuery做爲一個重要的js庫,雖然如今已經漸行漸遠,可是在前端發展歷史上其無疑是有着里程碑意義的一個表明,對於jq咱們影響最深的除了其幫助咱們簡化了dom操做,抹平了部分瀏覽器接口差別外,其實其最爲有影響力的當屬它的鏈式調用,咱們來看一下它是如何組織實現這一操做的:
// https://github.com/jquery/jquery/tree/1.12-stable/src/core.js jQuery.fn = jQuery.prototype = { constructor: jQuery, selector: "", length: 0, toArray: function() {}, get: function(num) {}, map: function() {}, slice: function() {}, first: function() {}, last: function() {}, eq: function() {}, end: function() {} }
能夠看出jq的鏈式調用經過constructor進行了一個名稱的複寫,利用js的特性進行了函數循環綁定從而作到了鏈式調用
對於redux,其中的compose及applyMiddleware都是函數式編程的一種理念體現
// https://github.com/reduxjs/redux/tree/master/src/compose.ts export default function compose(...funcs: Funcion[]) { if(funcs.length === 0) { return <T>(arg: T) => arg } if(funcs.length === 1) { return funcs[0] } // compose操做 return funcs.reduce((a,b) => (...args:any) => a(b(...args))) }
// https://github.com/reduxjs/redux/tree/master/src/applyMiddleware.ts export default function applyMiddleware( ...middlewares: Middleware[] ): StoreEnhancer<any> { return (createStore: StoreEnhancerStoreCreator) => <S, A extends AnyAction>( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> ) => { const store = createStore(reducer, preloadedState) const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } // map映射 const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose<typeof dispatch>(...chain)(store.dispatch) return { ...store, dispatch } } }
React 16.8以後推出的react hooks,給函數式組件給予了更大的應用空間,也更符合 React認爲的 ui是一種數據的設計哲學,固然react hooks也是趁着fiber架構的東風,從而將函數式理念體現到了最大,簡單看一下函數式編程的一些片斷,具體關於React hooks的分析,能夠出門右轉看一下做者以前寫的這篇文章前端 | React Hooks在SD-WAN項目中的實踐
// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.new.js function updateReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init?: I => S ): [S, Dispatch<A>] { const hook = updateWorkInProgressHook(); const queue = hook.queue; queue.lastRenderedReducer = reducer; const current: Hook = (currentHook: any); let baseQueue = current.baseQueue; const pendingQueue = queue.pending; if (baseQueue !== null) { const first = baseQueue.next; let newState = current.baseState; let newBaseState = null; let newBaseQueueFirst = null; let newBaseQueueLast = null; let update = first; // 走循環 hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } const dispatch: Dispatch<A> = (queue.dispatch: any); return [hook.memoizedState, dispatch]; }
Reactive Extensions在各個平臺都有,這裏重點分析rx.js相關,其屬於響應式編程,可是這裏會和函數式編程結合,從而實現函數式響應式編程,即Function Reactive Programming,咱們來簡單看一下其flatMap的實現:
// https://github.com/ReactiveX/rxjs/tree/master/src/internal/operators/flatMap.ts export function mergeMap<T, R, O extends ObservableInput<any>>( project: (value: T, index: number) => O, resultSelector?: ((outerValue: T, innerValue: ObservedValueOf<O>, outerIndex: number, innerIndex: number) => R) | number, concurrent: number = Infinity ): OperatorFunction<T, ObservedValueOf<O> | R> { if (isFunction(resultSelector)) { // DEPRECATED PATH return mergeMap((a, i) => map((b: any, ii: number) => resultSelector(a, b, i, ii))(innerFrom(project(a, i))), concurrent); } else if (typeof resultSelector === 'number') { concurrent = resultSelector; } return operate((source, subscriber) => mergeInternals(source, subscriber, project, concurrent)); }
lodash做爲一款工業級的工具庫,其對本來js中的各類操做api都進行了擴展,咱們來看一下其進行分割元素的一些實現:
// https://github.com/lodash/lodash/take.js function take(array, n=1) { if (!(array != null && array.length)) { return [] } return slice(array, 0, n < 0 ? 0 : n) }
做爲真正的lamda演算的一個js庫,其算是基本實現了全部lamda演算的需求
// https://github.com/ramda/ramda/tree/master/source/curryN.js export default function _curryN(length, received, fn) { return function() { var combined = []; var argsIdx = 0; var left = length; var combinedIdx = 0; while (combinedIdx < received.length || argsIdx < arguments.length) { var result; if (combinedIdx < received.length && (!_isPlaceholder(received[combinedIdx]) || argsIdx >= arguments.length)) { result = received[combinedIdx]; } else { result = arguments[argsIdx]; argsIdx += 1; } combined[combinedIdx] = result; if (!_isPlaceholder(result)) { left -= 1; } combinedIdx += 1; } return left <= 0 ? fn.apply(this, combined) : _arity(left, _curryN(length, combined, fn)); }; }
函數式編程做爲一種編程範式,其意義不只僅在於業務的具體實現,更重要的在於整個業態的發展趨勢,流式渲染,先後端同構,結合serverless、faas等相關,同時提供給flink等大數據相關的一些新的接入方案,私覺得這纔是函數式編程現在在前端如此備受關注的更加前瞻的視角;同時,咱們也不該該過渡的依賴函數式編程,認爲全部的代碼組織都得以函數式編程爲主,這樣過猶不及,並不是是一種好的編程理念,正如「軟件工程裏沒有銀彈(No Silver Bullet)」同樣,全部方法和模式都是爲人服務的,思想纔是最重要的,方法只是手段,共勉!