ZIO & Cats Effect: удачный союз

в 12:41, , рубрики: cats, cats io, functional programming, monix, scala, scalaz, zio, Блог компании Конференции Олега Бунина (Онтико), Программирование, функциональное программирование

Cats Effect стал своего рода «Reactive Streams» для функционального Scala-мира, позволив объединить всю разнообразную экосистему библиотек вместе.

Многие отличные библиотеки: http4s, fs2, doobie — реализуются только на базе тайп классов из Cats Effect. А библиотеки типа ZIO и Monix, уже в свою очередь, предоставляют инстансы этих тайп классов для своих типов эффектов. Несмотря на некоторые проблемы, которые будут исправлены в версии 3.0, Cats Effect помогает многим опенсорс контрибьюторам органично поддерживать всю функциональную экосистему языка Scala. Разработчики, которые используют Cats Effect, сталкиваются с трудным выбором: какую реализацию эффектов использовать для своих приложений.

На сегодня есть три альтернативы:

  • Cats IO, ссылочная реализация;
  • Monix, тип данных Task и связанная с ним реактивность в коде;
  • ZIO, тип данных ZIO и его прицел на многопоточность.

В этом посте я постараюсь доказать вам, что для создания своего приложения с использованием Cats Effect, ZIO — хороший выбор с дизайн решениями и возможностями, довольно сильно отличающимися от ссылочной реализации в Cats IO.

1. Более удачная MTL/ Tagless-Final архитектура

MTL (Monad Transformers Library) — это стиль программирования, в котором функции полиморфны по их типу эффекта, и выражают свои требования через «type class constraint». В Скале это часто называется tagless-final стиль (хотя это и не одно и тоже), особенно, когда тайп класс не имеет законов.

Хорошо известно, что невозможно определить глобальный инстанс для таких классических MTL тайп классов, как Writer и State, а также для таких типов эффектов, как Cats IO. Проблема в том, что инстансы этих тайп классов для подобных типов эффектов требуют доступа к изменяемому состоянию (mutable state), которое не может быть создано глобально, потому что создание изменяемого состояния тоже является эффектом.

Для лучшей производительности, тем не менее, важно избегать «monad transformers» и предоставлять реализацию Write и State напрямую, поверх основного типа эффекта.

Чтобы добиться этого, Scala-программисты используют хитрость: создают с эффектами (но чисто) экземпляры на верхнем уровне своих программ и затем предоставляют их далее в программе как локальные имплиситы:

Ref.make[AppState](initialAppState).flatMap(ref =>
  implicit val monadState = new MonadState[Task, AppState] {
    def get: Task[AppState] = ref.get 

    def set(s: AppState): Task[Unit] = ref.set(s).unit
  }

  myProgram
)

Несмотря на то, что такой трюк полезен, это все-таки «костыль». В идеальном мире, все экземпляры тайп классов могли бы быть когерентными (один экземпляр на один тип), а не создаваться локально, порождая эффекты, чтобы затем магическим образом оборачиваться в имплисит значения для использования последующими методами.

Замечательное свойство MTL/tagless-final состоит в том, что вы можете напрямую определять большинство экземпляров поверх типа данных ZIO, используя ZIO окружение.

Вот один из способов создать глобальное определение MonadState для типа данных ZIO:


trait State[S] {
  def state: Ref[S]
}
implicit def ZIOMonadState[S, R <: State[S], E]: MonadState[ZIO[R, E, ?], S] =
  new MonadState[ZIO[R, E, ?], S] {
    def get: ZIO[R, E, S] = ZIO.accessM(_.state.get)

    def set(s: S): ZIO[R, E, Unit] = ZIO.accessM(_.state.set(s).unit)
}

Экземпляр теперь определен глобально для любой среды, которая поддерживает хотя бы State[S].

Аналогично для FunctorListen, иначе известного как MonadWriter:


trait Writer[W] {
  def writer: Ref[W]
}
implicit def ZIOFunctorListen[W: Semigroup, R <: Writer[W], E]: FunctorListen[ZIO[R, E, ?], W] =
  new FunctorListen[ZIO[R, E, ?], W] {
    def listen[A](fa: ZIO[R, E, A]): ZIO[R, E, (A, W)] = 
    ZIO.accessM(_.state.get.flatMap(w => fa.map(a => a -> w)))

    def tell(w: W): ZIO[R, E, W] = 
    ZIO.accessM(_.state.update(_ |+| w).unit)
}

И, конечно, мы можем сделать тоже самое для MonadError:


implicit def ZIOMonadError[R, E]: MonadError[ZIO[R, E, ?], E] = 
  new MonadError[ZIO[R, E, ?], E]{
    def handleErrorWith[A](fa: ZIO[R, E, A])(f: E => ZIO[R, E, A]): ZIO[R, E, A] = 
      fa catchAll f

    def raiseError[A](e: E): ZIO[R, E, A] = ZIO.fail(e)
}

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

Больше никаких медленных монадных преобразований! Скажем «нет» созданию эффектов при инициализации экземпляров тайп класса, локальным имплиситам. Не надо больше никаких «костылей». Прямое погружение в чистое функциональное программирование.

2. Экономия ресурсов для простых смертных

Одной из первых возможностей ZIO было прерывание (interraption) — способность среды выполнения ZIO мгновенно прервать любой исполняемый эффект и гарантированно освободить все ресурсы. Сырая реализация этой возможности попала в Cats IO.

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

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

Рассмотрим следующий эффект:


for {
  handle <- openFile(file)
  data <- readFile(handle)
  _ <- closeFile(handle)
} yield data

Большинство разработчиков не удивится такому сценарию: closeFile не будет исполнено, если readFile упадет. К счастью, система эффектов имеет ensuring (guarantee в Cats Effect), который позволяет вам добавлять завершающий обработчик к эффекту finalizer, похожий на finally.

Итак, главная проблема кода выше может быть легко решена:


for {
  handle <- openFile(file)
  data <- readFile(handle).ensuring(closeFile(handle))
} yield ()

Теперь эффект стал «устойчивым к падениям», в том смысле, что если readFile сломается, файл все равно будет закрыт. А если readFile выполнится успешно, то файл тоже будет закрыт. Во всех случаях, файл будет закрыт.

Но все же не совсем во всех. Прерывание означает, что исполняющийся эффект может быть прерван везде, даже между openFile и readFile. Если такое произойдет, открытый файл не будет закрыт, и случится утечка ресурсов.

Паттерн получения и освобождения ресурса настолько распространен, что ZIO представила bracket оператор, который также появился и в Cats Effect 1.0. Bracket оператор защищает от прерываний: если получение ресурса прошло успешно, то освобождение произойдет, даже если эффект, использующий ресурс, будет прерван. Далее, ни получение, ни освобождение ресурса не может быть прервано, таким образом предоставляется гарантия ресурсобезопасности.

С использованием bracket, пример выше будет выглядеть так:

openFile(file).bracket(closeFile(_))(readFile(_))

К сожалению, bracket энкапсулирует только один (довольно общий) паттерн потребления ресурсов. Есть и много других, особенно с конкурентными структурами данных, получение доступа над которыми должно быть доступным для прерываний, иначе возможны утечки.

Вообще, вся работа с прерываниями сводится к двум основным вещам:

  • не допустить прерывания в некоторых участках, которые могут быть прерваны;
  • допустить прерывание в участках, которые могут зависать.

ZIO имеет возможности для реализации и того, и другого. Например, мы можем разработать свою версию bracket, используя низкоуровневые абстракции ZIO:


ZIO.uninterruptible {
  for {
    a <- acquire
    exit <- ZIO.interruptible(use(a))
    .run.flatMap(exit => release(a, exit)
    .const(exit))
    b <- ZIO.done(exit)
  } yield b
}

В этом коде, use(a) — единственная часть, которая может быть прервана. Окружающий код гарантирует исполнение release в любом случае.

В любой момент можно проверить, есть ли возможность для прерываний. Для этого необходимы лишь две примитивные операции (все остальные являются производными от них).

Эта композиционная полнофункциональная модель прерываний позволяет осуществить не только простую реализацию bracket, но и реализацию других сценариев в управлении ресурсами, в которых найден баланс между достоинствами и недостатками прерываний.

Cats IO предоставляет только одну операцию для управления прерываниями: комбинатор «uncancelable». Он делает целый блок кода непрерываемым. Хотя данная операция используется довольно редко, она может привести к утечке ресурсов или блокировкам.

В то же время выясняется, что можно определить примитив, внутри Cats IO, который позволяет добиться большего контроля над прерываниями. Весьма сложная реализация Фабио Лабелла оказалась крайне медленной.

ZIO позволяет писать код с прерываниями, оперируя на высоком уровне с декларативными составными операторами, и не заставляет вас выбирать между суровой сложностью в совокупности с низкой производительностью и утечками с блокировками.

Более того, недавно добавленная Software Transactional Memory в ZIO позволяет пользователю декларативно писать структуры данных и код, которые автоматически асинхронны, конкурентны и допускают прерывания.

3. Гарантированные Finalizers

Try/finally блок во многих языках программирования предоставляет гарантии, которые необходимы для написания синхронного кода без утечек ресурсов.

В частности, этот блок гарантирует следующее: если try блок начал исполнение, тогда блок finally исполнится, когда try остановится.

Эта гарантия сохраняется в случаях:

  • есть вложенные «try/finally» блоки;
  • есть ошибки в «try блоке»;
  • есть ошибки во вложенном finally блоке.

ZIO «ensuring» операцию можно использовать точно также, как и try/finally:

val effect2 = effect.ensuring(cleanup)

ZIO предоставляет следующие гарантии на «effect.ensuring(finalizer)»: если «effect» начал исполняться, то «finalizer» начнет исполнение, когда «effect» остановится.

Как и «try / finally», эти гарантии сохраняются в случаях:

  • есть вложенные «ensuring» композиции;
  • есть ошибки в «effect»;
  • есть ошибки во вложенных «finalizer».

Более того, гарантия сохраняется даже в случае, если эффект подвергается прерыванию (гарантии на «bracket» похожи, по факту, «bracket» реализована на «ensuring»).

Тип данных Cats IO реализует другую, более слабую гарантию. Для «effect.guarantee (finalizer)», она ослабляется следующим образом: если «effect» начал исполняться, «finalizer» начнет исполнение, когда «effect» остановится, если проблемный эффект не вставлен в «effect».

Более слабая гарантия также встречается в реализации «bracket» в Cats IO.

Чтобы получить утечку ресурсов, достаточно эффект используемый внутри «guarantee» или «bracket.use» эффект, скомпозировать с чем-нибудь подобным:

// Предполагается что `interruptedFiber` какой-то уже прерванный поток
val bigTrouble = interruptedFiber.join

Когда bigTrouble вставляется таким образом в другой эффект, эффект становится непрерываемым — никакие «finalizers», установленные через «guarantee», или очистка ресурсов через «bracket» не будут исполнены. Все это приводит к утечке ресурсов, даже когда в блоке есть «finalizer».

Например, «finalizer» в следующем коде никогда не начнет исполнение:

(IO.unit >> bigTrouble).guarantee(IO(println("Won’t be executed!!!«)))

Оценивая код без учета глобального контекста, невозможно определить, будет ли эффект, такой как «bigTrouble», вставлен где-либо в «use» эффект операции «bracket» или внутрь блока «finalizer».

Поэтому вы не сможете узнать, будет ли Cats IO программа работать с утечками ресурсов или пропущенными «finalizer» блоками, не оценив всей программы целиком. Всю программу целиком можно оценить только вручную, а это процесс всегда сопровождается ошибками, которые не могут быть проверены компилятором. К тому же, этот процесс нужно повторять каждый раз, когда происходят какие-либо важные изменения в коде.

ZIO имеет кастомную реализации «guarantee» из Cats Effect, «guaranteeCase» и «bracket». Реализации используют нативную ZIO семантику (не Cats IO семантику), которая позволяет здесь и сейчас оценить возможные проблемы по утечкам ресурсов, зная, что во всех ситуациях «finalizers» будет запущен, и ресурсы будут освобождены.

4. Стабильное переключение

В Cats Effect есть метод «evalOn» из «ContextShift», который позволяет перемещать исполнение некоторого кода в другой контекст исполнения.

Это полезно по целому ряду причин:

  • многие клиентские библиотеки принуждают вас делать некоторую работу в их пуле потоков (Thread pool);
  • UI библиотеки требуют, чтобы какие-то обновления происходили в UI потоке;
  • некоторые эффекты требуют изоляции на пулах потоков, адаптированным к их специфическим особенностям.

«EvalOn» операция исполняет эффект там, где он должен быть запущен, и затем возвращается в изначальный контекст исполнения. Например:

cs.evalOn(kafkaContext)(kafkaEffect)

Замечание: Cats IO имеет похожую конструкцию «shift», которая позволяет переключиться на другой контекст без возвращения назад, но на практике, такое поведение редко требуется, поэтому «evalOn» — вариант предпочтительнее.

ZIO реализация «evalOn» (сделанная на ZIO примитиве «lock») предоставляет гарантии, необходимые для однозначного понимания, где эффект работает, — эффект всегда будет исполняться на определенном контексте.

Cats IO имеет другую, более слабую гарантию — эффект будет исполняться на определенном контексте до первой асинхронной операции или внутреннего переключения.

Рассматривая маленький кусок кода, невозможно знать наверняка, будет ли асинхронный эффект (или вложенное переключение), встроенным в эффект, который будет переключаться, ведь асинхронность не отображается в типах.

Поэтому, как и в случае с безопасностью ресурсов, для понимания того, где будет запущен эффект Cats IO, необходимо изучение всей программы целиком. На практике, и из моего опыта, пользователи Cats IO удивляются, когда, при использовании `evalOn` в одном контексте, в дальнейшем обнаруживается, что большая часть эффекта случайно была выполнена в другом.

ZIO позволяет вам определить, где эффекты должны запускаться, и довериться тому, что так оно и произойдет во всех случаях, вне зависимости от того, как эффекты встроены в другие эффекты.

5. Сохранность сообщений об ошибках

Любой эффект, который поддерживает конкурентность, параллелизм или безопасный доступ к ресурсам, столкнется с проблемой, связанной с линейной моделью ошибок: в общем случае не все ошибки могут быть сохранены.

Это верно и для `Throwable`, фиксированного типа ошибок, встроенных в Cats IO, и для полиморфного типа ошибок, поддерживаемого ZIO.

Примеры ситуаций с множественными единовременными ошибками:

  • Finalizer бросает исключение;
  • два (падающих) эффекта объединены в параллельное исполнение;
  • два (падающих) эффекта в состоянии гонок;
  • прерванный эффект падает, перед тем как выйти из секции, защищенной от прерываний.

Так как не все ошибки сохраняются, ZIO предоставляет структуру данных «Cause[E]», основанную на свободном полукольце (абстракция из абстрактной алгебры, её знание здесь не предполагается), которая позволяет соединять последовательные и параллельные ошибки для любого типа ошибки. Во время всех операций (включая очистку для упавшего или прерванного эффекта), ZIO агрегирует ошибки в структуру данных «Cause[E]». Эта структура данных доступна в любое время. В результате ZIO всегда хранит все ошибки: они всегда доступны, их можно записать в лог, изучить, трансформировать, как того требуют бизнес требования.

Cats IO выбрала модель с потерей информации об ошибках. В то время как ZIO соединит две ошибки через Cause[E], Cats IO «потеряет» одно из сообщений об ошибках, например, через вызов «e.printStackTrace()» на возникшей ошибке.

Например, ошибка в «finalizer» в этом коде будет потеряна.

IO.raiseError(new Error("Error 1")).guarantee(IO.raiseError(new Error("Error 2«)))

Такой подход к отслеживанию ошибок означает, что нельзя локально обнаруживать и обрабатывать весь спектр ошибок, возникающих вследствие соединения эффектов. ZIO позволяет использовать любые типы ошибок, включая «Throwable» (или более конкретные подтипы как"`IOExceptio" или другую кастомную иерархию исключений), гарантируя, что никакие ошибки не будут потеряны во время исполнения программы.

6. Асинхронность без дедлоков

И ZIO и Cats IO предоставляют конструктор, который позволяет взять код с колбэком и обернуть его в эффект

Эта возможность предоставлена через Async тайп класс в Cats Effect:


val effect: Task[Data] = 
  Async[Task].async(k => 
    getDataWithCallbacks(
      onSuccess = v => k(Right(v)),
      onFailure = e => k(Left(e))
))

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

Заметьте, что как только код с коллбэком оборачивается в эффект, коллбэк-функция (здесь она называется `k`) вызывается. Эта коллбэк функция завершается со значением типа «успешно/ошибка». Когда вызывается эта колбэк функция, исполнение эффекта (до этого приостановленного) возобновляется.

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

Cats IO возобновляет выполнение эффекта в потоке, в котором выполнялся коллбэк. Разница между этими вариантами достаточно глубока: поток, вызывающий колбэк, не ожидает, что код колбэка может выполняться вечно, а допускает лишь небольшую задержку, перед тем как контроль вернется обратно. С другой стороны, Cats IO вообще не дает такой гарантии: вызывающий поток запускающий колбэк может зависнуть, ожидая неопределенное время, когда контроль исполнения вернётся обратно.

Ранние версии конкурентных структур данных в Cats Effect («Deferred», «Semaphore») возобновляли работу эффектов, которые не вернули контроль исполнения вызывающему потоку. В результате, в них были обнаружены проблемы, связанные с дедлоками и сломанным планировщиком исполнения. Хотя все эти проблемы были найдены, они исправлены только для конкурентных структур данных в Cats Effect.

Пользовательский код, который использует похожий подход как и в Cats IO, попадет в подобные неприятности, из-за того что такие задачи недетерминированы, ошибки могут возникать только очень редко, в рантайме, делая отладку и обнаружение проблемы сложным процессом.

ZIO предоставляет защиту от дедлоков и нормальный планировщик задач из коробки, а такжезаставляет пользователя явно выбирать поведение Cats IO (например, используя «unsafeRun» на «Promise», который завершился в возобновлённом асинхронном эффекте).

Хотя ни одно из решений не подходит для абсолютно всех случаев, и ZIO и Cats IO предоставляют достаточно гибкости, чтобы разрешить все ситуации (разными способами), выбор ZIO означает использование «Async» без всяких забот и заставляет помещать проблемный код в «unsafeRun», о котором известно, что он может вызывать дедлок

7. Совместимость с Future

Использование «Future» из стандартной библиотеки Scala реальность для большого числа кодовых баз. ZIO поставляется с методом «fromFuture», который предоставляет готовый контекст исполнения:

ZIO.fromFuture(implicit ec =>
// Create some Future using `ec`:
???
)

Когда этот метод используется, чтобы обернуть «Future» в эффект, ZIO может установить, где «Future» будет исполнена, и другие методы, такие как «evalOn`, правильным образом перенесут «Future» в необходимый контекст исполнения. Cats IO принимает «Future», которая была создана с внешним «ExecutionContext». Это означает, что Cats IO не может переместить выполнение «Future» в соответствии с требованиями методов «evalOn» или «shift». Более того, это обременяет пользователя определением контекста исполнения для «Future», что означает узкий выбор и отдельное окружение.

С тех пор как можно игнорировать предоставленный «ExecutionContext», ZIO может быть представлена как сумма возможностей Cats IO, гарантирующая более плавное и точное взаимодействие с «Future» в общем случае, но при этом все же бывают исключения.

8. Блокирующее IO

Как было показано в статье «Пул потоков. Лучшие практики с ZIO», для серверных приложений необходимо как минимум два отдельных пула для максимальной эффективности:

  • фиксированный пул для CPU/асинхронных эффектов;
  • динамический, с возможностью роста числа блокирующих потоков.

Решение запускать все эффекты на фиксированном пуле потоков однажды приведет к дедлоку, в то время как запуск всех эффектов на динамическом пуле может вести к потери производительности.

На JVM ZIO предоставляет две операции, которые поддерживают блокирующие эффекты:

  • «blocking (effect» оператор, который переключает исполнение определенного эффекта в пуле блокирующих потоков, имеющих хорошие преднастройки, которые можно изменить при желании);
  • «effectBlocking(effect)» оператор, который транслирует блокирующий код с побочными эффектами в чистый эффект, чьё прерывание остановит выполнение большей часть блокирующего кода.

Если у вас есть эффект, и вам нужно быть уверенным, что он исполнится на пуле блокирующих потоков, тогда вы можете обернуть его в «blocking». С другой стороны, если вы обернули какой-то код с побочными эффектами, который блокирует, тогда вы можете обернуть его в «effectBlocking» и получить преимущества от сочетаемых, глубоких и безопасных ZIO прерываний (где это возможно).

Cats IO предоставляет меньшее функциональное ядро, таким образом делегируя реализацию подобной функциональности стороне пользовательского кода. Хотя и существуют библиотеки, которые могут предоставить функциональность «blocking», они основаны на «evalOn», и, соответственно, не могут действительно гарантировать выполнение на пуле блокирующих потоков.

Опытные пользователи могут сконфигурировать их кастомный пул тредов с потоками (что вы можете сделать вместе с ZIO) или создать более чем эти два пула потоков (например, пул потоков для диспетчеризации событий с малой задержкой), но эти операции предоставляют желаемую семантику для абсолютного большинства событий.

9. Бесплатные эффекты

Многие приложения, написанные в функциональном стиле на Scala, в итоге используют одно или два из следующих монадных преобразований:

  • «ReaderT»/ «Kleisli», которые добавляют эффект доступа к окружающей среде;
  • «EitherT», который добавляет эффект типизированных ошибок (или «OptionT», который представляет из себя «EitherT» с типом «Unit» в качестве типа ошибки).

Этот паттерн настолько всеобъемлющ, что целые библиотеки были разработаны вокруг одного или другого (например, http4s много использует «Kleisli» и «OptionT»). Используя продвинутую технику под названием ротация эффектов («effect totation»), ZIO предоставляет и «reader» и «typed error» возможности прямо в типе данных ZIO. Так как не каждый пользователь будет нуждаться в «reader» и «typed error» паттернах, ZIO также предоставляет другие типы, которые покрывают обычные ситуации. Например, «Task[A]», в котором нет «reader» и «typed errors».

Это позволяет ZIO предоставить два наиболее популярных (вторичных) эффекта в функциональных приложениях без какого-либо оверхеда. В дополнение, поддержка этих эффектов напрямую в ZIO упростило оверхед на их запуск, что позволяет писать более простой код.

Cats IO предоставляет только первичный эффект. Это означает, что пользователь, которому нужны «reader» или «typed errors» или нормальная реализация тайп классов «state», «writer» и других, скорее всего начнет пользоваться монадными преобразованиями.

ZIO может быть быстрее в 8 раз чем Cats IO с эквивалентным стэком эффектов. В то время как влияние оверхеда эффекта на производительность приложения зависит от большого числа факторов, большая производительность увеличивает число приложений в функциональном стиле на Scala и позволяет разработчикам строить их приложения из четко очерченных эффектов.

10. Архитектура микроядра

ZIO использует архитектуру микроядра, которая выжимает как можно больше возможностей из рантайма, и переносит их в распоряжение пользователя. Действительно, хотя части микроядра сами по себе написаны в чистой функциональной Scala, еще используются более мелкие составные части.

Оригинальное ядро ZIO содержало примерно 2000 строк кода, но после добавления «typed errors» и окружения освобождения от избыточности и улучшения отрогональности, микроядро целиком сейчас представляет 375 строк в одном файле. Так как сложность современной системы эффектов в Scala выросла, также выросла и возможность возникновения ошибок. В мире очень мало людей, которые понимают как эти системы работают, поэтому возможность потенциальных ошибок и граничных ситуаций очень высока.

Лично я являюсь фанатом системы эффектов на микроядре по следующим причинам:

  • маленький код может быть легко проверен;
  • меньше места для ошибок и скрытых пограничных случаев;
  • можно дешевле, быстрее и надежнее отвечать на запросы пользователей;
  • новым контрибьюторам легче включиться в поддержку ядра.

В теории монолитные ядра могут быть очень хорошо оптимизированы. Тем не менее, из-за добровольной природы опенсорс разработки, мы имеем ограниченные ресурсы для оптимизации.

Из-за этих ограничений вы можете самостоятельно оптимизировать часть микроядра или все микроядро целиком. Первый вариант даст хорошую производительность в определенных случаях, а второй во многих вариантах. Из всех доступных систем эффектов ZIO рантайм является самой маленькой. Cats IO идет на втором месте, но ее рантайм вдове больше, чем ZIO (или втрое, зависит от того как вы будете считать).

11. Подходит начинающим

В ZIO есть много вещей, которые упрощают вхождение новым пользователям, без каких-то потерь или упрощений для более продвинутых пользователей.

  • Простые названия, без жаргона: «ZIO. succeed» вместо «Applicative[F].pure», «zip» вместо «Apply[F].product», «ZIO.foreach» вместо «Traverse[F].traverse».
  • Нет использования типов высших порядков или тайп классов (Cats, Cats Effect, Scalaz инстансы доступны в дополнительных модулях).
  • Нет имплиситов, которые должны быть заимпотрированы или вызваны (за исключением «Runtime», который должен быть имплиситом для Cats Effect проектов из-за текущего дизайна Cats Effect). Имплиситы — это постоянный источник фрустрации для новых пользователей Cats IO.
  • Нет обязательных синтаксических классов.
  • Дружное автодополнению наименование групп методов начинащихся с одного префикса. Например: "zip"/"zipPar", "ZIO.foreach"/"ZIO.foreachPar", "ZIO.succeed"/"ZIO.succeedLazy«.
  • Конкретные методы на конкретных типах данных, которые легко находить и по которым легко «пробегать». Это делает ZIO удобной в разлиных IDE.
  • Преобразование из всех типов данных Scala в ZIO эффект тип: «ZIO.fromFuture», «ZIO.fromOption», «ZIO.fromEither», «ZIO.fromTry».
  • Полная определимость для всех типов данных без различного «колдовства».

Забавно, но мне приходилось видеть людей без опыта в функциональной Scala, которые успешно делали прототипы, используя ZIO без внешней помощи, и перед тем как появилась хорошая документация, даже не подозревая, что используя ZIO, они пишут чистый функциональный код. Cats IO выбрало передачу большей части функциональности, наименований и решений о выводе типов в Cats.

Это сохраняет ссылочную реализацию маленькой, но увеличивает время вхождения для разработчиков, только начавших изучать функциональное программирование, и выражается в известных проблемах удобства (легкость нахождения, наименование, имплиситы и вывод типов).

12. Удобство

ZIO — маленький кросс-платформенный пакет, который дает отличный набор инструментов для построения асинхронных и конкурентных приложений.

Этот набор включает в себя:

  • наиболее важные конкурентные структуры данных, включая «Ref», «Promise», «Queue», «Semaphore» и маленький «Stream» для стриминга чтения файла/сокета/данных;
  • STM, которая может быть использована для построения составных, асинхронных, конкурентных и прерываемых структур данных;
  • «Schedule», который предлагает составные повторения;
  • крохотные и тестируемые «Clock», «Random», «Console» и «System» сервисы, которые используются почти любым приложением;
  • много дополнительных методов для типов эффектов, покрывающих наиболее общие случаи.

Из-за ссылочной реализации Cats IO не имеет таких бонусов. Это решение делает Cats IO более легковесным, но ценой добавления сторонних зависимостей (где это возможно) или написанием недостающего кода самим пользователем.

Заключение

Библиотека Cats Effect сделала много замечательных вещей для Scala-экосистемы, породив множество библиотек, которые теперь работают вместе.

Разработчики приложений, которые используют Cats Effect, сейчас столкнулись с трудностями, связанными с выбором основного эффекта для использования с Cats Effect библиотеками: Cats IO, Monix, Zio.

Разработчики всегда выбирают то, что лучше подходит в конкретной ситуации. Но если вам понравились некоторые из дизайнерских решений, описанных в этом посте, надеюсь, вы увидите: ZIO и Cats Effect вместе образуют убийственную комбинацию.

Хотите лучше разбираться в технологиях и инструментах программирования на Scala или узнать больше хардкорных подробностей — для вас отличная новость. В конце ноября в Москве пройдет первая в России конференция, полностью посвященная Scala. Расписание ScalaConf уже опубликовано, в нем 18 крутых докладов от признанных экспертов, и в том числе John A De Goes собственной персоной.

Автор: Alexander Fedorov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js