ZIO

Функциональное программирование

Рахимжанов Тимур

Проблема контроля побочных эффектов

Правила работы с побочными эффектами

  • Любое взаимодействие с внешним миром должно быть четко обозначено
  • Любой результат взаимодействия с миром должен быть завернут в “безопасную” оболочку
  • Для любого варианта возможного поведения стороннего эффекта необходимо четко указать сценарий реагирования.

trait DataBaseService[T] {
	def find(id: String): T
	def insert(entity: T): Unit
}

object SideEffect {
	case class Person(id: String, name: String, age: Int)

	def addPersonIfNotExists(person: Person): Unit = ???
}
						

trait DataBaseService[T] {
	def find(id: String): Either[Throwable, Option[T]]
	def insert(entity: T): Either[Throwable, Unit]
}

object SideEffect {
	case class Person(id: String, name: String, age: Int)

	def addPersonIfNotExists(person: Person): Unit = ???
}
		
						

trait DataBaseService[T] {
	def find(id: String): Either[Throwable, Option[T]]
	def insert(entity: T): Either[Throwable, Unit]
}

object SideEffect {
	case class Person(id: String, name: String, age: Int)

	def addPersonIfNotExists(person: Person)(dbService: DataBaseService[Person]): Unit = {
		val personQueryResult = dbService.find(person.id)
		// Проверяем, что запрос прошел без ошибок
		val result: Either[Throwable, Unit] =
			if (personQueryResult.isRight) {
				// Извлекаем объект из контейнера Either
				val existsPersonOpt = personQueryResult.getOrElse(None)
				// Проверяем, что запрос вернул не пустой объект
				val insertQueryResult = 
					if (existsPersonOpt.isEmpty)
						dbService.insert(person)
					else 
						Right(()) // Наличие сущ. записи - не ошибка. Значит, ставим заглушку

				insertQueryResult
			} else {
				personQueryResult.map(_ => ()) // Возвращаем наш запрос с ошибкой,
											   					     // но приводим к нужному типу
			}
		result.fold(err => println(err.getMessage()), res => res)
	}
}
						

trait DataBaseService[T] {
	def find(id: String): Either[Throwable, Option[T]]
	def insert(entity: T): Either[Throwable, Unit]
}

object SideEffect {
	case class Person(id: String, name: String, age: Int)

	def addPersonIfNotExists(person: Person)(dbService: DataBaseService[Person]): Unit = {
 		dbService.find(person.id)
			.flatMap { existsPersonOpt => 
				existsPersonOpt
					.map(person => dbService.insert(person))
					.getOrElse(Right(()))
			}.fold(err => println(err.getMessage()), res => res)
	}
}
						

trait DataBaseService[T] {
	def find(id: String): Either[Throwable, Option[T]]
	def insert(entity: T): Either[Throwable, Unit]
}

object SideEffect {
	case class Person(id: String, name: String, age: Int)

	def addPersonIfNotExists(person: Person)(dbService: DataBaseService[Person]): Unit = {
		for {
			existsPersonOpt <- dbService.find(person.id)
			_ <- existsPersonOpt.map(dbService.insert).getOrElse(Rigth(()))
		} yield ()
	}.fold(err => println(err.getMessage()), res => res)
}
						

Функциональные эффекты

  • Функциональный эффект - полиморфный тип объекта, являющийся надстройкой над функцией.
    Он позволяет работать с результатом вызова функции без необходимости реального вызова ее выполнения.

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

  • Функциональный эффект - IO монада.


case class IO[A](unsafeFunction: Function[Unit, A]) {
	
	def unsafeRun(): A = unsafeFunction()

	def unit[B](x: B): IO[B] = IO(_ => x)

	def map[B](f: A => B): IO[B] = IO[B](unsafeFunction andThen f)
	
	def flatMap[B](f: A => IO[B]): IO[B] = (unsafeFunction andThen f)()

}
					
  • Идеальная программа в функциональном стиле - композиция функций.
  • Функциональные эффекты реализуют эту идею, комбинируя функции в одном объекте.
  • Программа с использованием функциональных эффектов или IO монад - один огромный объект монада, который как клубок ниток содержит в себе все функции. При запуске программы этот клубок постепенно распутывается, и мы получаем результат.

val randomFirst: IO[Int] = IO(_ => Random.nextInt(25))
val randomSecond: IO[Int] = IO(_ => Random.nextInt(25))

lazy val printOfTwoRandomNumsIO =
	randomFirst
		.flatMap(f => randomSecond.map(s => (f, s)))
		.map { case (f, s) => f + s }
		.map { result => println(result) }
	
printOfTwoRandomNumsIO.unsafeRun()
					

val randomFirst: IO[Int] = IO(_ => Random.nextInt(25))
val randomSecond: IO[Int] = IO(_ => Random.nextInt(25))

lazy val printOfTwoRandomNumsIO =
	for {
		f <- randomFirst
		s <- randomSecond
		sum <- IO(_ => f + s)
		_ <- IO(_ => println(sum))
	} yield ()
	
printOfTwoRandomNumsIO.unsafeRun()
					

ZIO

ZIO[-R, +E, +A]

R - тип зависимостей

E - тип ошибки

A - тип результата


ZIO[R, E, A] ⇔ R => Either[E, A]

ZIO[Clock with Random with DBService, Throwable, String]

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

ZIO Aliases

  • Task[+A] = ZIO[Any, Throwable, A]
    • Эффект не требует никаких внешних зависимостей
    • Может вернуть ошибку типа Throwable
    • При удачном завершении возвращает A

  • UIO[+A] = ZIO[Any, Nothing, A]
    • Эффект не требует никаких внешних зависимостей
    • Эффект не возвращает никаких ошибок
    • При удачном завершении возвращает A

ZIO Aliases

  • RIO[-R, +A] = ZIO[R, Throwable, A]
    • Необходимы зависимости типа R для запуска
    • Может вернуть ошибку типа Throwable
    • При удачном завершении возвращает A

  • IO[+E, +A] = ZIO[Any, E, A]
    • Эффект не требует никаких внешних зависимостей
    • Может вернуть ошибку типа E
    • При удачном завершении возвращает A

  • URIO[-R, +A] = ZIO[R, Nothing, A]
    • Необходимы зависимости типа R для запуска
    • Эффект не возвращает никаких ошибок
    • При удачном завершении возвращает A

Создание ZIO объектов

Элементарные конструкторы


						val ok: ZIO[Any, Nothing, Int] = ZIO.succeed(42)

						val fail: IO[Throwable, Nothing] = ZIO.fail(new Throwable("Error!"))
					

Конструкторы из Scala объектов


						val fromOption: IO[Option[Nothing], String] = ZIO.fromOption(Some("Hello"))
						// Если Option - Some, то возвращает его содержимое
						// если Option - None, то возвращает пустую ошибку

						val fromEither: IO[String, Int] = ZIO.fromEither(Left[String, Int]("Not good"))
						// Right - возвращает содержимое как результат
						// Left - возвращает содержимое как ошибку

						val fromTry: Task[Int] = ZIO.fromTry(Try("24".toInt))
						// Если небыло исключения - возвращает результат
						// в случае исключения - возвращает Throwable
					

Базовые операции

Map

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


						// ZIO[Any, Nothing, Int] => ZIO[Any, Nothing, String]
						val map: ZIO[Any, Nothing, String] = ZIO.succeed(42).map(_.toString)
					

FlatMap

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


						// В данном случае вернет 48
						val flatMapSuccess: ZIO[Any, Nothing, Int] = 
							ZIO.succeed(25).flatMap(f => ZIO.succeed(23 + f))
						
						// Если один из ZIO возвращает ошибку, то и их комбинация через flatMap
						// также будет возвращать ошибку
						val flatMapFail: ZIO[Any, String, Int] =
							ZIO.succeed.flatMap(f => ZIO.fail("Error"))
					

For-yield


						val forYield: ZIO[Any, Serializable, String] =
						for {
							f <- ZIO.fromOption("42".toIntOption) // f = 42
							s <- ZIO.fromOption("0".toIntOption) // s = 0
							div <- ZIO.fromTry(Try(f / s)) // zio перейдет в состояние ошибки
							result <- ZIO.succeed("ok")    // эта часть не будет выполнена
						} yield result
						// в forYield будет ZIO с ошибкой типа Throwable
					

ZIP


						val helloWorld: ZIO[Any, Nothing, (String, String)] = 
							(ZIO.succeed("Hello") zip ZIO.succeed("World"))
					

Zip действует аналогично методу zip у объекта Option
Если хоть один из zio возвращает ошибку, то и их zip возвращает ошибку.
Если же оба возвращают значения, то zip вернет кортеж из них.

Обработка ошибок

MapError


						val errorWithType: ZIO[Any, SpecialError, Unit] =
							ZIO.fail(new Throwable("Error!"))
								.mapError(err => SpecialError(err.getMessage))
					

Работает аналогично map, только применяет заданную функцию к объекту ошибки

MapError

Ошибки можно комбинировать


						val zio1 = ZIO.fail("First zio error;")

						val zio2 = ZIO.succeed("hello")
							.flatMap(_ => zio1).mapError(err => err + " Second zio error;")

						val zio3 = ZIO.succeed("world")
							.flatMap(_ => zio2).mapError(err => err + " Third zio error;")

						zio3.catchAll(err => Console.printLine(err))
					

В результате на экран будет выведено:

First zio error; Second zio error; Third zio error;

CatchAll

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

						val errorCatchedAll: ZIO[Any, Nothing, String] = {
							for {
								_ <- ZIO.fromOption(None)
								_ <- ZIO.fail(new Throwable("F Error"))
								_ <- ZIO.fail(SpecialError("Special"))
							} yield "ok"
						}.catchAll { err: Serializable =>
							ZIO.succeed("Failed with error " + err.toString)
						}
					

CatchSome

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

						val errorCatchedSome: ZIO[Any, Serializable, String] = {
							for {
								_ <- ZIO.fromOption(None)
								_ <- ZIO.fail(new Throwable("F Error"))
								_ <- ZIO.fail(SpecialError("Special"))
							} yield "ok"
						}.catchSome {
							case err: Option[Nothing] => ZIO.succeed("Not found option")
							case SpecialError(errMsg) => ZIO.succeed("Special error " + errMsg)
						}
					

OrElse


						val orElse = ZIO.fromOption(None).orElse(ZIO.succeed(25))

						val orElseSucceed = ZIO.fromOption(None).orElseSucceed(25)

						val orElseFail = 
							ZIO.fromOption(None).orElseFail(new Throwable("Объект не найден"))
					

Управление зависимостями

ZIO[-R, +E, +A]

R - тип зависимостей

E - тип ошибки

A - тип результата

ZIO.service


						trait NameService {
							def name: String
						}
						   
						trait AgeService {
							def age: Int
						}

						val nameWithAge: ZIO[NameService with AgeService, Nothing, String] =
						for {
							nameServ <- ZIO.service[NameService]
							ageServ <- ZIO.service[AgeService]
							res = nameServ.name + ageServ.age
						} yield res
					

B with A ⇔ A with B


						type PersonInfo = NameService with AgeService
						val useAandB: ZIO[PersonInfo, Nothing, String] =
							for {
								nameServ <- ZIO.service[NameService]
								ageServ <- ZIO.service[AgeService]
								res = nameServ.name + ageServ.age
							} yield res
					

						trait Logger {
							def log(str: String): Task[Unit] // ZIO[Any, Throwable, Unit]
						}   

						val zioWithLogAge: ZIO[Logger with AgeService, Throwable, Int] =
							for {
								ageServ <- ZIO.service[AgeService]
								logger <- ZIO.service[Logger]
								age = ageServ.age
								_ <- logger.log(age.toString)
							} yield age

						val zioWithAwithNwithLogger: ZIO[Logger with AgeService with NameService, 
																			 		   Throwable, String] =
							for {
								nameServ <- ZIO.service[NameService]
								name = nameServ.name
								age <- zioWithLogAge
								res = name + age
							} yield res
					

serviceWith


						object AgeZio {
							def ageZ: ZIO[AgeService, Nothing, Int] = ZIO.serviceWith[AgeService](_.age)
						}

						object LoggerZio {
							def log(str: String): ZIO[Logger, Throwable, Unit] = 
								ZIO.serviceWithZIO[Logger](_.log(str))
						}

						val zioWithLogAge: ZIO[Logger with AgeService, Throwable, Int] =
							for {
								age <- AgeZio.ageZ
								_ <- LoggerZio.log(age.toString)
							} yield age
					

Старт программы


						object Main extends ZIOAppDefault {
							import Temps._
						 
							override def run: ZIO[ZIOAppArgs with Scope, Any, Any] = {
								zioWithLogAge
									.map(res => println(res))
							}
						}
					

Call your effect's provide method with the layers you need.

ZLayer

type ZLayer[-RIn, +E, +ROut] =
RIn => async Either[E, ROut]

ZLayer.succeed


						object LoggerObj {
							class LoggerImpl extends Logger {
							  override def log(str: String): Task[Unit] = Console.printLine(str)
							}
						   
							val live: ZLayer[Any, Nothing, Logger] = ZLayer.succeed(new LoggerImpl)
						}
					

ZLayer.fromFunction


						object AgeObj {
							trait AgeService {
							  def age: Int
							}
						   
							class AgeServiceImpl(nameService: NameService) extends AgeService {
								override def age: Int =
									nameService.name match {
										case "Александр" => 21
										case "Василий" => 40
										case _ => 20
									}
							}
						   
							val live: ZLayer[NameService, Nothing, AgeService] =
								ZLayer.fromFunction(new AgeServiceImpl(_))
						}
					

ZLayer.fromZIO


						object NameObj {
							trait NameService {
							  def name: String
							}
						   
							class NameServiceImpl extends NameService {
								override def name: String =
									List("Александр", "Василий", "Роман")(util.Random.nextInt(3))
							}
						   
							val live: ZLayer[Any, Nothing, NameService] =
								ZLayer.fromZIO(ZIO.succeed(new NameServiceImpl()))
						}
					

						zioWithLogAge: ZIO[Logger with AgeService, Throwable, Int]

						LoggerObj.live: ZLayer[Any, Nothing, Logger]

						AgeObj.live: ZLayer[NameService, Nothing, AgeService]

						NameObj.live: ZLayer[Any, Nothing, NameService]
					

Операции со слоями


						// Объединение
						LoggerObj.live ++ AgeObj.live
						ZLayer[NameService, Nothing, LoggerObj.Logger with AgeObj.AgeService]

						// Внедрение
						NameObj.live >>> AgeObj.live
						ZLayer[Any, Nothing, AgeObj.AgeService]

						// Комбинация
						NameObj.live >+> AgeObj.live  <=> NameObj.live ++ (NameObj.live >>> AgeObj.live)
						ZLayer[Any, Nothing, AgeObj.AgeService with NameObj.NameService]
					

provideLayer


						object Main extends ZIOAppDefault {
							import Temps._
						 
							override def run: ZIO[ZIOAppArgs with Scope, Any, Any] = {
								val layer = (NameObj.live >>> AgeObj.live) ++ LoggerObj.live
								zioWithLogAge
									.map(res => println(res))
									.provideLayer(layer)
							}
						}
					

Документация

https://zio.dev/overview/getting-started

Литература

Zionomicon