函数式编程——入门笔记与React实践
前言最近在看近来很火的函数式编程教程《Mostly Adequate Guide》 (中文版:《JS函数式编程指南》),收获很大。对于函数式编程的初学者,这本书不仅深入浅出,更让人感受到函数式编程的优势和美感,强烈推荐给想要学习函数式编程的朋友。 这篇文章是我个人的一个学习笔记,在总结知识的同时,也尝试以React组件的输入事件响应为例,用函数式编程去应对实际项目中的场景。 下文涉及React的代码出于阅读考虑有一定删减,完整代码在我的Github。 lodash与ramda部分代码由于比较简单,想看运行结果的话可以直接到lodash或ramda官网打开console运行。 纯函数纯函数引用原书的描述:
所谓外部状态,最常见的例子就是this,如果你的函数是: function(){ return 'hello,' + this.name; } 那它不可能是纯函数——你永远不知道this.name会被谁改写,测试用例也不可能覆盖所有情况。如果正巧有一个外部函数,它每隔一个月将this.name改写成 提倡函数式编程的人认为,这种共享状态导致的混乱是绝大多数bug的万恶之源 其实某种程度上这早已成为共识:不提倡全局变量其实就是这个道理。也许深刻意识到纯函数的优势还需要一点时间,也许你觉得纯函数不错,但对于如何在项目中使用它完全没有头绪,不用着急,现在我们暂时先记住:
Curry与Composecurry和compose可以说是函数式编程的众妙之门,而且必须相辅相成才见威力。就我个人而言,见过一些讲函数式的教程,讲了curry,我也知道了什么是curry,但是curry怎么用?能带来什么好处呢?还是没讲清楚,然后马不停蹄地往前讲functor讲monad,作为资质不那么高的函数式菜鸟,很快就云里雾中,不明觉厉了。 Currycurry的本质是函数的 class Form extends React.Component { setField(key){ return (e)=>{ this.setState({ [key]: e.target.value }) } } render(){ const {name,address} = this.state; return ( <form> <input value={name} onChange={this.setField('name')} /> <input value={address} onChange={this.setField('address')} /> </form> ) } } 完整代码 step_1 借助高阶函数式的function return function,对不同的key我们能够复用响应事件并setState的逻辑,上例可以认为就是脱掉了马甲的 换个写法试试: const setFieldOnContext = _.curry(function(context,key,e){ context.setState({ [key]: e.target.value }) }); class Form extends React.Component{ render(){ const {name,address} = this.state; const setField = setFieldOnContext(this); return ( <form> <input value={name} onChange={setField('name')} /> <input value={address} onChange={setField('address')} /> </form> ) } } 完整代码 step_2 部分应用的特性使得我们可以把关注点分散到每一个参数,在render函数中
设定了当前上下文,因为你肯定不会设置其它component的state,而具体到每一个onChange则关注不同的目标key。 也许你会想curry是让代码变得好看了一点,但也仅此而已,它只是用新的姿势解决问题,并没有解决新的问题或产生新的价值。 当然不是,curry真正产生的价值和魅力的地方,是它对组合的友好。 Compose对逻辑进行组合,这样的需求其实很常见,当我想要:
很多时候会写成这样: import _ from 'lodash' function filterFn(v){ return typeof v === 'number'; } function sortFn(v){ return Math.abs(v); } _.sortBy(_.filter(_.uniq([1,1,3,4,2,'a',-10]),filterFn),sortFn); // -> [1,-10] 嵌套的代码难以阅读,就像回调地狱一样。自然的逻辑应该是顺序而非嵌套的,因此很多人会更喜欢”链式“写法: _([1,-10]). uniq(). filter(filterFn). sortBy(sortFn). value(); // -> [1,-10] 看起来顺眼多了,用瓶子把东西封起来操作的思路很棒( 一个典型的场景是代码调试:我们想知道每一步的返回值,以便定位问题。然而无论是单步调试还是log打印,在面对链式代码时都显得有些束手无策(chrome devtool可以选中部分代码并执行,但对编译生成的代码不管用),如果你不想每次debug都把要打印的值扔给临时变量搞得一地鸡毛的话,或许可以这样: //_ is lodash _.prototype.log = function log(label){ var value = this.value(); console.log(label,value); return _(value); }; _([1,-10]). uniq(). log('does uniq() works right? '). filter(filterFn). sortBy(sortFn). value(); 可惜lodash原生并没有提供这样的log函数。这不难理解,原型链有尽而需求场景无穷,扩充原型来满足业务场景是注定被动的。 即使你打算打破教条
决定像上面代码一样扩充第三方对象的原型,这个log函数仍然有太多怪异的地方,解包 如果你还有其它更好的debug方法和经验,请一定分享出来。不过现在,让我们以Ramda为例,看看在函数式的世界里,问题是如何被解决的: import R from 'ramda' var log = R.curry(function (label,value){ console.log(label,value); return value }); R.compose( R.reverse,log('why we need a reverse ?'),R.sort(sortFn),R.filter(filterFn),R.uniq )([1,-10]) // -> [1,-10]
函数curry化,并把可变性高复用性低的参数后置,是函数式库的特征之一,也是写自定义函数时需要注意的地方。我们的log函数就遵循了这一点。 组合相比链式最大的优势,是函数可以自由而专注:不再受原型链的约束,也不再看this的脸色。对比之前的log函数,现在的版本没有了多余的解包与封包,也不再依赖this——现在它是一个纯函数。 很多人会用
应用实践在大致了解了函数组合后,让我们继续前面事件响应的例子,先回顾一下,之前我们用curry改写了 const setFieldOnContext = _.curry(function(context,address} = this.state; const setField = setFieldOnContext(this); return ( <form> <input value={name} onChange={setField('name')} /> <input value={address} onChange={setField('address')} /> </form> ) } }
import _ from 'ramda' const getValueFromEvent = function(e){ return e.target.value; }; const getValueFromX = function(x){ return x.value } const setFieldOnContext = _.curry(function(context,value){ context.setState({ [key]: value }) }); class Form extends React.Component{ render(){ const {name,x} = this.state; const setField = setFieldOnContext(this); return ( <form> <input value={name} onChange={_.compose(setField('name'),getValueFromEvent)} /> <X value={address} onChange={_.compose(setField('address'),getValueFromX)} /> </form> ) } } 完整代码 step_3 借助compose,我们的函数职责更加分离,setField只关心设值,对值的转换则由其它函数负责,虽然目前实现的版本用起来还有一些啰嗦,但我们得到了三个关注点(职责)高度分离的、可复用的函数。 在接着讨论前,让我们先统一一下用词,下面我会把 刚刚的代码之所以啰嗦,问题出在参数顺序和复用度不一致。
<form> <input value={name} onChange={_.compose(setField('foo'),getValueFromEvent)} /> <input value={name} onChange={_.compose(setField('bar'),getValueFromEvent)} /> <input value={name} onChange={_.compose(setField('baz'),getValueFromEvent)} /> <input value={name} onChange={_.compose(setField('baa'),getValueFromEvent)} /> <input value={name} onChange={_.compose(setField('zzz'),getValueFromEvent)} /> </form> 满眼的 这重申了curry的要点:通常我们会按照复用程度从高到低地排列参数,比如在同一个组件中,context的复用度最高,而key则次之,event没有复用度——每个事件源都是单独的。至于 下面是封装一层函数做参数顺序转换然后curry化的简单实现: import _ from 'ramda' const getValueFromEvent = function(e){ return e.target.value; }; const getValueFromX = function(x){ return x.value } const setFieldOnContext = _.curry(function(context,value){ context.setState({ [key]: value }) }); const getFieldSetter= _.curry(function(valueAdapter,name){ //返回真正的event handler return _.compose(setFieldOnContext(context,name),valueAdapter); }); const setFieldForEvent = getFieldSetter(getValueFromEvent); const setFieldForX = getFieldSetter(getValueFromX); React.createClass({ render(){ const {name,x} = this.state; return ( <form> <input value={name} onChange={setFieldForEvent(this,'name')} /> <X value={x} onChange={setFieldForX(this,'x')} /> </form> ) } }) 完整代码 step_4 数一数,我们一下子有了六个函数!或许你会为此感到不安:是不是弄错了什么? 不必担心,仔细看看,这六个函数都有各自的复用价值,随着项目的发展和膨胀,响应事件值的需求随处可见,而重复的代码和逻辑会慢慢蚕食可维护性。把高度解耦的函数们(比如valueAdapter们)组合起来,会让我们更轻松的应对挑战。 还有一点,上面六个函数中有五个都是纯函数!除了 追求纯函数有时候会比较困难,但它是值得的,如果你的函数依赖了this,或者其它外部状态,那最好重新审视你的代码——至少把不安全的依赖剥离到最小范围。 当然这一版本的实现仍不完美:
下一篇,我会借助Promise这个老面孔来介绍Functor和Monad——这两个你甚至没有见过,却无处不在的概念。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |