28

JavaScript ES6函数式编程(三):函子

 4 years ago
source link: https://www.tuicool.com/articles/Ajmaqm6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前面二篇学习了函数式编程的基本概念和常见用法。今天,我们来学习函数式编程的最后一个概念——函子(Functor)。

相信有一部分同学对这个概念很陌生,毕竟现在已经有很多成熟的轮子,基本能满足我们日常的业务开发,所以没必须重复造轮子。但是,作为一名(未来)优秀的程序员,光会用怎么能行呢?必须要理解更深层的思想。下面就来学习函子部分的知识...

函子(Functor)

在正式学习函子之前,我会先抛出一个问题,先用普通的方式解决,然后转换为用函子解决,这能帮助我们更好的理解函子。同时,这也是我想说的,在我们学习一个新的知识点前,首先必须清楚为什么会有它,或者说它是为了解决什么问题而生的,这也是我们学习新知识后能够快速达到学以致用的最有效方法,不然很容易被遗忘。

function double (x) {
  return x * 2
}
function add5 (x) {
  return x + 5
}

var a = add5(5)
double(a)
// 或者
double(add5(5))

我们现在想以数据为中心,串行的方法去执行,即:

(5).add5().double()

很明显,这样的串行调用清晰多了。下面我们就实现一个这样的串行调用:

要实现这样的串行调用,需要 (5) 必须是一个引用类型,因为需要挂载方法。同时,引用类型上要有可以调用的方法也必须返回一个引用类型,保证后面的串行调用。

class Num {
       constructor (value) {
          this.value = value ;
       }      
       add5 () {
           return new Num( this.value + 5)
       }
       double () {
           return new Num( this.value * 2)
       }
    }
var num = new Num(5);
num.add5 ().double ()

我们通过new Num(5) ,创建了一个 num 类型的实例。把处理的值作为参数传了进去,从而改变了 this.value 的值。我们把这个对象返会出去,可以继续调用方法去处理数据。

通过上面的做法,我们已经实现了串行调用。但是,这样的调用很不灵活。如果我想再实现个减一的函数,还要再写到这个 Num 构造函数里。所以,我们需要思考如何把对数据处理这一层抽象出来,暴露到外面,让我们可以灵活传入任意函数。来看下面的做法:

class Num {
       constructor (value) {
          this.value = value ;
       }      
       map (fn) {
           return  new Num( fn(this.value) )
       }
    }
var num = new Num(5);
num.map(add5).map(double)

我们创建了一个 map 方法,把处理数据的函数 fn 传了进去。这样我们就完美的实现了抽象,保证的灵活性。

到这里,我们的函子就该正式登场了。不用怕,其实函子的概念很简单,我们在上面其实已经创建了一个函子雏形。现在我们整理一下,创建一个真正的函子:

class Functor{
       constructor (value) {
          this.value = value ;
       }      
       map (fn) {
         return Functor.of(fn(this.value))
       }
    }

Functor.of = function (val) {
     return new Functor(val);
}

Functor.of(5).map(add5).map(double)

现在我们可以用 Functor.of(5).map(add5).map(double) 去调用,是不是觉得清爽多了。

下面总结一下这个函子的几个特征:

  • Functor 是 一个容器 ,它包含了值,就是this.value(想一想你最开始的new Num(5))
  • Functor 具有 map 方法 。该方法将容器里面的每一个值,映射到另一个容器。(想一想你在里面是不是new Num(fn(this.value))
  • 函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子。(想一想你是不是没直接去操作值)
  • 函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。(说的就是你传进去那个函数把 this.value 给处理了)
  • 函数式编程一般约定,函子有一个 of 方法,用来生成新的容器。(就是帮我们 new 了一个对象出来)

说了那么多,如果还是不理解函子概念的话,那也正常。因为仔细看看这也没什么的嘛,就是封装了一个简单的构造函数而已,咋就整出来一个新概念函子了呢?不理解不重要,主要是看到了函子帮我们更好的串行调用函数处理数据。回想一下我们上一节学的 compose,是不是很像呢?只是函子的调用方式显得更加优雅。

现在,我们已经认识了一个基础的函子。接下来,我们需要认识一个更加完善的函子——Maybe函子...

Maybe 函子

我们知道,在做字符串处理的时候,如果一个字符串是 null, 那么对它进行 toUpperCase() 就会报错。

Functor.of(null).map(value => value.toUpperCase())

所以我们需要对 null 值进行特殊过滤:

class Maybe{
       constructor (value) {
          this.value = value;
       }      
       map (fn) {
          return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);  
       }
    }
Maybe.of = function (val) {
     return new Maybe(val);
}

var a = Maybe.of(null).map(function (s) {
  return s.toUpperCase();
});

我们看到只需要把在中设置一个空值过滤,就可以完成这样一个 Maybe 函子。是不是so easy。

Monad 函子

Monad 函子也是一个函子,其实很原理简单,只不过在原有的基础上又加了一些功能。那我们来看看它与其它的 有什么不同吧。

我们知道,函子是可以嵌套函子的。比如下面这个例子:

function fn (e) { return e.value }

var a = Maybe.of( Maybe.of( Maybe.of('str') ) ) 
console.log(a);
console.log(a.map(fn));
console.log(a.map(fn).map(fn));

我们有时候会遇到一种情况,需要处理的数据是 Maybe {value: Maybe}。显然我们需要一层一层的解开。这样很麻烦,那么我们有没有什么办法得到里面的值呢?

class Monad {
       constructor (value) {
          this.value = value ;
       }      
       map (fn) {
          return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
       }
       join ( ) {
          return this.value;
       }
    }
Monad.of = function (val) {
     return new Monad(val);
}

这样,我们就能很轻易的处理嵌套函子的问题了:

var  a = Monad.of( Monad.of('str') ) 
console.log(a.join().map(toUpperCase))

Modan函子也是一个很简单的概念,仅仅多了个 join 函数,为我们处理嵌套函子。

总结

至此,js函数式编程已经接近尾声。我们到底学到了什么?

首先,我们认识到了函数式编程的关注点是数据的 映射关系 ,如何将一个数据结构更加优雅的转化为另一个数据结构。函数式编程的主体是 纯函数 ,函数的内部实现不能影响到外部环境。

然后,我们学习了几个常用的函数式编程场景——柯里化、偏函数、组合和管道。 帮助我们更好的实际业务中运用函数式编程。

最后,我们运用函子实现了灵活的同步链式调用函数。

参考链接: 在你身边你左右 --函数式编程别烦恼


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK