javascript函数式编程
19 December 2016

由于react和redux以及一群中间件的火起,越来越多的人关注函数式编程。我也去了解了下,我看了《javascript函数式编程》一书,也看了redux源码,总结一下自己的理解。文中会有书中的理论,也会有自己从redux中对这些理论的实践的思考。

javascript 与函数式编程

老实说,我的感受是js不是一门很适合函数式编程的语言,这个语言的设计模式更适合面向对象编程,因为从语言层面上,他并没有不可变数据,还是弱类型的数据。但是拥有强大的原型及原型链体系,加上es6、es7规范出来后从语言层面逐渐引入了类和继承。

如果真的想深入了解函数式编程,社区建议去看看HaskellClojureScriptElm,这些一开始就将函数式思想嵌入到语言层面的语言。

但是函数在js中是first class citizen,函数可以被当做参数传入函数,也能被函数返回,这就给函数式编程带来了环境。而且闭包的方式也能创造私有变量,保证不受污染。

函数式编程与面向对象编程各有优势,当面对一些场景使用函数式模型更方便时,多一个工具在面前,倒是也值的去研究一番。

函数式编程的常见手法

curry柯里化

柯里化函数为每一个逻辑参数返回一个新函数。柯里化函数逐渐返回消耗参数的函数,直到所有参数耗尽。

实现

function curry(fun){
    return function(arg){
        return fun(arg)
    }
}

这是简单的2个参数函数的柯里化实现,还可以实现3个、4个参数,譬如:

function curry3(fun){
    return function(last){
        return function(middle){
            return function(first){
                return fun(first,middle,last)
            }
        }
    }
}

ps:还是箭头函数更优雅:

curr3 = fun => last => middle => first => fun(first,middle,last)

实践

使用柯里化实现一个将色值rgb转化为16进制值的函数

function toHex(n){
	var hex = n.toString(16);
	return (hex.length<2) ? [0,hex].json(''):hex;
}

function rgbToHexString(r, g, b){
	return ["#",toHex(r),toHex(g),toHex(b)].join('');
}

var blueGreenish = curry3(rgbToHexString)(255)(200);
blueGreenish(0);
// #00c8ff

redux的applyMiddleware串联的中间件的结构就是典型的柯里化手法:

({ dispatch ,  getState })  =>  next  =>  action  =>  { 
  //middleware content here
}

如著名的库redux-thunk中的thunkMiddleware中间件的实现:

function thunkMiddleware(_ref) {
  var dispatch = _ref.dispatch;
  var getState = _ref.getState;

  return function (next) {
    return function (action) {
      return typeof action === 'function' ? action(dispatch, getState) : next(action);
    };
  };
}

这样在我们使用异步action时调用的dispatch(actionFun)就是thunkMiddleware封装的dispatch,actionFun能够取到dispatch和getState;如果是同步action,也就是action对象时,则直接运行next(action),这个next,是上一个中间件封装吐出来的dispatch方法。

作用

柯里化可以逐步接受参数进行“懒执行”,能按步骤处理参数,保存参数,为未来的调用做准备。

可以为每一个参数进行一定的处理操作,而且这种处理是“懒处理”,调用一次处理一个参数。这种方式对同一个函数实现部分定制化,譬如blueGreenish函数利用柯里化的方式将色值确定在蓝绿色段,再传入第4个参数得到蓝绿色段的不同色值。当然,我们还可以通过控制第2、3个参数得到红黄色段的处理函数。

使用柯里化比较容易产生流利的函数式API。Haskell中,函数式默认柯里化的。柯里化的用法比直接使用匿名函数要容易阅读,代码阅读起来越像他的行为描述越好。

缺陷

js的函数不是默认柯里化的,对于不能参数个数的函数实现柯里化需要大量手动转换,或者借助于Underscore这样的库。柯里化对于函数参数数目不确定的情况更是束手无策,灵活性不够。

部分应用

部分应用是一个“部分”执行,等待接收剩余的参数立即执行的函数。

部分应用不是一个个消耗参数,而是先处理一部分参数,等到另一部分参数传入调用后,再处理这部分剩下的参数。

实现

function partical(fun,/*,pargs*/){
	var pargs = _.rest(arguments);
	return function(/*arguments*/){
		var args = cat(pargs,_.toArray(arguments));
		return fun.apply(fun,args);
	}
}

(使用了Underscore)

高阶函数

这个概念比较基础,就不说了,就是函数的返回值也是函数,可嵌套任意层。

实践

react的高阶组件。

高阶组件就是一个高阶函数,通过传入的组件参数,对组件做一层封装和参数预处理,实现组件差异化。用高阶组件来说高阶函数有点不合形式,但是思想是一样的(其实react组件也可以用纯函数生成,而不是这种对象模式啊。)

当你有几个组件非常相似,但是又有部分不同时,这时可以封装成高阶组件来处理,传入的组件参数带有差异化的props,甚至是不同的组件,高阶组件内会做一些通用化的处理。

譬如搜索框组件:

const AsyncSelectDecorator = Wrapper => {
  return  class WrapperComponent extends Component {
    componentDidMount() {
      const { url } = this.props;
      
      fetch(url)
      .then(response => response.json())
      .then(data => {
        this.setState({
          data,
        });
      });
    }

    render() {
      const { data } = this.state;
      return (
        <Wrapper
          {...this.props}
          data={data}
        />
      );
    }
}

获取数据接口是通用的,但是搜索框ui样式可能各异,这时候我们可以通过传入的Wrapper组件来定制化搜索样式,而高阶组件WrapperComponent则完成取数据这种通用化操作。

作用

高阶函数可以实现函数的复合,达到更多的组合目的。

函数组合

compose函数能够实现函数的管道连接,简单说就是按一定顺序执行传入的函数参数,通常是从右至左,前一个函数的返回结果将成为下一个函数的参数。

实现

compose:从右到左
function compose(...funcs) {
  funcs = funcs.filter(func => typeof func === 'function')

  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

相当于 compose(a,b,c)(...args)的结果是

function(...args) {
  a(b(c(...args)))
}
pipe:从左到右
const pipe = (fns) => (x) => fns.reduce((v, f) => f(v), x)

eg:

const add1 = (a) => a + 1
const times2 = (a) => a * 2
const times2add1 = pipe([times2, add1])
times2add1(5) // => 11

实践

这也是applyMiddleware,redux丰富的中间件连接得以运转的核心模式:

function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

当我们使用中间件,如:

const store = applyMiddleware(
  thunkMiddleware,
  logger
)(createStore)(rootReducer, initialState);

applyMiddleware中的compose函数从右至左执行传入的函数,函数的运行结果作为参数传给下一个函数,在applyMiddleware就是不断逐层封装dispatch,使中间件在action派发时能对其做相应处理,实现强大的功能。

作用

compose实现了一种函数组合方式,这种端至端的管道连接,能够有序的完成一系列操作,保证了函数的输入和输出规范后,还可以自由组合符合规则的函数,实现丰富的功能。

管道

管道是将一群函数链接起来,上一个函数的执行结果作为下一个函数的参数

链接模式有利于给对象的方法创建流畅的API。

function pipeline(seed /*,args*/ ){
    return _.reduce(_.rest(arguments),
      function(l,r) {
        return r(l)
      },
      seed
    )
}

在js中使用管道,再加上柯里化和部分应用,提供了强有力的方式来组合流畅的函数。

但当数据从一个函数流到下一个时,经常会被间接和深嵌套函数阻碍。用管道可以使得数据流更加明确。但是管道并不适合所有情况,譬如带有副作用的I/O操作、Ajax调用或突变,因为他们数据流出不明确。

管道要求每个函数输入输出数据格式一致,这样才能让数据继续流到下一个函数被处理,这就要求我们做好控制流。抽象出一个通用的数据格式作为数据流。譬如redux中的action对象,抽出来就是{type:ACTION_TYPE, payload:data},这个action是每一个中间件的输入,也是输出,每个中间间内部通过getState接口获得最新的state,并能处理state暂存后用。

从redux中看函数式编程的实践

redux以及其连接的中间件都是非常棒的函数式编程的实践。

redux很精简,源码加起来也不超1000行,拥有几个核心函数createStore, combineReducers,bindActionCreators,applyMiddleware

createStore

除去容错代码,核心的dispatch,subscribe,getState的实现代码:

export default function createStore(reducer, preloadedState, enhancer) {
  
  if (typeof enhancer !== 'undefined') {
    return enhancer(createStore)(reducer, preloadedState)
  }

  var currentReducer = reducer
  var currentState = preloadedState
  var currentListeners = []
  var nextListeners = currentListeners
  var isDispatching = false

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  function getState() {
    return currentState
  }

  function subscribe(listener) {

    var isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      var index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    var listeners = currentListeners = nextListeners
    for (var i = 0; i < listeners.length; i++) {
      var listener = listeners[i]
      listener()
    }

    return action
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState
  }
}

比较有趣的是tsubscribe的实现,它返回了一个函数用来解除绑定,使用姿势:

let unsubscribe = store.subscribe(handleChange)
unsubscribe()

这种方式去除了冗余代码,非常优雅简洁。

如果传入enhancer 传进createStore 里面的话,那基本上得到的store 就会是enhancer 包装出来的结果。

ps官方使用示例:

const store = createStore(
  reducer,
  preloadedState,
  applyMiddleware(...middleware)
)

仔细一看,跟我之前提到的接入了redux-thunk的thunkMiddleware其实是一样的,thunkMiddleware的方式更直接。所以函数式还是蛮绕的

const store = applyMiddleware(
  thunkMiddleware,
  logger
)(createStore)(rootReducer, initialState);

combineReducers

去除容错代码,核心代码是一个高阶函数:

export default function combineReducers(reducers) {
  
  var finalReducerKeys = Object.keys(reducers)

  return function combination(state = {}, action) {
    var hasChanged = false
    var nextState = {}
    for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i]
      var reducer = finalReducers[key]
      var previousStateForKey = state[key]
      var nextStateForKey = reducer(previousStateForKey, action)
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
  }
}

combineReducers在creatStore前被调用,譬如:

const rootReducer = combineReducers({a,b,c});
const store = applyMiddleware(
  thunkMiddleware,
  logger
)(createStore)(rootReducer, initialState);

返回函数在dispatch中被调用用于获得新状态。

bindActionCreator

function bindActionCreator(actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args))
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  var keys = Object.keys(actionCreators)
  var boundActionCreators = {}
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i]
    var actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

bindActionCreator返回对象或者函数,对象的属性也是函数,这个函数返回是dispatch调用的actionCreator函数调用结果,所以actionCreator的调用结果能够被继续dispatch下去。也就是通过bindActionCreator可以注入dispatch,且通过connect将bindActionCreator返回结果注入到view层中,这样我们在view层调用action只用使用this.props.actions.funA(),而不用处处import {funA} from actionA ,然后dispatch(funA())了。

这种语法糖带来了便捷。

applyMiddleware已分析过。

总的来看,函数是一个功能的单元,函数的作用域是隔离的,高阶函数的调用让我们既能保护函数内容私有变量,又能获得函数的引用,被多处调用。

核心

数据流、纯度

纵观函数式编程得益点多是纯函数链接成管道,数据流流入流出,保持不变性。所以函数式编程的要素是保持函数的纯度。

因此,函数式系统努力减少可见的状态修改。向一个遵循函数式原则的系统添加新功能,需要在一个局限的隔离的上下文环境中(独立作用域),进行无破坏性的数据转换,来实现新函数。

函数式编程以命令式的方式构建系统,并通过将显性的状态改变缩减到最小来变得更加模块化。实际应用中,想要完全消除状态改变可能不太现实,但是我们要将任何已知系统中的突变尽量压缩到最少,突变影响范围做到可控。

适用场景

函数式编程风格非常适合开发一些中间件和独立的模块。譬如日志统计、安全控制、异常处理。

把一些跟核心业务逻辑模块无关的功能抽离出来。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。

面向对象编程 vs 函数式编程

面向对象编程

面向对象编程的主要目标是问题分解,每个小模块是一个类,多个类组合在一起形成更大的功能集群。基于每个部件和他们之间的组合关系,我们可以从部件之间的交互和值来描述一个系统。

缺陷

在一个面向对象系统的内部,我们发现对象建的交互会引起各个对象内容状态的变化,而整个系统的状态转变则是由许许多多小的,细微的变化混合来形成的,这些相互关联的状态变化形成了一个变化网,系统越大越难搞清楚究竟变化源在哪里,影响面积有多大,总是给系统的维护和继续开发带来麻烦。

函数式编程

函数式编程的方法解决问题,也会将一个问题分成几部分来解决。与面向对象方法将问题分解成多组“名词”或对象不同,函数式方法将相同的问题分解成多组“动词”或函数。组合单个函数来构建更大的函数,实现更加抽象的系统。

一种将函数式部件构成一个完整系统的方法是取一个值,逐渐的将它“改变”成另一个值,也就是形成数据流。

优势

函数式编程相对于面向对象编程,会尽量减少可见的状态修改。

因为数据流和函数纯度的保证,对于测试非常友好

劣势

保持函数式纯函数需要做很多额外的努力,因为js语言层面上的几种基本类型数据都是可变的。

团队的接受度和认可度。大多数人习惯的是面向对象的风格,函数式的代码初读会很懵逼(譬如({ dispatch , getState }) => next => action => { //middleware}),高阶函数的内容并不那么直观,对调用也有一定要求。所以如果多人合作的团队只有你一人热衷于函数式,这种风格肯定是不能在项目中发展的,因为会给其他人带来困惑和维护合作上的困难。

选择最合适的方式

在实践中我们不应该追求纯粹的函数式编程或者面向对象编程方式,而是哪种方式更合适就选择哪种,需要参考具体应用场景和团队习惯。

甚至我们可以结合2者,来实现一个功能丰富又足够灵活的系统,譬如Mixins的手法。

番外

《javascript函数式编程》

这本书收到的赞扬在我看来有点过了,里面很多结论来的比较突然,研究好几遍还是不明白结论怎么来的,甚至一些结论并没有给出依据。当然也有可能我读的是翻译版,比较生硬。

使用Underscore作为示例也是一个不太友好的地方,没接触过这个库看示例会很不习惯,总是会不由自主脑补js原生函数的写法。当然作者写这本书的时候可能这个库比较火。这个库也是函数式规范的一个很好实践,我看了3本与函数式编程相关的书籍的示例都是依赖这个库的。。。

书的前几章比较基础的介绍概念,一群名词,比较生硬,很多解释也很唐突,还不如去搜一些其他的资料了解下基础。但是从第七章开始,开始有很棒的函数式思想贯穿,示例也变得完整和系统性了,7、8、9章还是很值得读的,介绍了纯度、不变性和对变化的控制,介绍了基于流的链接、管道,数据流和控制流,以及无类编程,重点给出很好的示例体现了Mixins的强大。书中有一些示例很不错,譬如错误验证,前置条件和后置条件的逐步添加,很好的体现了函数式控制流的灵活性。

关于“流行”模式

看完《javascript函数式编程》,会觉得redux并不是什么新东西,里面的模式都是函数式编程的常见手法,几个关键实现譬如 dispatchactionthunk都在书中有基础示例,而这本书比redux诞生的早的多。

所以前端的很多新流行起来的东西其实在一些老玩家面前并不新鲜,可能就是某些经典模式的实践,或者某些后端已有的模式譬如之前的mvvm,前端起步比较晚,正在快速吸收其他程序领域的经典实践,新手才感觉变化大吧,估计老玩家不会有我这种风起云涌的感受。