简要介绍fp-ts(一)

3,528 阅读6分钟

有谁能够在见识过函数式编程的优雅之后还不心动的呢?

对我来说,函数式编程是一个可望而不可及的梦想。它很美好,但是却有点超脱现实。如果说,全然开始使用 Haskell 等函数式编程语言是疯狂,那么从这个编程范式凝聚的智慧中学习一些有用的思想应用于日常的开发那就非常地 make sense。fp-ts 就给我了我们这样一个机会。虽然它使用起来远没有直接使用 Haskell 优雅和完美,但却也是在函数式编程和命令式编程之间取了一个比较好的折中。

fp-ts 中的很多概念,比如 typeclass、instance 等都是取自 Haskell,这对于没有使用过 Haskell 的人来说可能不是非常友好。因此本文将根据自己的使用感受和个人的理解介绍一下 fp-ts 提供的一些我有使用过的一些 modules,希望对 fp-ts 有兴趣的人有帮助。当然,由于本人的经验和能力有限,文中难免存在错误和疏漏。希望可以得到大家的指正,也欢迎交流和讨论。

预备知识

在开始正式之前有一些准备知识需要了解。

type

在学 C 语言的第一节上我们就会认识到很多的类型,比如intchar等。虽然 JavaScript 是弱类型的语言,我们也不需要在申明变量时指定变量的类型,但是 JavaScript 值同样都是有类型的。比如"abc"的类型是 string,而1的类型是 number。TypeScript 更是在这门语言的基础上增加了静态的类型系统。在 TypeScript 中,我们可以使用 type annotations 来描述变量或者参数的类型。同时,也赋予了我们通过 union type 等描述更复杂类型的能力。

同属于一种类型的值可以组成一个集合,这个集合包含了这种类型的所有可能值。因此我们可以用"is a member of"来描述值和类型之间的关系。比如true is a member of type boolean

type constructor

顾名思义,type constructor是用来创建类型的。大家所熟悉的Array就是一个 type constructor。Array本身不能直接作为某个值的类型使用,它必须接受另一个 type:

const x: Array = []; // 错误 Generic type 'Array<T>' requires 1 type argument

const y: Array<string> = []; // 正确

在 TypeScript 中,type constructor 是用 generic type 模拟的

Array虽然也是类型,但是它和 number、string 这些 type 之间却有着非常明显的区别。因此我们必须引入一个概念来区别这些类型。正如值具有类型,类型本身也具有类型,也就是类型的类型,被称为 kind。number 等可以作为值的类型使用的类型被称为 concrete type。它们的 kind 可以使用*表示。而 Array 的 kind 是* -> *,也就是说必须提供另一个 concrete type,才能得到一个可以作为值的类型使用的 concrete type。这样的类型被称为 higher-kinded type(以后缩写为 HKT)。当然,也存在需要接受更多类型的 HKT,比如 kind 为* -> * -> *的需要接受两个参数的 HKT。* -> * -> *这个表示也是在暗示着我们,当我们只提供一个类型时,我们将得到一个 kind 为* -> *的 HKT。只是,这一点无法使用 TypeScript 的 generic type 来模拟,因为 generic type 要求我们必须同时提供所需要的类型参数。

typeclass

观察 Haskell 中函数 show 的定义show :: Show a => a -> String=> 之前的内容是用来表达对类型变量的限制的(constraints on type variables)。Show a的含义是a类型必须满足Show typeclass。我们可以把 typeclass 想象称为一个个社团,比如有Eq社、Show社,而各种 type 就是社员。某 type 如果想要成为某社团的一员,那它必须要满足这个社团的入社要求。例如,Eq社要求入社的 type 需要支持判等的操作,也就是必须实现一个equals函数,这个函数接受任意两个属于 type 的值ab,输出一个布尔值。在 Haskell 中我们可以使用 instance declaration,并提供 typeclass 所定义的成员的实现,来为某 type 提供入团证明。而在 fp-ts 中,typeclass 是使用 interface 模拟的。

由此看来,typeclass 的功能类似于接口,用于描述某些类型具备某些行为特性或者支持某种操作。

Eq

既然上文已经提到了Eq这个typeclass,我们就先来看Eq能为我们带来什么以及如何使用。Eq表示能够判等的类型。它在 fp-ts 中的定义如下:

interface Eq<A> {
  readonly equals: (x: A, y: A) => boolean;
}

假如我们有类型Point定义如下:

interface Point {
    x: number;
    y: number;
}

我们认为xy都相等的点是相等的,因此写出equals的实现如下:

const equals = (a: Point, b: Point) => a.x === b.x && a.y === b.y;

我们也就为Point类型定义了一个 Eq 的 instance:

const eqPoint: Eq<Point> = {
  equals
}

那么Eq能为我们带来什么呢。来看 fp-ts 的 Array 模块提供的一个elem函数的定义:

declare const elem: <A>(
  E: Eq<A>
) => {
  (a: A): (as: Array<A>) => boolean
  (a: A, as: Array<A>): boolean
}

这个函数要求一个Eq<A>类型的参数,也就是说必须为类型A定义了Eq的 instance。而我们的Point就满足了这个条件。于是,我们就可以使用elem函数判断某个点是否在类型为Array<Point>的数组中存在:

const points: Array<Point> = [
  { x: 1, y: 2 },
  { x: 2, y: 2 },
  { x: 3, y: 4 },
];

elem(eqPoint)({ x: 1, y: 2 })(points) // true
elem(eqPoint)({ x: 3, y: 2 })(points) // false

fp-ts 已经为一些类型,比如number、string等提供了Eq的 instance。我们在创建自己的instance时,也可以选择利用这些已有的instance:

import { Eq as eqNumber } from "fp-ts/number";
import { Eq, struct } from "fp-ts/Eq";

const eqPoint: Eq<Point> = struct({
  x: eqNumber,
  y: eqNumber
})

Ord

Ord 表示支持比较操作的类型,于是在 Eq 的基础上又增加了新的要求:

type Ordering = -1 | 0 | 1

interface Ord<A> extends Eq<A> {
  readonly compare: (first: A, second: A) => Ordering
}

也就是说要先创建 Ord 的 instance,我们必须先创建 Eq 的 instance。 而比较的能力又为我们带来了什么呢?fp-ts/Array 提供了一个用于排序的函数:

const sort: <B>(O: Ord<B>) => <A extends B>(as: A[]) => A[]

假如我们有类型定义如下:

interface User {
    name: string;
    age: number;
}

我们希望按照年龄大小对用户进行排序。为了使用 sort,我们需要为 User 类型提供一个 Ord 的 instance:

const equals = (a: User, b: User) => a.age === b.age
const compare = (a: User, b: User) => equals(a, b) ? 0 : a.age > b.age ? 1 : -1

const ordUser: Ord<User> = {
  equals: equals,
  compare: compare 
}

const users = [
  {
    name: "Tomas",
    age: 25
  },
  {
    name: "Ammy",
    age: 21
  },
  {
    name: "Kat",
    age: 23
  }
]

sort(ordUser)(users) // [{"name":"Ammy","age":21},{"name":"Kat","age":23},{"name":"Tomas","age":25}]

同样的,fp-ts 也为 number 类型提供了 Ord 的 instance。我们也可以利用已有的 instance 来简化为 User 创建 instance 的过程:

const ordUser = contramap<number, User>(u => u.age)(eqNumber)

通过这两个例子,我们了解了 typeclass 以及 instance 的作用和使用。接下来的文章,我们正式开始了解一些 fp-ts 提供的类型。