函数式编程了解一下

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

  • 入门简介
  • HOC简介
  • 函数柯里化与偏应用

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

  • 组合与管道
  • 函子和Monad
  • 再回首Generator

入门简介

函数的第一原则是要小,函数的第二原则是要更小

什么是函数式编程?为什么他重要

在理解什么是函数式编程的开始,我们先了解下什么数学中,函数具有的特性

  • 函数必须总是接受一个参数
  • 函数必须总是返回一个值
  • 函数应该依据接受到的参数,而不是外部的环境运行
  • 对于一个指定的x,必须返回一个确定的y

所以我们说,函数式编程是一种范式,我们能够以此创建仅依赖输入就可以完成自身逻辑的函数。这保证了当函数多次调用时,依然可以返回相同的结果。因此可以产生可缓存的、可测试的代码库

引用透明

所有的函数对于相同的输入都返回相同的结构,这一特性,我们称之为引用透明。 比如:

let identity = (i) => {return i};

这么简单?对,其实就是这样,也就是说他没有依赖任何外部变量、外部环境,只要你给我东西,我经过一顿鼓捣,总是给你返回你所能预测的结果。

这也为我们后面的并发代码、缓存成为可能。

命令式、声明式和抽象

函数式编程主张声明式编程和编写抽象代码。其实这个比较有意思,感觉更像是面向对象的编程。

光说不练都是扯淡。举个栗子

  var array = [1,2,3,4,5,6];
  for(let i = 0;i<array.length;i++){
    console.log(array[i])
  }

这段代码的作用简单明了,就是遍历!但是你有没有感觉这个代码呆呆的。没有一丁点的灵气?都是我告诉你该怎么该怎么做的。我们告诉编译器,你先去获取下数组的长度的,然后挨个log出来。这种编码方式,我们通常称之为“命令式”解决方案。

而在函数式编程中,我们其实更加主张用“声明式”解决方案

let array = [1,2,3,4,5,6];
array.forEach(item=>{console.log(item)})

简单体会下,是不是有那么一丢丢的灵感来了?等等,你这个forEach函数哪来的嘛!对,也是自己写的,但是不是我们通过编写这种抽象逻辑代码,而让整体的业务代码更加的清晰明了了呢?开发者是需要关心手头上的问题就好了,只需要告诉编译器去干嘛而不是怎么干了。是不是轻松了?

其实函数式编程主张的就是以抽象的方式创建函数。这些函数可以在代码的其他部分被重用。

函数式编程的好处

好处个人不喜欢扯太多,不是因为他没有好处,而是对于刚刚接触函数式编程的哥们,上来就说好处其实是没什么概念的,所以这里我简单提一提,后面文章会细细说明。

纯函数 => 可缓存

熟悉redux的同学应该对这个词语都不陌生,所谓的纯函数,其实也就是我们说的引用透明,稳定输出!好处呢?可预测嘛,容易编写测试代码哇,可缓存嘛。什么是可缓存?可以看我之前发的文章哈,这里简单举个栗子

let longRunningFunction = (input)=>{
  //进行了非常麻烦的计算,然后返回出来结果
  return output;
}

如果longRunningFunction是一个纯函数,引用透明。我们就可以说对于同样的输出,总是返回同样的结果,所以我们为什么不能够运用一个对象将我们每一次的运算结果存起来呢?

let longRunningFunctionResult = {1:2,2:3,3:4};
//检查key是否存在,存在直接用,不存在再计算
longRunningFunctionResult.hasOwnProperty(input)?longRunningFunctionResult[input]:longRunningFunctionResult[input] = longRunningFunction(input)

比较直观。不多说了哈。其实好处还有之前说到的并发。不说的这么冠冕堂皇了,啥并不并发呀,我不依赖别人的任何因素,只依据你的输出我产出。你说我支持什么就是什么咯,只要你给我对的参数传进来就可以了。

结束语

匆匆收尾!仅作为抛砖引玉。后面咱们在系统性的学习下函数式编程。

高阶函数(HOC)简介

概念

JavaScript作为一门语言,将函数视为数据。允许函数代替数据传递是一个非常强大的概念。接受一个函数作为参数的函数成为高阶函数(Higher-Order Function)

从数据入门HOC

JavaScript支持如下几种数据类型:

  • Number
  • String
  • Boolean
  • Object
  • null
  • undefined

这里面想强调的是JavaScript将函数也同样是为一种数据类型。当一门语言允许将函数作为数据那样传递和使用的时候,我们就称函数为一等公民。

所以说这个就是为了强调说明,在JavaScript中,函数可以被赋值,作为参数传递,也可以被其他函数返回。

//传递函数
let tellType = (arg)=>{
  if(typeof arg === 'function'){
    arg();
  }else{
    console.log(`this data is ${arg}`)
  }
}

let dataFn = ()=> {
  console.log('this is a Function');
}

tellType(dataFn);
//返回函数
let returnStr = ()=> String;

returnStr()('Nealyang')

//let fn = returnStr();
//fn('Nealyang');

从上我们可以看到函数可以接受另一个函数作为参数,同样,函数也可以将两一个函数作为返回值返回。

所以高阶函数就是接受函数作为参数并且/或者返回函数作为输出的函数

HOC 到底你是干嘛的

当我们了解到如何去创建并执行一个高阶函数的时候,同行我们都想去了解,他到底是干嘛的?OK,简单的说,高阶函数常用于抽象通用的问题。换句话说,高阶函数就是定义抽象。简单的说,其实就类似于命令式的编程方式,将具体的实现细节封装、抽象起来,让开发者更加的关心业务。抽象让我们专注于预定的目标而不是去关心底层的系统概念。

理解这个概念非常重要,所以下面我们将通过大量的栗子来说明

举斤栗子

const every = (arr,fn)=>{
  let result = true;
  for(const value of arr){
    result  = result && fn(value);
  }
  return result;
}

every([NaN,NaN,4],isNaN);

const some = (arr,fn)=>{
  let result = true;
  for(const value of arr){
    result  = result || fn(value);
  }
  return result;
}
some([3,1,2],isNaN);
//这里都是低效的实现。这里主要是理解高阶函数的概念
let sortObj = [
  {firstName:'aYang',lastName:'dNeal'},
  {firstName:'bYang',lastName:'cNeal'},
  {firstName:'cYang',lastName:'bNeal'},
  {firstName:'dYang',lastName:'aNeal'},
];

const sortBy = (property)=>{
  return (a,b) => {
    return (a[property]<b[property])?-1:(a[property]>b[property])?1:0
  }
}

sortObj.sort(sortBy('lastName'));
//sort函数接受了被sortBy函数返回的比较函数,我们再次抽象出compareFunction的逻辑,让用户更加关注比较,而不用去在乎怎么比较的。

HOC必然离不开闭包

上面的sortBy其实大家都应该看到了闭包的踪影。关于闭包的产生、概念这里就不啰嗦了。总之我们知道,闭包非常强大的原因就是它对作用域的访问。

简单说下闭包的三个可访问的作用域:

  • 在它自身声明之内的变量
  • 对全局变量的访问
  • 对外部函数变量的访问(*)

接着举栗子

const forEach = (arr,fn)=>{
  for(const item of arr){
    fn(item);
  }
}
//tap接受一个value,返回一个带有value的闭包函数
const tap = (value)=>(fn)=>{
  typeof fn === 'function'?fn(value):console.log(value);
}

forEach([1,2,3,4,5],(a)=>{
  tap(a)(()=>{
    console.log(`Nealyang:${a}`)
  })
});

函数柯里化与偏应用

函数柯里化

概念

直接看概念,柯里化是把一个多参函数转换为一个嵌套的一元函数的过程

不理解,莫方!举个栗子就明白了。

假设我们有一个函数,add:

const add = (x,y)=>x+y;

我们调用的时候当然就是add(1,2),没有什么特别的。当我们柯里化了以后呢,就是如下版本:

const addCurried = x => y => x + y;

调用的时候呢,就是这个样子的:

addCurried(4)(4)//8

是不是非常的简单?

说到这,我们在来回顾下,柯里化的概念:把一个多参函数转换成一个嵌套的一元函数的过程。

如何实现多参函数转为一元

上面的代码中,我们实现了二元函数转为一元函数的过程。那么对于多参我们该如何做呢?

这个是比较重要的部分,我们一步一步来实现

我们先来添加一个规则,最一层函数检查,如果传入的不是一个函数来调用curry函数则抛出错误。当如果提供了柯里化函数的所有参数,则通过使用这些传入的参数调用真正的函数。

let curry = (fn) => {
if(typeof fn !== 'function'){
  throw Error('not a function');
}
return function curriedFn (...args){
  return fn.apply(null,args);
}
}

所以如上,我们就可以这么玩了

const multiply = (x,y,z) => x * y * z;
curry(multiply)(1,2,3);//6

革命还未成功,我们继续哈~下面我们的目的就是把多参函数转为嵌套的一元函数(重回概念)

const multiply = (x,y,z) => x * y * z;
let curry = (fn) => {
  if(typeof fn !== 'function'){
    throw Error('not a 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);
  }
}
curry(multiply)(1)(2)(3)

如果是初次看到,可能会有些疑惑。我们一行行来瞅瞅。

args.length < fn.length

这段代码比价直接,就是判断,你传入的参数是否小于函数参数长度。

args.concat([].slice.call(arguments))

我们使用cancat函数链接一次传入的一个参数,并递归调用curriedFn。由于我们将所有的参数传入组合并递归调用,最终if判断会失效,就返回结果了。

####小小实操一下 我们写一个函数在数组内容中查找到包含数字的项

let curry = (fn) => {
  if(typeof fn !== 'function'){
    throw Error('not a 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);
  }
}
let match = curry(function(expr,str){return str.match(expr)});

let hasNumber = match(/[0-9]+/);

let filter = curry(function(f,ary){
  return ary.filter(f)
});

filter(hasNumber)(['js','number1']);

通过如上的例子,我想我们也应该看出来,为什么我们需要函数的柯里化:

  • 程序片段越小越容易被配置
  • 尽可能的函数化

偏应用

假设我们需要10ms后执行某一个特定操作,我们一般的做法是

setTimeout(() => console.log('do something'),10);
setTimeout(() => console.log('do other thing'),10);

如上,我们调用函数都传入了10,能使用curry函数把他在代码中隐藏吗?我擦,咱curry多牛逼!肯定不行的嘛~

因为curry函数应用参数列表是从最左到最右的。由于我们是根据需要传递函数,并将10保存在常量中,所以不能以这种方式使用curry。我们可以这么做:

const setTimeoutFunction = (time , fn) => {
  setTimeout(fn,time);
}

但是如果这样的话,我们是不是太过于麻烦了呢?为了减少了10的传递,还需要多造一个包装函数?

这时候,偏应用就出来了!!!

简单看下代码实现:

const partial = function (fn,...partialArgs){
  let args = partialArgs;
  return function(...fullArgs){
    let arg = 0;
    for(let i = 0; i<args.length &amp;&amp; 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'));

如上大家应该都能够理解。这里不做过多废话解释了。

简单总结的说:

所以,像map,filter我们可以轻松的使用curry函数解决问题,但是对于setTimeout这类,最合适的选择当然就是偏函数了。总之,我们使用curry或者partial是为了让函数参数或者函数设置变得更加的简单强大。

下节预告

上一部分说的比较浅显基础,希望大家也能够从中感受到函数式编程的精妙和灵活之处。大神请直接略过求指正求指导

下一节中,将主要介绍下,函数式编程中的组合、管道、函子以及Monad。最后我们在介绍下es6的Generator,或许我们能从最后的Generator中豁然开朗获得到很多启发哦~~

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

回顾柯里化、偏应用

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

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

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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp;
       moduleData.list &amp;&amp;
       moduleData.list.data &amp;&amp;
       moduleData.list.data.settings &amp;&amp;
       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'] }}

题外话

img

如果大家对Java有些了解的话,一定会发现这个跟Java8 中Optional非常的相似。其实Optional就是一个函子~ 

最后谈一谈Monad

概念

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

需求

该需求分为两步:

IMAGE
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) + "&amp;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}}

interesting

得到的结果还是套两层,+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!

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

结束语

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

本文转载自Nealyang的博客,原文地址
https://github.com/Nealyang/PersonalBlog/blob/master/2018/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B%E4%BA%86%E8%A7%A3%E4%B8%80%E4%B8%8B(%E4%B8%8B).md

发表评论

电子邮件地址不会被公开。 必填项已用*标注