chengaofeng
发布于 2024-10-12 / 6 阅读
0
0

Type holes in TypeScript

原文:https://dev.to/gcanti/type-holes-in-typescript-2lck

这篇文章的标题是《Type holes in TypeScript》,由Giulio Canti在2019年6月28日发表,并在6月29日更新。文章讨论了如何在TypeScript中模拟“类型洞”的概念,这是一种编程技巧,通过编写函数的一部分,然后利用编译器来帮助完成其余部分。这个过程是迭代的,可以看作是与编译器的对话。通过这种方式,开发者可以逐步接近正确的答案,最终可能在不太了解具体如何实现的情况下完成函数的编写。

什么是类型洞?

类型洞的概念是先实现你已知如何完成的函数的一小部分,然后请求编译器帮助完成其余部分。这是一个迭代的过程,是与编译器的对话。在这个过程中,每一步都让你更接近正确的答案,经过足够的迭代后,你的函数几乎可以自我完成——即使你并不完全清楚它是如何实现的。

TypeScript不支持类型洞,但可以进行某种程度的模拟。

第一个例子

文章首先展示了一个函数声明的例子:

declare function jonk<A, B>(
  ab: (a: A) => B,
  ann: (an: (a: A) => number) => number
): (bn: (b: B) => number) => number

为了模拟类型洞,作者使用了一个通用的函数声明:

declare function _<T>(): T

将这个函数放入jonk函数体中:

function jonk<A, B>(
  ab: (a: A) => B,
  ann: (an: (a: A) => number) => number
): (bn: (b: B) => number) => number {
  return _()
}

如果你将鼠标悬停在“类型洞”_上,你可以看到TypeScript推断的类型参数T

(bn: (b: B) => number) => number

所以,类型检查器告诉我们两件事:

  • 我们想要替换_()的表达式必须具有类型(bn: (b: B) => number) => number

  • 我们有一些局部绑定(abannjonk及其类型),我们可以使用它们来帮助实现。

由于我们的洞具有类型(bn: (b: B) => number) => number,我们应该在lambda中绑定bn(从现在起,我将只写函数体):

return bn => _() // 推断类型:number

新的推断类型是什么?number。我们如何产生一个number?我们可以使用abannbn。由于annbn都返回一个number,让我们选择ann作为猜测:

return bn => ann(_()) // 推断类型:(a: A) => number

我们的新洞具有函数类型,所以让我们引入一个lambda:

return bn => ann(a => _()) // 推断类型:number

我们需要再次产生一个number,这次让我们选择bn

return bn => ann(a => bn(_())) // 推断类型:B

现在我们需要产生一个B。我们有一个函数可以做到这一点,ab: (a: A) => B

return bn => ann(a => bn(ab(_()))) // 推断类型:A

最后,我们有一个洞,其类型是A。由于我们有一个Aa参数),让我们直接使用它:

function jonk<A, B>(
  ab: (a: A) => B,
  ann: (an: (a: A) => number) => number
): (bnn: (b: B) => number) => number {
  return bn => ann(a => bn(ab(a)))
}

现在我们几乎完全由类型检查器驱动地完成了实现。

第二个例子

让我们来处理博客文章中的第二个例子:zoop

declare function zoop<A, B>(abb: (a: A) => (b: B) => B, b: B, as: Array<A>): B

我们注意到as的类型是Array<A>,让我们使用foldLeft来“模式匹配”它:

import { foldLeft } from 'fp-ts/lib/Array'
import { pipe } from 'fp-ts/lib/pipeable'

function zoop<A, B>(abb: (a: A) => (b: B) => B, b: B, as: Array<A>): B {
  return pipe(
    as,
    foldLeft(
      () => _(), // 推断类型:B
      (head, tail) => _() // 推断类型:B
    )
  )
}

我们需要为“空”情况(即数组为空时)产生一个B。由于我们有一个B,让我们直接使用它(从现在起,我将只写函数体):

return pipe(
  as,
  foldLeft(
    () => b,
    (head, tail) => _() // 推断类型:B
  )
)

再次,我们想要为其他情况产生一个B,我们想要调用abb。由于它接受两个参数,让我们给它两个洞:

return pipe(
  as,
  foldLeft(
    () => b,
    (head, tail) =>
      abb(
        _() // 推断类型:A
      )(
        _() // 推断类型:B
      )
  )
)

head的类型是A,所以让我们使用它:

return pipe(
  as,
  foldLeft(
    () => b,
    (head, tail) =>
      abb(head)(
        _() // 推断类型:B
      )
  )
)

现在我们必须产生一个B,我们想要使用tail,它具有类型Array<A>。我们唯一的选择是使用zoop本身:

function zoop<A, B>(abb: (a: A) => (b: B) => B, b: B, as: Array<A>): B {
  return pipe(
    as,
    foldLeft(() => b, (head, tail) => abb(head)(zoop(abb, b, tail)))
  )
}

// p.s. `zoop`是`reduceRight`

这就是文章的主要内容。文章通过两个例子展示了如何在TypeScript中使用类型推断来模拟类型洞的概念,从而帮助开发者更有效地编写和理解代码。


评论