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 调用的东西)。
谁能给我解释一下为什么 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 调用的东西)。