Эффект кошки: Как преобразовать Map[x,IO[y]] в IO[Map[x,y]]
У меня есть карта строки IO, как это Map[String, IO[String]]
Я хочу превратить его в IO[Map[String, String]]
, Как это сделать?
1 ответ
Вы должны быть немного осторожны с этим. Карты в 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 на самом деле не коммутативна. Вот определение коммутативности:
map2(u, v)(f) = map2(v, u)(flip(f)) // Commutativity (Scala)
Это определение показывает, что результат одинаков, даже если эффекты происходят в другом порядке.
Вы можете скомпилировать приведенный выше код, предоставив экземпляр CommutativeApplicative[IO]
но это все еще не делает монаду IO коммутативной. Если вы запустите следующий код, вы увидите, что побочные эффекты не обрабатываются в том же порядке:
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^^)
Чтобы обойти это, я бы предложил сделать на карте что-то с порядком, например List, где последовательность не требует коммутативности. Следующий пример - только один из способов сделать это:
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
здесь, но, как указал коденудл, это не работает, потому что IO
не является коммутативным аппликативом. Однако есть тип, который называетсяIO.Par
. Как следует из названия, этоap
комбинатор не будет выполнять вещи последовательно, а параллельно, поэтому он коммутативен - выполнение a и затем b не то же самое, что выполнение b и затем a, но выполнение a и b параллельно - то же самое, что выполнение b и a параллельно.
Итак, вы можете использовать unorderedTraverse
используя функцию, которая не возвращает IO
но IO.Par
. Однако недостатком этого является то, что теперь вам нужно конвертировать изIO
к IO.Par
а затем обратно - вряд ли улучшение.
Чтобы решить эту проблему, я добавил parUnorderedTraverse
в cats 2.0, который позаботится об этих преобразованиях за вас. И поскольку все это происходит параллельно, это также будет более эффективно! Это такжеparUnorderedSequence
, parUnorderedFlatTraverse
а также parUnorderedFlatSequence
.
Следует также отметить, что это работает не только для IO
но также и для всего остального с Parallel
например, например Either[A, ?]
(где A
это CommutativeSemigroup
). Также должно быть возможноList
/ZipList
, но, похоже, пока никто не позаботился об этом.