HKT在fp-ts中是如何运作的?

How HKT really works in fp-ts?

提问人:Shnd 提问时间:10/22/2023 最后编辑:Shnd 更新时间:10/26/2023 访问量:53

问:

查看下面的代码。我还将其添加到打字稿游乐场的此链接中。我把部分 fp-ts 收集在一个地方,以测试和了解 HKT 的工作原理并在那里进行模拟。

type Identity<A> = { _tag: 'Identity', a: A }

// --------------------

type Option<A> = Some<A> | None

interface Some<A> {
  _tag: 'Some'
  value: A
}

interface None {
  _tag: 'None'
}

const some = <A,>(x: A): Option<A> => 
  ({ _tag: 'Some', value: x })

const none: Option<never> = 
  { _tag: 'None' }

const isNone = <A,>(x: Option<A>): x is None => 
  x._tag === 'None'

// --------------------

type Either<E, A> = Left<E> | Right<A>

interface Left<E> {
  _tag: 'Left'
  left: E
}

interface Right<A> {
  _tag: 'Right'
  right: A
}

const left = <E,A=never>(x: E):  Either<E, A> => ({ _tag: 'Left', left: x})
const right = <A,E=never>(x: A):  Either<E, A> => ({ _tag: 'Right', right: x})

const isLeft = <E, A>(a: Either<E, A>): a is Left<E> => 
  a._tag === 'Left'


// --------------------

interface URItoKind1<A> {
    'Identity': Identity<A>
    'Option': Option<A>
}

interface URItoKind2<E,A> {
    'Either': Either<E,A>
}

type URIS1 = keyof URItoKind1<any>
type URIS2 = keyof URItoKind2<any, any>

type Kind1<URI extends URIS1, A> = URItoKind1<A>[URI]
type Kind2<URI extends URIS2, E, A> = URItoKind2<E,A>[URI]

// type HKT1<URI, A> = { URI: URI; a: A };              // FROM FP-TS
type HKT1<URI, A> = { What: string }                    // HERE IS CONFUSING TO ME. THIS COMPILES
// type HKT2<URI, A, B> = { URI: URI; a: A; b: B };     // FROM FP-TS
type HKT2<URI, A, B> = { Hello: number }                // HERE IS CONFUSING TO ME. THIS COMPILES

interface Functor1<F extends URIS1> {
    map: <A, B>(fa: Kind1<F, A>, f: (a: A) => B) => Kind1<F, B>
}

interface Functor2<F extends URIS2> {
    map: <E, A, B>(fa: Kind2<F, E, A>, f: (a: A) => B) => Kind2<F, E, B>
}

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

// --------------------------

const optionFunctor: Functor1<'Option'> = {
  map: <A,B>(fa: Option<A>, f: (x: A) => B): Option<B> => 
    isNone(fa) ? none : some(f(fa.value))
}

const eitherFunctor: Functor2<'Either'> = {
  map: <E,A,B>(fa: Either<E, A>, f: (x: A) => B): Either<E, B> => 
    isLeft(fa) ? fa : right(f(fa.right))
}

// ---------------------------

function lift<F extends URIS2>(F: Functor2<F>): <A, B>(f: (a: A) => B) => <E>(fa: Kind2<F, E, A>) => Kind2<F, E, B>
function lift<F extends URIS1>(F: Functor1<F>): <A, B>(f: (a: A) => B) => (fa: Kind1<F, A>) => Kind1<F, B>
function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT1<F, A>) => HKT1<F, B>       // IF YOU COMMENT THIS TYPESCRIPT WILL NOT BE HAPPY, BUT WHY IT WORKS IN THE FIRST PLACE!
{
  return (f) => (fa) => F.map(fa, f)
}

const liftOption = lift(optionFunctor)
const liftEither = lift(eitherFunctor)

const increment = (x: number) => x+1

console.log(liftOption(increment)(some(12)))
console.log(liftEither(increment)(right(12)))

这编译正常,运行正常。

我把 和 改成完全无稽之谈的东西HKT1HKT2

type HKT1<URI, A> = { URI: URI; a: A };
type HKT2<URI, A, B> = { URI: URI; a: A; b: B };

type HKT1<URI, A> = { What: string }
type HKT2<URI, A, B> = { Hello: number }

但是代码编译正常。为什么?

更奇怪的是,如果我们评论这句话......

function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT1<F, A>) => HKT1<F, B>

...这是重载和默认情况。代码停止编译!

香港电讯在这里如何运作?这些类型如何在 Functor 下将 Functor1 和 Functor2 粘合在一起?HKT1

打字稿 Higher-kinded-Types FP-TS

评论

0赞 jcalz 10/22/2023
请在问题本身中以纯文本形式包含最小可重现示例的所有相关代码。游乐场链接是一个很好的补充,但还不够。
1赞 merryweather 10/22/2023
这两者完全没有关系。更改 HKTn 的定义没有任何作用,因为......它是有效的 TypeScript,而 HKTn 从未作过(可以这么说,它总是被用作“叶子”)。注释该行会破坏所有内容,因为您基本上已经删除了该函数的重载。
0赞 Shnd 10/23/2023
@kelsny我不明白你为什么这么说,没有关系。如果它们不相关,为什么注释 HKT1 电梯过载会导致类型不匹配? 在其定义中使用(下面使用 )。在我们不使用 !似乎唯一相关的地方是重载(叶)签名。HKT1liftlift(optionFunctor)optionFunctorFunctor1Functor1URItoKind1Functor1HKT1HKT1optionFunctorlift

答:

0赞 Beraliv 10/26/2023 #1

让我们一一介绍:


  1. 类型如何与 , , ...在 TypeScript 级别。FunctorFunctor1Functor2

您有一个带有 2 个重载签名和一个实现签名函数重载

function lift<F extends URIS2>(F: Functor2<F>): <A, B>(f: (a: A) => B) => <E>(fa: Kind2<F, E, A>) => Kind2<F, E, B>
function lift<F extends URIS1>(F: Functor1<F>): <A, B>(f: (a: A) => B) => (fa: Kind1<F, A>) => Kind1<F, B>
function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT1<F, A>) => HKT1<F, B> 

当您与:liftoptionFunctor

const liftOption = lift(optionFunctor)
//                  ^? function lift<"Option">(F: Functor1<"Option">): <A, B>(f: (a: A) => B) => (fa: Option<A>) => Option<B>

使用第二个重载签名,因为是Functor1optionFunctorFunctor1

当您与:lifteitherFunctor

const liftEither = lift(eitherFunctor)
//                  ^? function lift<"Either">(F: Functor2<"Either">): <A, B>(f: (a: A) => B) => <E>(fa: Either<E, A>) => Either<E, B>

将第一个重载签名与 because is 一起使用Functor2eitherFunctorFunctor2

TS Playground 与我添加的评论 - https://tsplay.dev/WYylvW


  1. 对于函数重载,我无法解释为什么注释最后一个重载(带有 和 的重载)会为其他重载创建错误。liftFunctorHKT

函数重载应该有实现签名,因此每个重载都会从它缩小范围(最后)。

但你也需要记住,你的是连接到:HKT1Functor

type HKT1<URI, A> = { What: string }

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

因此,由于您没有任何通用约束<F>

function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT1<F, A>) => HKT1<F, B>

它仍然是正确的 TypeScript 代码,即使它映射到您意想不到的内容。

但是,当您将其注释掉时,您开始出现类型错误,例如Argument of type 'Functor1<"Option">' is not assignable to parameter of type 'Functor2<"Either">'

这里重要的一点是“实现签名也必须与重载签名兼容”。当您删除实现签名并使 second 重载实现签名时,它会破坏此规则,因此 中的函数不再具有公分母(它是 )。famapHKT1<F, A>

你可以通过删除来证明这一点,然后把它作为泛型重载,类型错误就消失了:HKT1Functor

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

带有我更改的 TypeScript Playground - https://tsplay.dev/NaZREm


  1. 为什么将类型定义更改为某些随机对象类型不会导致编译错误。HKT1

在实现签名(带有 和 的那个)处没有通用约束,因此您不会收到类型错误FunctorHKT1

实现的签名从外部不可见。编写重载函数时,应始终在函数实现上方具有两个或多个签名。


如果您有任何其他问题,请告诉我,很乐意解释