关于函数式编程:前端函数式编程浅析

39次阅读

共计 8258 个字符,预计需要花费 21 分钟才能阅读完成。

前言

在浅析函数式编程之前,咱们须要明确两个前导概念,即:编程范式(Programming Paradigm)与设计模式(Design Pattern):

对于编程范式(Programming Paradigm),维基百科给出的定义如下:

Programming paradigms are a way to classify programming languages based on their features. Languages can be classified into multiple paradigms.

能够看出,编程范式是一种组织代码的形式,它与各大语言的特点(特地是语言设计及编译器)非亲非故;

而设计模式(Design Pattern),维基百科给出的定义如下:

In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design.

从定义能够得出:设计模式是一种通用的解决方案,编程范式与语言的特点是强相干的,而设计模式则是任何语言都能够依据其特点进行实现的一种通用模板

从前导的概念,咱们能够理解,函数式编程是一种编程范式而不是一种设计模式,因此其与语言是强相干的,从上图能够看出,对于编程范式不能独自通过某一属性或某一边界将其辨别开,尤其是古代高级语言都已根本借鉴了其余语言的特色与办法,所以,目前大多数文章或教材中对编程形式的辨别办法都是做点状剖析,因为其从全局明确辨别的确比拟艰难,而常见的能够泛泛的将编程范式分为:命令式编程和申明式编程。其中命令式编程包含面向过程编程及面向对象编程(也有说面向对象编程属于元编程),而申明式编程包含函数式编程、逻辑式编程、响应式编程,咱们不谨严的能够简略的将常见编程范式进行简化为上图所示分类

概念

前言中介绍了编程范式是与语言强相干的,因此函数式编程也是语言强相干的,最早的函数式编程语言是 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 能够实现函数式编程的特点,那么函数式编程都有什么特点,或者说怎么样组织代码就是函数式编程了?

In computer science, functional programming is a programming paradigm where programs are constructed by applying and composing functions.

维基百科中给出的定义是应用函数来组合和利用的编程范式,那么这里边最外围的就是函数,那么什么是函数?咱们来看一下函数或者函数式编程在数学中的实践根底

函数

设 F 为二元关系,若任意 x ∈ domF 都存在惟一的 y ∈ ranF 使 xFy 成立,则称 F 为函数。对于函数 F,如果有 xFy,则记作 y = F(x),并称 y 为 F 在 x 的值。

从《离散数学》中的定义能够看出,函数式是一种非凡的二元关系。简略来说,函数是连贯两种实体的一种媒介关系,在编程中常见的就是 输出 -> 输入 的一种关系。从这里咱们能够看出,输出什么,输入是有一个预期冀望能够取得的,一般来说,咱们只想解决输出的局部,对非输出的局部尽量做到不影响或者说隔离,那么当输入不合乎咱们的预期,即输入会影响其余输出意外的数据时候,咱们就说产生了副作用(Side Effect);而如果输出什么同时可能输入雷同的后果,咱们就称这样的函数为纯函数(Pure Function),这时纯函数对于它的执行环境不会带来任何的扭转,咱们就说这种现实状况下函数对于环境是援用通明(Referential Transparency)的。

λ 演算

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)等演算模式,从而能够看出函数式编程具备如下特点:1、管道化链式调用;2、惰性求值、惰性加载;3、操作符操作,暗藏外部细节;4、不可变数据。

幺半群

设 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

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

对于 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 Hooks

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

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

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

Ramda

作为真正的 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)”一样,所有办法和模式都是为人服务的,思维才是最重要的,办法只是伎俩,共勉!

参考

  • 前端函数式演进
  • JavaScript 函数式编程指南
  • 离散数学(第 2 版)
  • 函数式编程理解一下(上)
  • 函数式编程理解一下(下)
  • 领域论
  • 领域论学习笔记
  • Category Theory Course
  • Category Theory
  • 领域论简史
  • 10 种编程语言实现 Y 组合子
  • 编写高质量可保护的代码:编程范式
  • 聊聊编程范式
  • 轻松玩转函数式编程
  • Stanford CS107 Programming Paradigms
  • Programming paradigm
  • Software design pattern
  • JavaScript at 20
  • 函数式编程入门教程
  • 前端 | React Hooks 在 SD-WAN 我的项目中的实际

正文完
 0