TypeScript 中的通用类型参数和单子

Generic type parameter and monads in TypeScript

我目前正在学习 monads - 并尝试使用 TypeScript 更好地理解它们。

我想做以下事情:

type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
type Unit<M, A> = (a: A) => M<A>

但我收到以下 TypeScript 错误:

error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
                          ~~~~

error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
                                                     ~~~~

error TS2315: Type 'M' is not generic.
type Bind<M, A, B> = (ma: M<A>) => (a_mb: ((a: A) => M<B>)) => M<B>
                                                               ~~~~

error TS2315: Type 'M' is not generic.
type Unit<M, A> = (a: A) => M<A>
                            ~~~~

有没有办法指定类型参数 M 应该是通用的?或者还有其他明智的方法吗?

我的想法是我稍后可以通过 Maybe 代替 M,其中 Maybe 是一个通用类型:

type Maybe<T> = { kind: 'nothing' } | { kind: 'some', value: T }

不,TypeScript 不直接支持定义 BindUnit 所必需的 更高种类的类型 microsoft/TypeScript#1213 上有一个长期存在的开放功能请求要求这样做,目前尚不清楚何时或是否会实施。

TypeScript 目前缺乏抽象类型函数的能力。 (类型函数作用于类型的方式与常规函数作用于值的方式相同;它采用一些输入类型并产生输出类型。)它可以表示单个具体类型函数,例如 Array,并且您可以定义新的个别具体类型函数,如:

type Foo<T> = {x: T} // Foo is a type function

但是你不能一般地谈论 about 类型函数(没有 Foo 本身可以引用),也不能将类型函数传递给其他高阶类型职能。因为你的类型参数 M 需要是一个任意类型的函数才能工作,所以没有办法直接做。


请注意,我说它不直接支持更高种类的类型。可以模拟更高种类的类型,但我所知道的所有这样做的方法都很笨拙,需要大量的手动干预和样板代码。我不知道我会称之为“疯狂”,但我也不一定会说这是“另一种明智的做法”。

如果您想使用现有的库,可以查看 fp-ts。否则,实现的草图可能如下所示:

interface TypeFunction<T> { }
type TypeFunctionName = keyof TypeFunction<any>
type Apply<F extends TypeFunctionName, T> = TypeFunction<T>[F];

对于您想讨论的每个类型函数,您需要 merge 进入 TypeFunction<T>。例如:

interface TypeFunction<T> {
  Array: Array<T>
}

interface TypeFunction<T> {
  Foo: Foo<T>
}

完成后,您可以将 M<T> 的任何用法更改为 Apply<M, T>,其中 M 现在是您为 TypeFunction<T> 中的类型函数指定的键。所以你可以说 Apply<"Array", string>,它将是 string[],或者 Apply<"Foo", string>,它将是 {x: string}


鉴于这样的草图,我们可以这样写BindUnit

type Bind<M extends TypeFunctionName> =
  <A>(ma: Apply<M, A>) => <B>(a_mb: ((a: A) => Apply<M, B>)) => Apply<M, B>;
type Unit<M extends TypeFunctionName> =
  <A>(a: A) => Apply<M, A>;

(请注意 AB 的范围发生了怎样的变化...您希望单个 Bind<"Array"> 对所有 AB) 甚至定义其他操作,如 fmap:

const fmap = <M extends TypeFunctionName>(
  bind: Bind<M>, unit: Unit<M>) => <A, B>(f: (a: A) => B) =>
    (ma: Apply<M, A>) => bind(ma)(a => unit(f(a)))

为了好玩,我们可以为 ArrayFoo monads 实现 bindunit

namespace ArrayMonad {
  const bind: Bind<"Array"> = ma => mf => ma.flatMap(mf);
  const unit: Unit<"Array"> = a => [a];

  const map = fmap(bind, unit);
  const mapYell = map((x: number) => x + "!");
  const strs = mapYell([1, 2, 3]);
  console.log(strs) // ["1!", "2!", "3!"]
}

namespace FooMonad {
  const bind: Bind<"Foo"> = ma => mf => mf(ma.x);
  const unit: Unit<"Foo"> = a => ({ x: a });

  const map = fmap(bind, unit);
  const mapYell = map((x: number) => x + "!");
  const fooStr = mapYell({ x: 1 });
  console.log(fooStr) // {x: "1!"}
}

看起来不错,或多或少。我对不得不写 Bind<"Foo"> 而不是 Bind<Foo> 并不感到兴奋,但由于没有 Foo 可参考,这与您目前所能得到的最接近。

我会把实施 Maybe 留给你。

Playground link to code