猫效应:如何将 Map[x,IO[y]] 转换为 IO[Map[x,y]]
cats-effect:How to transform Map[x,IO[y]] to IO[Map[x,y]]
我有一个字符串到 IO 的映射,像这样 Map[String, IO[String]]
,我想将它转换成 IO[Map[String, String]]
。怎么做?
你必须小心一点。 Scala 中的地图是无序的,所以如果你尝试像这样使用猫的 sequence
…
import cats.instances.map._
import cats.effect.IO
import cats.UnorderedTraverse
object Example1 {
type StringMap[V] = Map[String, V]
val m: StringMap[IO[String]] = Map("1" -> IO{println("1"); "1"})
val n: IO[StringMap[String]] = UnorderedTraverse[StringMap].unorderedSequence[IO, String](m)
}
您将收到以下错误:
Error: could not find implicit value for evidence parameter of type cats.CommutativeApplicative[cats.effect.IO]
这里的问题是 IO monad 实际上不是可交换的。这是 definition of commutativity:
map2(u, v)(f) = map2(v, u)(flip(f)) // Commutativity (Scala)
这个定义表明即使效果以不同的顺序发生,结果也是相同的。
您可以通过提供 CommutativeApplicative[IO]
的实例来编译上述代码,但这仍然不能使 IO monad 可交换。如果您 运行 以下代码,您可以看到副作用未按相同顺序处理:
import cats.effect.IO
import cats.CommutativeApplicative
object Example2 {
implicit object FakeEvidence extends CommutativeApplicative[IO] {
override def pure[A](x: A): IO[A] = IO(x)
override def ap[A, B](ff: IO[A => B])(fa: IO[A]): IO[B] =
implicitly[Applicative[IO]].ap(ff)(fa)
}
def main(args: Array[String]): Unit = {
def flip[A, B, C](f: (A, B) => C) = (b: B, a: A) => f(a, b)
val fa = IO{println(1); 1}
val fb = IO{println(true); true}
val f = (a: Int, b: Boolean) => s"$a$b"
println(s"IO is not commutative: ${FakeEvidence.map2(fa, fb)(f).unsafeRunSync()} == ${FakeEvidence.map2(fb, fa)(flip(f)).unsafeRunSync()} (look at the side effects above^^)")
}
}
输出如下:
1
true
true
1
IO is not commutative: 1true == 1true (look at the side effects above^^)
为了解决这个问题,我建议您制作具有顺序的地图,例如列表,其中序列不需要交换性。以下示例只是执行此操作的一种方法:
import cats.effect.IO
import cats.implicits._
object Example3 {
val m: Map[String, IO[String]] = Map("1" -> IO {println("1"); "1"})
val l: IO[List[(String, String)]] = m.toList.traverse[IO, (String, String)] { case (s, io) => io.map(s2 => (s, s2))}
val n: IO[Map[String, String]] = l.map { _.toMap }
}
在这里使用 unorderedTraverse
会很好,但正如 codenoodle 指出的那样,它不起作用,因为 IO
不是可交换的应用程序。然而有一种类型是,它叫做IO.Par
。顾名思义,它的 ap
组合子不会顺序执行而是并行执行,所以它是可交换的——执行 a 然后执行 b 与执行 b 然后执行 a 不同,但同时执行 a 和 b 是和同时做b和a一样。
因此,您可以使用 unorderedTraverse
和 return IO
而不是 IO.Par
的函数。然而,这样做的缺点是现在您需要从 IO
转换为 IO.Par
然后再转换回来——几乎没有什么改进。
为了解决这个问题,我在 cats 2.0 中添加了 parUnorderedTraverse
方法,它将为您处理这些转换。而且因为这一切都是并行发生的,所以它也会更有效率!还有 parUnorderedSequence
、parUnorderedFlatTraverse
和 parUnorderedFlatSequence
.
我还应该指出,这不仅适用于 IO
,而且适用于具有 Parallel
实例的所有其他内容,例如 Either[A, ?]
(其中 A
是CommutativeSemigroup
)。 List
/ZipList
应该也可以,但似乎还没有人费心去做。
我有一个字符串到 IO 的映射,像这样 Map[String, IO[String]]
,我想将它转换成 IO[Map[String, String]]
。怎么做?
你必须小心一点。 Scala 中的地图是无序的,所以如果你尝试像这样使用猫的 sequence
…
import cats.instances.map._
import cats.effect.IO
import cats.UnorderedTraverse
object Example1 {
type StringMap[V] = Map[String, V]
val m: StringMap[IO[String]] = Map("1" -> IO{println("1"); "1"})
val n: IO[StringMap[String]] = UnorderedTraverse[StringMap].unorderedSequence[IO, String](m)
}
您将收到以下错误:
Error: could not find implicit value for evidence parameter of type cats.CommutativeApplicative[cats.effect.IO]
这里的问题是 IO monad 实际上不是可交换的。这是 definition of commutativity:
map2(u, v)(f) = map2(v, u)(flip(f)) // Commutativity (Scala)
这个定义表明即使效果以不同的顺序发生,结果也是相同的。
您可以通过提供 CommutativeApplicative[IO]
的实例来编译上述代码,但这仍然不能使 IO monad 可交换。如果您 运行 以下代码,您可以看到副作用未按相同顺序处理:
import cats.effect.IO
import cats.CommutativeApplicative
object Example2 {
implicit object FakeEvidence extends CommutativeApplicative[IO] {
override def pure[A](x: A): IO[A] = IO(x)
override def ap[A, B](ff: IO[A => B])(fa: IO[A]): IO[B] =
implicitly[Applicative[IO]].ap(ff)(fa)
}
def main(args: Array[String]): Unit = {
def flip[A, B, C](f: (A, B) => C) = (b: B, a: A) => f(a, b)
val fa = IO{println(1); 1}
val fb = IO{println(true); true}
val f = (a: Int, b: Boolean) => s"$a$b"
println(s"IO is not commutative: ${FakeEvidence.map2(fa, fb)(f).unsafeRunSync()} == ${FakeEvidence.map2(fb, fa)(flip(f)).unsafeRunSync()} (look at the side effects above^^)")
}
}
输出如下:
1
true
true
1
IO is not commutative: 1true == 1true (look at the side effects above^^)
为了解决这个问题,我建议您制作具有顺序的地图,例如列表,其中序列不需要交换性。以下示例只是执行此操作的一种方法:
import cats.effect.IO
import cats.implicits._
object Example3 {
val m: Map[String, IO[String]] = Map("1" -> IO {println("1"); "1"})
val l: IO[List[(String, String)]] = m.toList.traverse[IO, (String, String)] { case (s, io) => io.map(s2 => (s, s2))}
val n: IO[Map[String, String]] = l.map { _.toMap }
}
在这里使用 unorderedTraverse
会很好,但正如 codenoodle 指出的那样,它不起作用,因为 IO
不是可交换的应用程序。然而有一种类型是,它叫做IO.Par
。顾名思义,它的 ap
组合子不会顺序执行而是并行执行,所以它是可交换的——执行 a 然后执行 b 与执行 b 然后执行 a 不同,但同时执行 a 和 b 是和同时做b和a一样。
因此,您可以使用 unorderedTraverse
和 return IO
而不是 IO.Par
的函数。然而,这样做的缺点是现在您需要从 IO
转换为 IO.Par
然后再转换回来——几乎没有什么改进。
为了解决这个问题,我在 cats 2.0 中添加了 parUnorderedTraverse
方法,它将为您处理这些转换。而且因为这一切都是并行发生的,所以它也会更有效率!还有 parUnorderedSequence
、parUnorderedFlatTraverse
和 parUnorderedFlatSequence
.
我还应该指出,这不仅适用于 IO
,而且适用于具有 Parallel
实例的所有其他内容,例如 Either[A, ?]
(其中 A
是CommutativeSemigroup
)。 List
/ZipList
应该也可以,但似乎还没有人费心去做。