因为你可以,而且有时它会让生活更轻松。
假设我们有一些子类型
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 调用的东西)。