Kleisli flatMap stange 类型参数子类型化

Kleisli flatMap stange type parameter subtyping

谁能给我解释一下为什么 cats 中 Kelisli 的 flatMap 签名如下:

def flatMap[C, AA <: A](f: B => Kleisli[F, AA, C])(implicit F: FlatMap[F]): Kleisli[F, AA, C] =
    Kleisli.shift(a => F.flatMap[B, C](run(a))((b: B) => f(b).run(a)))

我其实不太明白的是AA <: A为什么AA一定是A的子类型?

我得到了 Kleisli 平面图操作,它是我没有得到的子类型?

因为你可以,而且有时这会让生活更轻松。

假设我们有一些子类型

trait Animal {
  def makeNoise: IO[Unit]
}

case class DogFood(label: String)

case class Dog(name: String) extends Animal {
  def makeNoise = IO.println(s"$name says Woof!")
  def consumeEdibles(df: DogFood) = IO.println(s"$name ate '${df.label}'. Yum!")
}

而且我们可以制作几个 Kleislis:

val doNoise = Kleisli((_: Animal).makeNoise)
val eatNewFood = Kleisli((_: Dog).consumeEdibles(new DogFood("Fancy Dog Food")))

请注意,它们中的第一个只需要一个 Animal,但它们都可以用 Dog 调用。听起来很合理,我们能够以某种方式将两者组合成一个可以用狗叫的 Kleisli。正确的?我们来试试吧。


如果您从 Dog Kleisli 开始,它就会起作用:

// Both valid - contravariance makes it so that doNoise : Kleisli[IO, Animal, Unit]
// extends Kleisli[IO, Dog, Unit] and compiler figures it out. Result is Kleisli[IO, Dog, Unit]
eatNewFood.flatMap(_ => doNoise)
(eatNewFood >> doNoise)

但是请注意你不能这样做:

(doNoise >> eatNewFood)

那是因为>>很“笨”。由于您以 Kleisli[IO, Animal, Unit] 开头,因此要求下一个也是 Kleisli[IO, Animal, something]

不过,我们可以通过告诉编译器在推断 >> 的类型之前扩大 Kleisli 来纠正它:

((doNoise: Kleisli[IO, Dog, Unit]) >> eatNewFood)

这是冗长和丑陋的。如果我们可以告诉“嘿,不要要求右侧是更广泛的类型,而是允许结果是对两者都有效的更窄类型”,那就太好了。


flatMap 签名就是这么写的。子类型化意味着较窄的 AA 型可以代替较宽的 A 型用于左侧和右侧。

// Valid because of this trick you're asking about
// Here AA = Dog <: Animal = A
// Result value is Kleisli[F, AA, C] which resolves to Kleisli[IO, Dog, Unit]
doNoise.flatMap(_ => eatNewFood)

我们所失去的只是一些用于缩小输入范围的类型归属,没有人喜欢这些。

注意 AA 和 A 相同满足 AA <: A。因此虽然它没有是不同的子类型,但它可以 并且它既合法又有意义(可以用 Dog 调用两者 => 可以将它们组合成可以用 Dog 调用的东西)。