函数式编程了解一下(下)
GitHub原文地址:https://github.com/Nealyang.md)
回顾柯里化、偏应用
对于上一篇文章,有朋友群里艾特说不是很明白柯里化的函数,这里我们拿出来简单说下
let curry = (fn) =>{ if(typeof fn !== 'function'){ throw Error('No Function'); } return function curriedFn(...args){ if(args.length < fn.length){ return function(){ return curriedFn.apply(null,args.concat([].slice.call(arguments))); } } return fn.apply(null,args); } } function add (a,b,c) { return a+b+c } curry(add)(1)(2)(3)
一步一步来理解,第一次调用curry函数的时候,返回一个curried函数,待调用状态,当我们传入1的时候,返回的依旧是一个函数,args是利用闭包,记录你传入的参数是否为函数定义时候的参数个数,如果不是,那我接着等待你在传入。因为我们利用args来记录每次传入的值,所以我们每次拿curry函数后的传入的参数就必须使用arguments了,由于它是类数组,我们想拿到参数值,所以这里我们使用slice。最终,我们其实还是调用a+b+c的运算。
同理,偏应用的存在其实就是弥补了柯里化传参顺序的短板
const partial = function (fn,...partialArgs){ let args = partialArgs; return function(...fullArgs){ let arg = 0; for(let i = 0; i<args.length && fullArgs.length;i++){ if(arg[i] === undefined){ args[i] = fullArgs[arg++]; } } return fn.apply(null,args) } } let delayTenMs = partial(setTimeout , undefined , 10); delayTenMs(() => console.log('this is Nealyang'));
同样利用闭包存储参数,利用undefined来占位
组合、管道
概念
官方解释为,函数式编程中的函数组合被称之为组合。说的云里雾里的,其实就是多个函数一起完成一件事,组合嘛。那管道呢?咱通俗点,类似gulp的pipe概念,你处理完了,吐出来,我接着处理(此处不禁想起人体蜈蚣,哇~),咳咳,正式点,将最左侧的函数输出所为输入发送给右侧函数,从技术上来说,就是管道。
为什么要这样呢?其实还是我们之前说的,函数的原则就是小、单一、简单。因为易测、简单。而我们呢,通过组合使用这些简单的函数而实现一个不简单的函数,完成一个不简单的功能。是不是类似于React编写组件的概念。通过组合各种小组件完成页面编写的感觉?
bingo~
compose 函数的实现
先看一个简答的实现
const compose = (a,b)=>(c)=>a(b(c)); let splitIntoSpaces = (str) => str.split(" "); let count = (array) => array.length; const countWords = compose(count,splitIntoSpaces); countWords('Hello , I am Nealyang');
在后面的开发中,我们只需要通过countWords就可以统计出单词的数量,通过这种方式实现的也非常的优雅。
其实这种编写的技巧就是将多个小而巧的函数组合完成不一样的功效出来。举个栗子:
let map = (array,fn) => { let results = [] for(const value of array) results.push(fn(value)) return results; }; let filter = (array,fn) => { let results = [] for(const value of array) (fn(value)) ? results.push(value) : undefined return results; }; let apressBooks = [ { "id": 111, "title": "C# 6.0", "author": "ANDREW TROELSEN", "rating": [4.7], "reviews": [{good : 4 , excellent : 12}] }, { "id": 222, "title": "Efficient Learning Machines", "author": "Rahul Khanna", "rating": [4.5], "reviews": [] }, { "id": 333, "title": "Pro AngularJS", "author": "Adam Freeman", "rating": [4.0], "reviews": [] }, { "id": 444, "title": "Pro ASP.NET", "author": "Adam Freeman", "rating": [4.2], "reviews": [{good : 14 , excellent : 12}] } ]; const compose = (a, b) => (c) => a(b(c)) const partial = function (fn,...partialArgs){ let args = partialArgs.slice(0); return function(...fullArguments) { let arg = 0; for (let i = 0; i < args.length && arg < fullArguments.length; i++) { if (args[i] === undefined) { args[i] = fullArguments[arg++]; } } return fn.apply(this, args); }; }; console.log("筛选结果",map(filter(apressBooks, (book) => book.rating[0] > 4.5),(book) => { return {title: book.title,author:book.author} })) //工具类函数 let filterOutStandingBooks = (book) => book.rating[0] === 5; let filterGoodBooks = (book) => book.rating[0] > 4.5; let filterBadBooks = (book) => book.rating[0] < 3.5; let projectTitleAndAuthor = (book) => { return {title: book.title,author:book.author} } let projectAuthor = (book) => { return {author:book.author} } let projectTitle = (book) => { return {title: book.title} } let queryGoodBooks = partial(filter,undefined,filterGoodBooks); let mapTitleAndAuthor = partial(map,undefined,projectTitleAndAuthor) let titleAndAuthorForGoodBooks = compose(mapTitleAndAuthor,queryGoodBooks) console.log(titleAndAuthorForGoodBooks(apressBooks)) let mapTitle = partial(map,undefined,projectTitle) let titleForGoodBooks = compose(mapTitle,queryGoodBooks) //console.log(titleForGoodBooks(apressBooks))
通过如上的代码,我们可以很轻松的看出通过组合这些小函数,而实现很多功能。非常的灵活。
多个函数的组合
当前版本的compose只实现了俩个函数的组合,那么如果对于多个函数呢?
const compose = (...fns) => (value) => reduce(fns.reverse(),(acc , fn ) => fn(acc),value);
上面最主要的一行是
reduce(fns.reverse(),(acc , fn ) => fn(acc),value)
此处我们首先fns.reverse()反转了函数数组,并传入了函数(acc,fn)=>fn(acc) ,它会以传入的acc作为其参数依次调用每一个函数。很显然,累加器的初始值为value,它将作为函数的第一个输入。
const composeN = (...fns) => (value) => reduce(fns.reverse(),(acc, fn) => fn(acc), value); const pipe = (...fns) => (value) => reduce(fns,(acc, fn) => fn(acc), value); let oddOrEven = (ip) => ip % 2 == 0 ? "even" : "odd" var oddOrEvenWords = composeN(oddOrEven,count,splitIntoSpaces); let count = (array) => array.length; console.log(oddOrEvenWords("hello your reading about composition")) oddOrEvenWords = pipe(splitIntoSpaces,count,oddOrEven); console.log(oddOrEvenWords("hello your reading about composition"))
如上的代码,有没有发现composeN和pipe非常的相似?其实就是执行序列的不同而已。从左至右处理数据流我们称之为管道。
函子
概念
在编写代码中的时候,我们肯定会涉及到关于错误的处理,而我们现在涉及到的新名词:函子,其实也不是什么高大上的东西,简单的说就是在函数式编程中的一种错误处理方式。我们用这种纯函数的方式来帮助我们处理错误。
函子是一个普通对象,它实现了map函数,在遍历每一个对象的时候生成新的对象
一步步梳理概念
首先我们可以将函子理解为容器。
const Container = function(val){ this.value = val; }
优化上面的容器,我们给Container添加一个of的静态方法,就是用来创建对象的
Container.of = function(val){ return new Container(val); }
到这一步,我们再回头看概念,函子是一个普通对象,它实现了一个map函数。。。,所以下一步,我们就来实现一个map函数吧
Container.property.map = function(fn){ return Container.of(fn(this.value)); }
如上,我们就编写除了一个函子,是不是也就那么回事?所以有哥们会问了,咱编写这个干嘛呢?有啥用?啊哈,咱接着往下看呗
MayBe 函子
MayBe函子能够让我们能够以更加函数式的方式处理错误
简单的看下具体的实现吧
const MayBe = function(val) { this.value = val; } MayBe.of = function(val) { return new MayBe(val); } MayBe.prototype.isNothing = function() { return (this.value === null || this.value === undefined); }; MayBe.prototype.map = function(fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value)); }; console.log("MayBe chaining",MayBe.of("George") .map((x) => x.toUpperCase()) .map((x) => "Mr. " + x)) console.log("MayBe chaining null", MayBe.of("George") .map(() => undefined) .map((x) => "Mr. " + x))
如上代码的执行结果为:
### MayBe函子的用途
在说用途之前呢,我们可以看一下在之前处理接口返回数据的一般逻辑(hack方式)
let value = 'string'; if(value != null || value != undefind){ return value.toupperCase(); } //实际项目中的例子 getPageModuleData = () => { return this.getDataFromXctrl(pageData.moduleData).then(moduleData => { if (moduleData.filter.data.hotBrands.length) { this.setState({ moduleData: moduleData.filter.data }); } // 小于多少个拍品,进行推荐 if ( moduleData && moduleData.list && moduleData.list.data && moduleData.list.data.settings && moduleData.list.data.settings.length ) { this.recLimit = moduleData.list.data.settings[0].showRecLimit; } if (!this.recLimit) { this.recLimit = 5; // 兜底 } }); };
对,这种命令式的方式总是把一些不必要的逻辑暴露出来,使用MayBe函子就不会有这个问题
他的操作,会让你感觉非常的舒服
MayBe.of('Nealyang') .map((x)=>x.toUpperCase()) .map(x=>`Mr.${x}`);
啰嗦了这么多,我们就为了说明两个MayBe函子重要的属性
1: 即使给map传入返回null或者undefined的函数,MayBe也依旧可以处理
2:所有的map函数都会调用,无论他是否接收到null or undefined
实际操刀
说了这么多,那么在我们的日常开发中,我们MayBe到底如何使用呢。这里我们还是拿项目中常见的请求接口来举栗子~
var request = require('sync-request'); ... let getTopTenSubRedditPosts = (type) => { let response try{ response = JSON.parse(request('GET',"https://www.reddit.com/r/subreddits/" + type + ".json?limit=10").getBody('utf8')) }catch(err) { response = { message: "Something went wrong" , errorCode: err['statusCode'] } } return response } let getTopTenSubRedditData = (type) => { let response = getTopTenSubRedditPosts(type); return MayBe.of(response).map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr, (x) => { return { title : x['data'].title, url : x['data'].url } } )) } console.log("正确的接收到返回:",getTopTenSubRedditData('new')) console.log("错误时候的情况",getTopTenSubRedditData('neww')) //MayBe{value:[{title:...,url:...},{}...]}
如上,我们请求一个接口,然后日常处理接口返回数据,并不需要去担心值是否存在而导致程序异常~
Either函子
上面,我们可以正确的处理数据了,但是错误的数据呢?我们需要将错误信息跑出给出提示,这也是我们常见的需求,但是使用MayBe函子就不能够很好地定位到错误的分支到底在哪了。!!!哇,搞了半天,你MayBe不咋地啊~ 其实不然,只是不同的函子有自己不同的侧重,在这个时候,我们就需要一个更加强大的MayBe函子了:Either函子
大家都是聪明人,我就不多介绍了,直接看代码:
const Nothing = function(val) { this.value = val; }; Nothing.of = function(val) { return new Nothing(val); }; Nothing.prototype.map = function(f) { return this; }; const Some = function(val) { this.value = val; }; Some.of = function(val) { return new Some(val); }; Some.prototype.map = function(fn) { return Some.of(fn(this.value)); } const Either = { Some : Some, Nothing: Nothing }
上面我们写了两个函数,Some和Nothing,Some简单易懂,我们来说说Nothing,他也是一个Container,但是其map不执行指定的函数,而是直接返回对象本身。直接的说就是一些函数可以在Some上运行但是不能再Nothing中运行
console.log("Something example", Some.of("test").map((x) => x.toUpperCase())) console.log("Nothing example", Nothing.of("test").map((x) => x.toUpperCase()))
比较简单,在实际的应用中,我们只需要简单修改response的处理方式即可
let getTopTenSubRedditPostsEither = (type) => { let response try{ response = Some.of(JSON.parse(request('GET',"https://www.reddit.com/r/subreddits/" + type + ".json?limit=10").getBody('utf8'))) }catch(err) { response = Nothing.of({ message: "Something went wrong" , errorCode: err['statusCode'] }) } return response } let getTopTenSubRedditDataEither = (type) => { let response = getTopTenSubRedditPostsEither(type); return response.map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr, (x) => { return { title : x['data'].title, url : x['data'].url } } )) } console.log("正确的运行: ",getTopTenSubRedditDataEither('new')) console.log("错误:",getTopTenSubRedditDataEither('new2'))//Nothing{value:{ message: "Something went wrong" , errorCode: err['statusCode'] }}
题外话
如果大家对Java有些了解的话,一定会发现这个跟Java8 中Optional非常的相似。其实Optional就是一个函子~
最后谈一谈Monad
概念
直接点,Monad其实也是一个函子,存在即合理,咱来说一说他到底是一个啥样子的函子。现在我们的需求是获取Reddit的评论,当然,我们可以使用MayBe函子来搞定的,稍后我们来看下实现。只不过,这里需要说明的是,MayBe函子更加的专注问题本身,而不必关心不必要的麻烦例如undefined或者null
需求
该需求分为两步:
- 为了搜索指定的帖子或者评论,需要调用接口:https://www.reddit.com/search... 如上,我们搜索functional programming得到如下结果
- 对,标记出来的Permalink是我们的下一步,访问permalink字段,拼接地址为:https://www.reddit.com//r/pro...:
我们需要获取评论对象后,将我们需要的title合并结果并返回新对象:{title:...,comments:[Object,Object,...]}
MayBe 版本实现
第一步的实现
let searchReddit = (search) => { let response try{ response = JSON.parse(request('GET',"https://www.reddit.com/search.json?q=" + encodeURI(search) + "&limit=2").getBody('utf8')) }catch(err) { response = { message: "Something went wrong" , errorCode: err['statusCode'] } } return response } let getComments = (link) => { let response try { console.log("https://www.reddit.com/" + link) response = JSON.parse(request('GET',"https://www.reddit.com/" + link).getBody('utf8')) } catch(err) { console.log(err) response = { message: "Something went wrong" , errorCode: err['statusCode'] } } return response }
上面代码就是实现了两个请求api。具体实现不解释了,非常简单。
第二步的实现
let mergeViaMayBe = (searchText) => { let redditMayBe = MayBe.of(searchReddit(searchText)) let ans = redditMayBe .map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr,(x) => { return { title : x['data'].title, permalink : x['data'].permalink } } )) .map((obj) => arrayUtils.map(obj, (x) => { return { title: x.title, comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))) } })) return ans; }
说说问题
是的,我们解决了我们的需求,但是仔细看上面代码,貌似丢失我们使用函子的初衷:代码简洁,看着爽~ 而上面的map多到怀疑人生,自己写起来可能会很好,但是别人维护起来是一个非常头疼的事情!
最头痛的时候,运行上面的函数后,我们拿到的值也是函子套函子,所以,该如何解决呢?这就是我们要说的Monad函子的用途
let answer = mergeViaMayBe("functional programming") console.log(answer) /* 需要两次map才能拿到我们想要的 */ answer.map((result) => { arrayUtils.map(result,(mergeResults) => { mergeResults.comments.map(comment => { console.log(comment) }) }) })
在我们获取Components的时候,他也是一个函子,所以我们得使用map
简单的把问题展开是酱紫的:
let example=MayBe.of(MayBe.of(5)); //将value 加 4 的需求 example.map(x=>x.map(v=>v+4)) //MayBe{value:MayBe{value:9}}
得到的结果还是套两层,+4的需求麻烦,得到的结果嵌套也麻烦,那么是否可以将两层,拨开呢????
join 来也
来的目标很简单,拨开嵌套!!!
直接看实现:
MayBe.prototype.join = function(){ return this.isNothing?MayBe.of(null):this.value }
搞定!
在回头看上面的需求:
let example=MayBe.of(MayBe.of(5)); example.join().map(v=>v+4);//=> MayBe(value:9)
搞定!!!
再回头看上上面的需求:
let mergeViaJoin = (searchText) => { let redditMayBe = MayBe.of(searchReddit(searchText)) let ans = redditMayBe.map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr,(x) => { return { title : x['data'].title, permalink : x['data'].permalink } } )) .map((obj) => arrayUtils.map(obj, (x) => { return { title: x.title, comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))).join() } })) .join() return ans; } let answer = mergeViaJoin("functional programming") console.log(answer)
如上代码,我们在函子后添加了两个join,成功的解决了函子套函子的问题。
对的,上面的join的确加入的方式有点尴尬~~~~ OK~我们在改造改造。
目前,我们总是要在map后调用join方法,下面我们把逻辑封装到一个名为chain中
MayBe.prototype.chain = function(f){ return this.map(f).join() } ... let mergeViaChain = (searchText) => { let redditMayBe = MayBe.of(searchReddit(searchText)) let ans = redditMayBe.map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr,(x) => { return { title : x['data'].title, permalink : x['data'].permalink } } )) .chain((obj) => arrayUtils.map(obj, (x) => { return { title: x.title, comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))).chain(x => { return x.length }) } })) return ans; } //trying our old problem with chain answer = mergeViaChain("functional programming") console.log(answer)
什么是Monad
啰嗦了这么多,所以到底什么是Monad呢?貌似我们一直以来都在解决问题,这种感觉就像现实中,这个人很面熟了,但是。。。还不知道怎么称呼一样。尴尬~
OK,Monad就是一个含有chain方法的函子,这就是Monad!(是不是感觉这个定义非常的山寨,哈哈)
如你所见,我们通过添加一个chain(当然也包括join)来展开MayBe函子,是其成为了一个Monad!
这种感觉就像~给自行车加了个电瓶,他就叫电瓶车了一样,哈啊
结束语
函数式编程,意在告诉我们使用数学式函数思维来解决问题,别忘了我们的原则:最小单一原则!
我也还在学习的路上,不当的地方,还希望多多指教~