容器

通过一些基础的函数式编程操作,我们得以更优雅地进行编程,但是之前使用的纯函数编程方法却并不能解决所有的问题。比如:控制流、异常处理、异步操作、状态变化等。这些问题都可以用本节抽象的概念解决。

Container

通过如下方式定义的对象称为一个简单的容器:

1
2
3
4
class Container {
  constructor(x) { this.$value = x; }
  static of (x) { return new this.constructor(x); }
}

其中 static 函数 of 仅仅是为了方便代码更加函数化而定义,不影响理论探究的函数。

具体的,容器的操作应该遵循以下的约定:

  1. Container 是一个有且仅有一个属性的对象,我们后面将抽象地将它命名为 $value
  2. $value 不能被约束为任何特定的类型,否则我们的使用场景将相当有限;
  3. $value 一旦进入容器,它将一只被设置在容器内。我们可以但不应当通过 .$value 这种访问属性的方式访问他。

Functor (Identity)

Functor 是一类特殊的 Container,我们后续讨论的容器都是基于 Functor 的。它的简单实现:

1
2
3
4
5
class Functor extends Container {  
  map(f) {
    return Functor.of( f(this.$value) );
  }
}

所以 Functor 是:一种实现了 map 方法的容器。

有了 Functor,我们就可以像函数式编程一样处理有状态的问题,比如:

1
Functor.of('bombs').map( append(' away') ).map( prop('length') ); // Functor.$value === 10

FunctorContainer 中就像我们之前在 compose 中提到的恒等函数 id 一样平凡,所以通常也被称为 Identity

Maybe

上面的工作是简单平凡的,如果我们在 Container 中实现更多的方法,就可以拥有更丰富的功能。

Maybe 是一种用 Functor 实现空值检测的容器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Maybe extends Functor {
  get isNothing() {
    return this.$value === null || this.$value === undefined;
  }
  
  map(fn) {
    return this.isNothing ? this : Maybe.of( fn(this.$value) );
  }
  
  inspect() {
    return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
  }
}

通常情况下,我们强制要求每次调用都需要以 Maybe 作为返回值,但是如果我们非要用一个非 Maybe 的函数作为返回值,可以借助下面这种方式:

1
2
3
4
5
6
7
8
9
//  maybe :: b -> (a -> b) -> Maybe a -> b
var maybe = curry(function(x, f, m) {
  return m.isNothing() ? x : f(m.$value);
});

// 返回值是 string, 而非 Maybe
var getTwenty = compose(
  maybe("You're broke!", finishTransaction), withdraw(20)
);

Either

Either 也是一类特殊的 Functor,它的本意是指返回值可以是一个 SumType

这种 Fucntor 可以被用于进行错误处理,不同于 throw/catch,使用这种方式进行错误处理更加温和,我们可以定义一个左值作为发生异常时的类型(比如一个承载错误信息的字符串,类型为 String),右值作为执行成功时的真正结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Either extends Functor {
  static of(x) {
    return new Right(x);
  }
}

class Left extends Either {
  map(fn) { return this; }
}

class Right extends Either {
  map(fn) {
    try {
      return Eitherr.of( fn(this.$value) );
    } catch (e) {
      return new Left(e);
    }
  }
}

IO

在基础部分提到进行函数编程的前提是需要纯函数,但是某些函数的执行结果由于依赖于外部的环境,所以相同的输入通常会得到不同的结果,我们可以这个函数原子化并以其本身作为返回值,构造一个二级函数,这个二级函数显然是一个纯函数。

以这个思想构造的容器称为 IO,即 $value 为函数的 Functor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class IO extends Functor {
  static of(x) {
    return new IO(() => x);
  }

  constructor(fn) {
    this.$value = fn;
  }

  map(fn) {
    return new IO(compose(fn, this.$value));
  }

  inspect() {
    return `IO(${inspect(this.$value)})`;
  }
}

在系列函数执行之后,函数的 $value 本质是一个函数,我们还需要在最后执行它。但是这一直接访问 $value 的操作在我们的容器设计中总是不安全的,所以我们重命名为 unsafePerformIO 以指示这一区别。下面是一个应用 IO 的例子:

1
2
3
4
5
6
7
8
const url = new IO(() => window.location.href);

const toPairs: string => string[][] = compose(map(split('=')), split('&'));
const params: string => string[][] = compose(toPairs, last, split('?'));
const findParam: string => IO<Maybe<string[]>> = key => url.map( compose(Maybe.of, find(compose(eq(key), head)), params) );

// impure code
findParam('searchTerm').unsafePerformIO();

PostScript

  • map 函数的理解:第一个参数是映射函数,第二个参数是定义域,返回值则是值域。
  • 虽然我们现在定义的这些函数都是成员函数,但是之后它们可能会经常以独立函数的身份出现,它们通常被定义为“接受一个对应 Functor 类型并且执行对应成员函数”的函数,由于这个规定是为了优化 compose 函数的编写,因此容器类型的参数通常是最后一个参数。比如:map = curry( (fn, m) => m.map(fn) );

Task

最后异步类型的任务可以用一个叫 Task 的容器处理,这个容器的具体实现过程过于复杂,下面仅仅列举它的一个使用的例子:

1
2
3
4
5
6
7
8
9
var getJSON: string => object => Task<Error, object> = curry(
  (url, params) => new Task((reject, result) => {
    $.getJSON(url, params, result).fail(reject);
	})
);

var blogPage: object => HTML = Handlebars.compile(blogTemplate);
var renderPage: object => HTML = compose(blogPage, sortBy('date'));
var blog: object => Task<Error, object> = compose(map(renderPage), getJSON('/posts'));

实际函数在执行时,则需要调用 fork 函数进行异步地执行:

1
2
3
4
blog({}).fork(
  error => $('#error').html(error.message),
  page => $('#main').html(page),
);

范畴学理论

上面提到的这些 Container 都被称为 Functor,而 Functor 本身是可以构成一个范畴学意义上的 category 的:

  1. 恒等映射:map(id) === id
  2. 组合律:compose(map(f), map(g)) === map(compose(f, g))