由于react和redux以及一群中间件的火起,越来越多的人关注函数式编程。我也去了解了下,我看了《javascript函数式编程》一书,也看了redux源码,总结一下自己的理解。文中会有书中的理论,也会有自己从redux中对这些理论的实践的思考。
javascript 与函数式编程
老实说,我的感受是js不是一门很适合函数式编程的语言,这个语言的设计模式更适合面向对象编程,因为从语言层面上,他并没有不可变数据,还是弱类型的数据。但是拥有强大的原型及原型链体系,加上es6、es7规范出来后从语言层面逐渐引入了类和继承。
如果真的想深入了解函数式编程,社区建议去看看Haskell
、ClojureScript
、Elm
,这些一开始就将函数式思想嵌入到语言层面的语言。
但是函数在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并不是什么新东西,里面的模式都是函数式编程的常见手法,几个关键实现譬如 dispatch
、action
、thunk
都在书中有基础示例,而这本书比redux诞生的早的多。
所以前端的很多新流行起来的东西其实在一些老玩家面前并不新鲜,可能就是某些经典模式的实践,或者某些后端已有的模式譬如之前的mvvm,前端起步比较晚,正在快速吸收其他程序领域的经典实践,新手才感觉变化大吧,估计老玩家不会有我这种风起云涌的感受。