函数式编程了解一下(下)

GitHub原文地址:https://github.com/Nealyanggit

回顾柯里化、偏应用

函数式编程了解一下(上)github

对于上一篇文章,有朋友群里艾特说不是很明白柯里化的函数,这里咱们拿出来简单说下编程

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的运算。json

同理,偏应用的存在其实就是弥补了柯里化传参顺序的短板gulp

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来占位api

组合、管道

概念

官方解释为,函数式编程中的函数组合被称之为组合。说的云里雾里的,其实就是多个函数一块儿完成一件事,组合嘛。那管道呢?咱通俗点,相似gulp的pipe概念,你处理完了,吐出来,我接着处理(此处不由想起人体蜈蚣,哇~),咳咳,正式点,将最左侧的函数输出所为输入发送给右侧函数,从技术上来讲,就是管道。数组

为何要这样呢?其实仍是咱们以前说的,函数的原则就是小、单1、简单。由于易测、简单。而咱们呢,经过组合使用这些简单的函数而实现一个不简单的函数,完成一个不简单的功能。是否是相似于React编写组件的概念。经过组合各类小组件完成页面编写的感受?bash

bingo~闭包

compose 函数的实现

先看一个简答的实现app

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))
复制代码

如上代码的执行结果为:

IMAGE

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:...},{}...]}
复制代码

如上,咱们请求一个接口,而后平常处理接口返回数据,并不须要去担忧值是否存在而致使程序异常~

img

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就是一个函子~

img

最后谈一谈Monad

概念

直接点,Monad其实也是一个函子,存在即合理,咱来讲一说他究竟是一个啥样子的函子。如今咱们的需求是获取Reddit的评论,固然,咱们可使用MayBe函子来搞定的,稍后咱们来看下实现。只不过,这里须要说明的是,MayBe函子更加的专一问题自己,而没必要关心没必要要的麻烦例如undefined或者null

需求

该需求分为两步:

  • 为了搜索指定的帖子或者评论,须要调用接口:https://www.reddit.com/search.json?q=keyword 如上,咱们搜索functional programming获得以下结果

IMAGE

  • 对,标记出来的Permalink是咱们的下一步,访问permalink字段,拼接地址为:https://www.reddit.com//r/programming/comments/3ejsyq/john_carmack_why_functional_programming_is_the/.json获得以下结果:

IMAGE

咱们须要获取评论对象后,将咱们须要的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;
}
复制代码

img

说说问题

是的,咱们解决了咱们的需求,可是仔细看上面代码,貌似丢失咱们使用函子的初衷:代码简洁,看着爽~ 而上面的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的需求麻烦,获得的结果嵌套也麻烦,那么是否能够将两层,拨开呢????

interesting

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!

这种感受就像~给自行车加了个电瓶,他就叫电瓶车了同样,哈啊

结束语

函数式编程,意在告诉咱们使用数学式函数思惟来解决问题,别忘了咱们的原则:最小单一原则!

我也还在学习的路上,不当的地方,还但愿多多指教~

相关文章
相关标签/搜索