HOME | EDIT | RSS | ABOUT | GITHUB

Type Classes in TypeScript

Typeclass 可以想象为函数式编程的一种设计模式, 虽然并没有设计模式这一说. 在 Haskell 是非常烂大街的一个概念.

在面向对象中, 我们对数据结构的表示为一个包含数据的 Class, 然后在这个 Class 里定义对数据的操作.

但是函数式不是这样的, 数据的定义与其操作的定义是完全分开的.

这就导致了 Type Class 的概念, 就像 Class 一样,我们需要给一些特定的操作归类, 按照可操作的类型.

例如现在有个数据类型 Xstream

class Xstream<T> {
  value: T
  constructor(v: T) {
    this.value = v
  }
}

如果是面向对象, 我们想让它能够被 map, 那么通常会抽象出接口 Mapable

interface Mapable<A> {
  map<B>(f: (v:A)=>B): Mapable<B>
}

然后猥琐的打开 Xstream 来实现 Mapablemap 方法.

class Xstream<T> implements Mapable<T> {
  value: T
  constructor(v: T) {
    this.value = v
  }

  map<B>(f: (v: T) => B): Mapable<B> {
    return new Xstream(f(this.value))
  }
}

那么如果我还想要让这个数据结构能 fold, 就需要再打开一次:

class Xstream<T> implements Mapable<T>, Foldable<T> {
   ...
  fold<B>(f:(acc:B,v:T)=>B, base:B):B {
    return f(this.value, base)
  }
}

现在, 面向对象抽象数据结构的坏味道出来了, 对操作的添加是一点也不开放呢.

而函数式, 正好相反, 对操作的添加尤其开放.

Functor

函数式中的这个 Mapable 接口就叫做 Functor type class,

函数式不会这样做

class Xstream<T> implements Mapable<T> {...}

相反, 我们需要数据类型作为接口的类型参数

interface Functor<F> {
  map<A, B>(f: (a: A) => B, fa: F<A>): F<B>
}

然后为 Xstream 实现 map 方法.

class XstreamFunctor implement Functor<Xstream<any>> {
  map<A, B>(f: (v: A) => B, fa: Xstream<A>): Xstream<B> {
    return new Xstream(f(this.value))
  }
}

注意: 现在 map 是 Functor<Xstream<any>> 的方法, 而不是 Xstream<any> 自己的方法

使用起来就像这样:

new XstreamFunctor.map((a)=>a+1, new Xstream(1))

Higher Kind Type

但是问题是, 上面的代码放到 TypeScript 是不编译的. 因为 TypeScript 并没有 Higher Kind Type(HKT)

interface Functor<F> {
  map<A, B>(f: (a: A) => B, fa: F<A>): F<B>
}

比如这里的 F 就是 HKT, F 不是一个具体类型, F<number> 才是, F 是相对 F<number> 更高阶的类型

大概就像高阶函数

function a(b){
  return function(){
     b + 1
  }
}

a 是高阶函数, a(1) 是普通函数.

受到 https://github.com/gcanti/fp-ts 的启发, 幸好我们可以在 TypeScript 中模拟 HKT

需要一个 _<A> 借口来存放 HKT, 就像一个 类型的字典, 给一个高阶类型和一个类型, 可以给你查到具体类型:

interface _<A> { }

HKT 就是所有高阶类型的 key, 注意到TypeScript 有 string literal type, 所以 key 其实就是 String Literal Type.

type HKT = keyof _<any>

使用 $<F,A> 可以方便而且美观的查到到一个高阶与低阶类型组成的具体类型:

type $<F extends HKT, A> = _<A>[F]

欢迎来到 Type Level Programming, 至今为止我们都还没有值的操作, 全部都是类型, 而这些代码都不会编译到 JS 中

再试试用这个HKT来实现 Functor

interface Functor<F extends HKT> {
  map<A, B>(f: (a: A) => B, fa: $<F, A>): $<F, B>
}

现在, 可以试想 Xstream's Functor 实例了

  1. Xstream 类型的 key 是 "Xstream"
interface _<A> {
  "Xstream": Xstream<A>
}
  1. 虽然 Functor<"Xstream"> 中的 "Xstream" 看起来像是字符串, 但其实它是类型, 而且是类型安全的, 敲错字符会导致编译错误.
class XstreamFunctor implements Functor<"Xstream"> {
  map<A, B>(f: (v: A) => B, fa: Xstream<A>): Xstream<B> {
    return new Xstream(f(fa.value))
  }
}

多态

但是, 用起来好难看, 每次用map还要 new 这么个 Functor 出来, 比如

new XstreamFunctor.map(a=>a+1, new Xstream(1))

面向对象的多态如何能找到对应解决方法呢? 比如现在多出来一个类型叫 Ystream.

如何实现一个多态的 map 能作用到所有 Functor 的实例上呢?

理论上 map 应该是这样的:

function map<F extends FunctorInstance, A, B>(f: (v: A) => B, fa: $<F, A>): $<F, B> {
  return new Functor<F>().map(f, fa)
}

但是 TypeScript 的弱弱的类型系统不会帮你找到 Functor<F> 的实例, Typescript 无法通过类型 Functor<"Xstream"> 就能找到 XstreamFunctor. 这种从类型找实例的技能就像 Scala 中 implicit .但是由于TypeScript最终会编译成 JS, 除非编译时有 macro, 不然没办法把类型的计算和信息带到 JS 代码中.

解决方法有些麻烦, 类似于类型 _ , 我们还需要一个字典, 但是这次是值字典而不是类型字典

namespace Functor {
  const Xstream = new XstreamFunctor
  const Ystream = new YstreamFunctor
}

Functor 字典存放所有 Functor 实例, 以类型名为 key

Functor['Xstream'].map(a=>a+1, new Xstream(1))
Functor['Ystream'].map(a=>a+1, new Ystream(1))

用的时候用类型名查找, 但问题是, 类型名是个 String 值, 不是一个类型

不管了, 先看看实现 map 还缺些什么

type FunctorInstance = keyof typeof Functor

FunctorInstance 目前来说是 'Xstream' | 'Ystream'

function map<F extends FunctorInstance, A, B>(f: (v: A) => B, fa: $<F, A>): $<F, B> {
  return Functor[F].map(f, fa)
}

这样还是编译不过的, 正如刚刚说的, F 是类型 Functor[F] 需要的 F 是值.

使用 TypeScript 是办不到的, 因为最终 Functor[F] 是会留到 JS 中, 而类型 F 会被丢掉.

JS 中没法得到 F 的任何信息.

但是反过来想, 不能从类型中拿到值, 那么能不能从值中提取出类型信息呢?

例如我们都知道 fa 的 constructor.name 是 'Xstream'

function map<F extends FunctorInstance, A, B>(f: (v: A) => B, fa: $<F, A>): $<F, B> {
  return Functor[fa.constructor.name as F].map(f, fa)
}

马德这还是编译不过

因为 Functor[fa.constructor.name as F] 有可能是类型 XstreamFunctorYstreamFunctor, fa 有可能是 XstreamYstream, 编译器会发现有可能出现 Functor[fa.constructor.name as F]XstreamFunctorfaYstream 的情况, 那就该编译错误了.

但是我们明明知道 Xstream 一定会叨叨 XstreamFunctorYstream 一定会找到 YStreamFunctor, 只能越过 TypeScript 的萨比检查了.

function map<F extends FunctorInstance, A, B>(f: (v: A) => B, fa: $<F, A>): $<F, B> {
  return (<any>Functor[fa.constructor.name as F]).map(f, fa) as $<F, B>
}

让我试试类型多态的 map

map<"Xstream", number, number>(a=>a+1, new Xstream(1))
map<"Ystream", number, number>(a=>a+1, new Ystream(1))

终于工作了

但是, 还是有问题. 如果我们 minify 代码, 还是会挂, 因为我们依赖了 constructor name压缩后如何保证 Functor 中的 Xstream 变量和这个构造函数的名会压缩成同样的字母呢?

Reflect Metadata

比较妥善的办法是通过 Reflect Metadata, 已经是 ECMA 的一个 proposal 但是不知什么时候会通过, 也不知道现在是stage 几, 反正 TypeScript 在推, Angular 在用, 也有polyfill

可以声明俩函数来存头数据和取头数据.

  • datatype 给数据类型打赏类型标签
  • kind 取出来
function datatype(name: string) {
  return (constructor: Function) => {
    Reflect.defineMetadata('design:type', name, constructor);
  }
}

function kind(target: any) {
  return Reflect.getMetadata('design:type', target.constructor);
}

比如 Tag Xstream

datatype('Xstream')(Xstream)

或是用 decorator 在声明class时加

@datatype('Xstream')
class Xstream<A> {...}

这样, 就可在 map 上使用 kind 拿到可靠的 key 了.

function map<F extends FunctorInstance, A, B>(f: (v: A) => B, fa: $<F, A>): $<F, B> {
  return (<any>Functor[kind(fa) as F]).map(f, fa) as $<F, B>
}

Cartesian

这么久就讲了一个 Type Class, 下面随便讲两个常用的, 体会体会用 TypeClass 的好处吧

现在我们再想给 Xstream 加操作, 就完全不需要动以前的代码了.

比如加 Cartesian Type Class

Cartesian 只有一个函数 product, 可以把两个数据类型合并成一个, 其内部数据合并成 tuple

type CartesianInstances = keyof typeof Cartesian

interface Cartesian<F extends HKT> {
  product<A, B>(fa: $<F, A>, fb: $<F, B>): $<F, [A, B]>
}

羡慕我们来实现 Xstream 的 Cartesian 实例

namespace Cartesian {
  export let Xstream: Cartesian<"Xstream">
}

function product<F extends CartesianInstances, A, B>(fa: $<F, A>, fb: $<F, B>): $<F, [A, B]> {
  let instance = (<any>Cartesian)[kind(fa)]
  return instance.product(fa, fb) as $<F, [A, B]>
}

// Cartesian Xstream instance
class XstreamCartesian implements Cartesian<"Xstream"> {
  product<A, B>(fa: Xstream<A>, fb: Xstream<B>): Xstream<[A, B]> {
    return new Xstream([fa.value, fb.value] as [A, B])
  }
}

Cartesian.Xstream = new XstreamCartesian

// product of two Xstream
product<"Xstream", number, number>(new Xstream(1), new Xstream(2))
// => Xstream([1,2])

一个新的能作用在 Xstream 类型上的函数就这么定义好了.

Apply

同样还可以扩展 TypeClass, 比如 Apply 就扩展了 Cartesian 和 Functor

interface Apply<F extends HKT> extends Cartesian<F>, Functor<F> {
  ap<A, B>(fab: $<F, (a: A) => B>, fa: $<F, A>): $<F, B>
}

type ApplyInstances = keyof typeof Apply

namespace Apply {
  export let Xstream: Apply<"Xstream">
}

function ap<F extends ApplyInstances, A, B>(fab: $<F, (a: A) => B>, fa: $<F, A>): $<F, B> {
  let instance = (<any>Functor)[kind(fab)]
  return instance.ap(fab, fa) as $<F, B>
}

在实现 Xstream 的实例时, 别忘了吧 map 和 product 都 alias到对应 Type Class 的方法上.

class XstreamApply implements Apply<"Xstream"> {
  ap<A, B>(fab: Xstream<(a: A) => B>, fa: Xstream<A>): Xstream<B> {
    return new Xstream(fab.value(fa.value))
  }
  map = Functor.Xstream.map
  product = Cartesian.Xstream.product
}

如果我们给 TypeClass 上加方法, 所有的实例也立马可以享受到

export function ap2<F extends ApplyInstances, A, B, C>(fabc: $<F, (a: A, b: B) => C>, fa: $<F, A>, fb: $<F, B>): $<F, C> {
  let instance: any = Apply[kind(fabc) as F]
  return instance.ap(
    instance.map(
      (f: (a: A, b: B) => C) => (([a, b]: [A, B]) => f(a, b))
      , fabc)
    , instance.product(fa, fb)
  ) as $<F, C>
}

现在 Xstream 自然就可以 ap2

ap2<"Xstream", number, number, number>(
  new Xstream((a: number, b: number) => a + b),
  new Xstream(2),
  new Xstream(3)
)
// => Xstream(5)

更多 Type Classes 请看 XREACT 源码…