Эффект кошки: Как преобразовать 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, но, похоже, пока никто не позаботился об этом.

Другие вопросы по тегам