Эффект кошки - параллельная композиция независимых эффектов

Я хочу объединить несколько IO значения, которые должны работать независимо друг от друга параллельно.

val io1: IO[Int] = ???
val io2: IO[Int] = ???

На мой взгляд, у меня есть варианты:

  1. Используйте волокна кошачьего эффекта с рисунком вилки
    val parallelSum1: IO[Int] = for {
      fiber1 <- io1.start
      fiber2 <- io2.start
      i1 <- fiber1.join
      i2 <- fiber2.join
    } yield i1 + i2
    
  2. Использовать Parallel экземпляр для IO с parMapN (или один из его братьев и сестер, таких как parTraverse, parSequence, parTupled так далее)
    val parallelSum2: IO[Int] = (io1, io2).parMapN(_ + _)
    

Не уверен насчет плюсов и минусов каждого подхода, и когда я должен выбрать один над другим. Это становится еще сложнее, если абстрагироваться от типа эффекта IO (стиль без тегов):

def io1[F[_]]: F[Int] = ???
def io2[F[_]]: F[Int] = ???

def parallelSum1[F[_]: Concurrent]: F[Int] = for {
  fiber1 <- io1[F].start
  fiber2 <- io2[F].start
  i1 <- fiber1.join
  i2 <- fiber2.join
} yield i1 + i2

def parallelSum2[F[_], G[_]](implicit parallel: Parallel[F, G]): F[Int] =
  (io1[F], io2[F]).parMapN(_ + _)

Parallel Класс типов требует 2-х конструкторов типов, что делает его несколько более громоздким в использовании без границ контекста и с дополнительным неопределенным параметром типа G[_]

Ваше руководство ценится:)

Amitay

1 ответ

Я хочу объединить несколько значений ввода-вывода, которые должны работать независимо параллельно.

То, как я это вижу, для того, чтобы выяснить, "когда я использую что?", Нам нужно вернуть обсуждение "старая параллель против параллельной", которое в основном сводится к (цитируя принятый ответ):

Параллельность - это когда две или более задач могут запускаться, выполняться и завершаться в перекрывающиеся периоды времени. Это не обязательно означает, что они оба будут работать одновременно. Например, многозадачность на одноядерной машине.

Параллелизм - это когда задачи выполняются буквально одновременно, например, на многоядерном процессоре.

Нам часто нравится предоставлять пример параллелизма, когда мы выполняем операции, подобные вводу-выводу, такие как создание беспроводного вызова или общение с диском.

Вопрос в том, какой из них вы хотите, когда говорите, что хотите выполнить "параллельно", это первый или второй?

Если мы ссылаемся на первое, то с помощью Concurrent[F] оба передают намерение с помощью подписи и обеспечивают правильную семантику исполнения. Если это последнее, и мы, например, хотим обрабатывать набор элементов параллельно, тогда перейдем к Parallel[F, G] было бы лучшим решением.

Это часто сбивает с толку, когда мы думаем о семантике этого относительно IO потому что у него есть оба экземпляра для Parallel а также Concurrent и мы в основном используем его для непрозрачного определения побочных эффектов.

Как примечание, причина Parallel принимая два одинарных конструкторов типа из-за того, что MParallel[M[_], F[_]]) всегда Monad экземпляр, и нам нужен способ доказать, что Монада имеет Applicative[F] Например, для параллельных выполнений, потому что когда мы думаем о Monad, мы всегда говорим о семантике последовательного выполнения.

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