Часть 6. Cats и Circe

Страничка курса: https://maxcom.github.io/scala-course-2022/

План

  1. Cats Modules
  2. Cats Type Classes
  3. Cats Data Types
  4. Circe

Библиотека Cats

Абстрактные котики, функциональный подход и теория категорий

* Название является шутливым сокращением слова "категория"

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

Монады

(и связанные с ними концепции)

Архитектурные строительные блоки, которые появляются в программах снова и снова.

т.е. являются для ФП эквивалентом шаблонов проектирования ООП

Их преимущества над ООП

  • Формально (а, значит, очень точно) определены
  • Являются максимально обобщёнными

Модули библиотеки Cats

некоторые из них находятся в собственных репозиториях

cats-kernel, cats-core

  • набор type-классов
  • минимальный набор структур данных для их поддержки
  • экземпляры классов типов для этих структур данных и стандартных типов

cats-laws, cats-testkit,
cats-testkit-scalatest

  • набор тестов для проверки собственных инстансов на соответствие законам
  • поддерживается Specs2, ScalaTest, ScalaCheck и др.

cats-effect

  • библиотека для ассинхронных вычислений
  • основной тип IO - очень похож на Future, но более строгий
(рассмотрим подробнее на следующих лекциях)
Есть и другие, но нам пока хватит.

Полезные ссылки:

Рассмотрим наиболее полезные классы

Semigroup

Определена операция

trait Semigroup[A] {
  def combine(x: A, y: A): A
}

Должна удовлетворять закону ассоциативности

combine(x, combine(y, z)) = combine(combine(x, y), z)

Пример

Тип Int и операция сложения

import cats.Semigroup

implicit val intAdditionSemigroup = new Semigroup[Int] {
  def combine(x: Int, y: Int): Int = x + y
}
Поэтому часто операцию "combine" называют "сложением"

cats предоставляет infix-синтаксис для полугруп

import cats.implicits._

1 |+| 2

Ещё примеры

  • Тип Int и операция умножения
  • Тип String и операция "склеивания"
  • Кортежи: пары, тройки и т.д.

Тип Map и операция merge

import cats.implicits._

val map1 = Map("hello" -> 1, "world" -> 1)
val map2 = Map("hello" -> 2, "cats"  -> 3)

Semigroup[Map[String, Int]].combine(map1, map2)
// res1: Map[String, Int] =
//   Map("hello" -> 3, "cats" -> 3, "world" -> 1)

map1 |+| map2
// res2: Map[String, Int] =
//   Map("hello" -> 3, "cats" -> 3, "world" -> 1)

Аналогично для List и других коллекций

Monoid

Полугруппа с "нулём"

trait Semigroup[A] {
  def combine(x: A, y: A): A
}

trait Monoid[A] extends Semigroup[A] {
  def empty: A
}

В математике чаще называют "единицей" или "нейтральным элементом"

Также удовлетворяет закону ассоциативности

combine(x, combine(y, z)) = combine(combine(x, y), z)

И дополнительно закон для "нуля":

combine(x, empty) = combine(empty, x) = x

Большинство полугрупп легко расширяются до моноида.

  • Целые числа (Int, Long), сложение и 0
  • Целые числа (Int, Long), умножение и 1
  • String, конкатенация и пустая строка ""
  • Коллекция, конкатенация и пустая коллекция того же типа:
    • Map[A, B] и Map.empty[A, B]
    • List[A] и List.empty[A]
    • и др.
import cats.Monoid

implicit val intAdditionMonoid: Monoid[Int] = new Monoid[Int] {
  def empty: Int = 0
  def combine(x: Int, y: Int): Int = x + y
}
val x = 1

Monoid[Int].combine(x, Monoid[Int].empty)
// res1: Int = 1

Monoid[Int].combine(Monoid[Int].empty, x)
// res2: Int = 1

Cats предоставляет операции для коллекций над моноидами.

def combineAll[A: Monoid](as: List[A]): A =
      as.foldLeft(Monoid[A].empty)(Monoid[A].combine)
import cats.implicits._

combineAll(List(1, 2, 3))
// res: Int = 6
import cats.implicits._

combineAll(List("hello", " ", "world"))
// res: String = "hello world"
import cats.implicits._

combineAll(List(
             Map('a' -> 1),
             Map('a' -> 2, 'b' -> 3),
             Map('b' -> 4, 'c' -> 5)
          ))
// res: Map[Char, Int] = Map('b' -> 7, 'c' -> 5, 'a' -> 3)
import cats.implicits._

combineAll(List(
             Set(1, 2),
             Set(2, 3, 4, 5)
          ))
// res: Set[Int] = Set(5, 1, 2, 3, 4)

Не всякая полугруппа является моноидом

Но любая полугруппа может им стать, если её завернуть в Option!

Option[A] не является ни моноидом, ни полугруппой:
не определена операция combine

Но Option[A: Semigroup] является!

NonEmptyList - полугруппа, но не моноид

final case class NonEmptyList[A](head: A, tail: List[A]) {

  def ++(other: NonEmptyList[A]) =
                        NonEmptyList(head, tail ++ other.toList)

  def toList: List[A] = head :: tail

}
implicit def nonEmptyListSemigroup[A] =
                                new Semigroup[NonEmptyList[A]] {

  def combine(x: NonEmptyList[A], y: NonEmptyList[A]) = x ++ y

}
implicit def optionMonoid[A: Semigroup] = new Monoid[Option[A]]
{
  def empty: Option[A] = None

  def combine(x: Option[A], y: Option[A]): Option[A] =
    x match {
      case None => y
      case Some(xv) =>
        y match {
          case None => x
          case Some(yv) => Some(xv |+| yv)
        }
    }
}

Такое комбинирование получаем из
Semigroup.combineAllOption

val nel1 = NonEmptyList(1, List(2, 3))
val nel2 = NonEmptyList(4, List(5, 6))

val all = List(nel1, nel2)

Monoid.combineAll(all)
// Ошибка: NonEmptyList не является моноидом
val nel1 = NonEmptyList(1, List(2, 3))
val nel2 = NonEmptyList(4, List(5, 6))

val lifted = List(nel1, nel2).map(nel => Option(nel))

Monoid.combineAll(lifted)
// res: Option[NonEmptyList[Int]] =
//                   Some(NonEmptyList(1, List(2, 3, 4, 5, 6)))

MonoidK

Моноид над конструкторами
типов с одним аргументом

Будем называть их "контейнерами"

В частности - над коллекциями

Monoid[A]

  • Позволяет комбинировать значения A
  • Существует "пустое" значение A, которое функционирует как "ноль"

MonoidK[F]

  • Позволяет комбинировать два значения F[A]
  • Для любого A существует "пустое" значение F[A]
  • Комбинация и пустое значение зависят только от структуры F, но не от структуры A

Метод empty теперь параметризуется
типом элементов

Monoid[List[String]].empty
// res1: List[String] = List()

MonoidK[List].empty[String]
// res2: List[String] = List()

MonoidK[List].empty[Int]
// res3: List[Int] = List()

Метод combineK теперь параметризуется
типом элементов

Monoid[List[String]].combine(
             List("hello", "world"),
             List("bye", "moon"))
// res1: List[String] = List("hello", "world", "bye", "moon")

MonoidK[List].combineK[String](
             List("hello", "world"),
             List("bye", "moon"))
// res2: List[String] = List("hello", "world", "bye", "moon")

MonoidK[List].combineK[Int](List(1, 2), List(3, 4))
// res3: List[Int] = List(1, 2, 3, 4)

SemigroupK

Полугруппа над конструкторами
типов с одним аргументом

Functor

Добавляет операцию map к контейнерам

trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}
Требования к реализации

fa.map(f).map(g) = fa.map(f.andThen(g))   //composition

fa.map(x => x) = fa                       //identity
					

new Functor[Option] {
  def map[A, B](fa: Option[A])(f: A => B): Option[B] =
    fa match {
      case None    => None
      case Some(a) => Some(f(a))
    }
}
					

Cats предоставляет функцию lift


def lift[A, B](f: A => B): F[A] => F[B] = fa => map(fa)(f)
				

"превращает" функцию A => B
в функцию F[A] => F[B]


import cats.Functor
import cats.implicits._

val lenOption: Option[String] => Option[Int] =
   Functor[Option].lift(_.length)
					

lenOption(Some("abcd"))
// res: Option[Int] = Some(4)
					

Cats предоставляет композицию функторов


import cats.Functor
import cats.implicits._

val listOpt = Functor[List] compose Functor[Option]
//Functor[List[Option]]
					

listOpt.map( List(Some(1), None, Some(3)) )(_ + 1)
// res: List[Option[Int]] = List(Some(2), None, Some(4))
					

Cats предоставляет функцию fproduct


def fproduct[A, B](fa: F[A])(f: A => B): F[(A, B)] =
   map(fa)(a => a -> f(a))
					
как "map", но возвращает пару

import cats.Functor
import cats.implicits._

val source = List("Cats", "is", "awesome")
val product = Functor[List].fproduct(source)(_.length)
// res1: List[(String, Int)] =
//    List((Cats,4), (is,2), (awesome,7))
					

product.toMap
// res2: Map[String,Int] =
//    Map(Cats -> 4, is -> 2, awesome -> 7)
					

Semigroupal

trait Semigroupal[F[_]] {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}
Комбинирует два контейнера разного типа

  new Semigroupal[Option] {

    def product[A, B](fa: Option[A],
                      fb: Option[B]): Option[(A, B)] =

      if (fa.isDefined && fb.isDefined)
        Some((fa.get, fb.get))
      else None

  }

Applicative

trait Semigroupal[F[_]] {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}

trait Applicative[F[_]] extends Semigroupal[F] {
  def pure[A](a: A): F[A]
}

Аналогично тому как моноид расширяет полугруппу

pure "заворачивает" значение в контейнер типа
(помещает значение в контекст)

trait Applicative[Option] {
  def pure[A](a: A): F[A] = Some(a)
}
trait Applicative[Future] {
  def pure[A](a: A): F[A] = Future.successful[A](a)
}
trait Applicative[List] {
  def pure[A](a: A): F[A] = List(a)
}
Требования к реализации

fa.product(fb).product(fc) ~ fa.product(fb.product(fc))
                                        //associativity

pure(()).product(fa) ~ fa               //left identity
fa.product(pure(())) ~ fa               //right identity
					

Эквивалентное определение

trait Applicative[F[_]] {
  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

  def pure[A](a: A): F[A]
}

На самом деле в cats

import cats.Functor

trait Applicative[F[_]] extends Functor[F] {
  def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

  def pure[A](a: A): F[A]

  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
    ap(map(fa)(a => (b: B) => (a, b)))(fb)

  def map[A, B](fa: F[A])(f: A => B): F[B] = ap(pure(f))(fa)

}

Любой аппликатив является функтором и полугруппой

Композиция аппликативов

import cats.Applicative

val composed = Applicative[Future].compose[Option]
val x: Future[Option[Int]] = Future.successful(Some(5))
val y: Future[Option[Char]] = Future.successful(Some('a'))

composed.map2(x, y)(_ + _)
// composed: Future[Option[Int]] = Future(Success(Some(102)))

val username: Option[String] = Some("username")
val password: Option[String] = Some("password")
val url: Option[String] = Some("some.login.url.here")

def connect(username: String,
            password: String,
            url: String): Option[Connection] = None

Applicative[Option].map3(username, password, url)(connect)
// res: Option[Option[Connection]] = Some(None)
					

import cats.implicits._

(username, password, url).mapN(connect)
// res: Option[Option[Connection]] = Some(None)
					

def sequenceFuture[A](fa: List[Future[A]]): Future[List[A]]

def traverseFuture[A, B](as: List[A])
                        (f: A => Future[B]): Future[List[B]]
					

import cats.implicits._

List(1, 2, 3).traverse(i => Future.successful(i))
// res: Future[List[Int]] = Future(Success(List(1, 2, 3)))
					

Monad

Applicative с функцией flatten


  Option(Option(1)).flatten
  // res1: Option[Int] = Some(1)

  Option(None).flatten
  // res2: Option[Nothing] = None
					

  List(List(1),List(2,3)).flatten
  // res: List[Int] = List(1, 2, 3)
					
map затем flatten ~ flatMap

Используется в конструкции
for { ... } yield { ... }

Об этом рассказывали в прошлых лекциях

  • Semigroup (полугруппа над скалярами)
  • Monoid (моноид - полугруппа с "нулём")
  • SemigroupK (полугруппа над "контейнерами" с одинаковым параметром)
  • MonoidK (моноид над "контейнерами" с одинаковым параметром)
  • Functor ("контейнер" над скаляром с операцией map)
  • Semigroupal (полугруппа над "контейнерами" с разными параметрами)
  • Applicative (моноид над "контейнерами" с разными параметрами)
  • Monad (аппликатив с функцией flatten)

На самом деле иерархия типов в cats гораздо сложнее

Изучайте документацию и исходники

Cats Data Types

Id

type Id[A] = A

Id - Это монада


  def map[A, B](fa: Id[A])(f: A => B): Id[B]
  def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B]
					

OptionT

OptionT[F[_], A] ~ F[Option[A]]


val firstNameF: Future[String]
                        = askFirstName()

val lastNameFO: Future[Option[String]]
                        = askLastName()

val ticketNumO: Option[String]
                        = getTicketNumber(???, ???)
					

for {
  first  <- OptionT.liftF(firstNameF)
  last   <- OptionT(lastNameFO)
  ticket <- OptionT.fromOption[Future](ticketNumO(first, last))
} yield s"Hello, $last $first. Your ticket $ticket"
					

val greet: OptionT[Future,String] = OptionT.pure("Hola!")

val greetAlt: OptionT[Future,String] = OptionT.some("Hi!")

val failedGreet: OptionT[Future,String] = OptionT.none
					

Nested


  final case class Nested[F[_], G[_], A](value: F[G[A]])
					

NonEmptyList


  final case class NonEmptyList[+A](head: A, tail: List[A]) {
    ???
  }
					

  def average(xs: List[Int]): Double = {
    xs.sum / xs.length.toDouble
  }
					

  average(List.empty)    // Exception
					

  def average(xs: List[Int]): Option[Double] =
    if (xs.nonEmpty)
      Some(xs.sum / xs.length.toDouble)
    else None
					

Можно, но не красиво
и возвращается Option


  import cats.data.NonEmptyList

  def average(xs: NonEmptyList[Int]): Double = {
    xs.reduceLeft(_+_) / xs.length.toDouble
  }
					

  def one[A](head: A): NonEmptyList[A]

  def of[A](head: A, tail: A*): NonEmptyList[A]

  def ofInitLast[A](init: List[A], last: A): NonEmptyList[A]

  def fromList[A](l: List[A]): Option[NonEmptyList[A]]
  def fromListUnsafe[A](l: List[A]): NonEmptyList[A]
					

  NonEmptyList.one(42)

  NonEmptyList.of(1)
  NonEmptyList.of(1, 2, 3, 4)

  NonEmptyList.ofInitLast(List(), 4)
  NonEmptyList.ofInitLast(List(1,2,3), 4)

  NonEmptyList.fromList(List())      // None
  NonEmptyList.fromList(List(1,2,3))
					

  import cats.syntax.list._

  List(1,2,3).toNel    // Some(NonEmptyList(1, List(2, 3)))
					

OneAnd


  import cats.data.OneAnd      //OneAnd[F[_],A]

  type NonEmptyList[A]   = OneAnd[List, A]
  type NonEmptyStream[A] = OneAnd[Stream, A]
					

Ior

Отношение ”inclusive-or” между двумя типами

PS: Either ~ “exclusive-or”


import cats.data._

val right = Ior.right[String, Int](3)     // Right(3)
val left = Ior.left[String, Int]("Error") // Left("Error")
val both = Ior.both("Warning", 3)         // Both("Warning", 3)
					

import cats.implicits._

val right: Ior[Nothing, Int]   = 3.rightIor    // Right(3)
val left: Ior[String, Nothing] = "Err".leftIor // Left("Err")
					

IorT

IorT[F[_], A, B] ~ F[Ior[A, B]]

EitherT

EitherT[F[_], A, B] ~ F[Either[A, B]]

Chain

Коллекция с константным временем
добавления в начало и конец.

sealed abstract class Chain[+A]

case object Empty extends Chain[Nothing]
case class Singleton[A](a: A) extends Chain[A]
case class Append[A](left: Chain[A],
                     right: Chain[A]) extends Chain[A]
case class Wrap[A](seq: Seq[A]) extends Chain[A]
					

NonEmptyChain

Аналогично NonEmptyList

Validated


sealed abstract class Validated[+E, +A]

final case class Valid[+A](a: A) extends Validated[Nothing, A]
final case class Invalid[+E](e: E) extends Validated[E, Nothing]
					

Пример:
валидация заполнения полей web-формы


  final case class RegistrationData(name: String,
                                    pass: String,
                                    age: Int)
					

  sealed trait ValidationError { def err: String }

  case object NameError extends ValidationError { val err = ??? }
  case object PassError extends ValidationError { val err = ??? }
  case object AgeError  extends ValidationError { val err = ??? }
					

def validateName(name: String): Either[ValidationError, String] =
  Either.cond(???, name, NameError)

def validatePass(pass: String): Either[ValidationError, String] =
  Either.cond(???, pass, PassError)

def validateAge(age: Int): Either[ValidationError, Int] =
  Either.cond(???, age, AgeError)
					

def validateForm(name: String, pass: String, age: Int) =
  for {
    validName <- validateName(name)
    validPass <- validatePass(pass)
    validAge  <- validateAge(age)
  } yield RegistrationData(validName, validPass, validAge)

// Either[ValidationError, RegistrationData]
					

Проблема:

вернём только первую ошибку

Решение:

Validated

Первая попытка

import cats.data._
import cats.data.Validated._
import cats.implicits._

def validateForm(name: String, pass: String, age: Int) =
  for {
    validName <- validateName(name).toValidated
    validPass <- validatePass(pass).toValidated
    validAge  <- validateAge(age).toValidated
  } yield RegistrationData(validName, validPass, validAge)

// Validated[ValidationError, RegistrationData]
					

Не скомпилируется


// error: value flatMap is not a member of
//        cats.data.Validated[ValidationError,String]
//
//     validName <- validateName(name).toValidated
//                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
					

Validated - не монада, но зато аппликатив

Вторая попытка


type ValidatedNec[+E, +A] = Validated[NonEmptyChain[E], A]
					

class ValidatedSugar[A](a: A) extends AnyVal {

  def validNec[B]: ValidatedNec[B, A] =
                                  Validated.Valid(a)

  def invalidNec[B]: ValidatedNec[A, B] =
                                  Validated.invalidNec(a)

}

type ValidationResult[A] = ValidatedNec[ValidationError, A]
// type ValidationResult[A] =
//              Validated[NonEmptyChain[ValidationError], A]
					

def validateName(name: String): ValidationResult[String] =
        if(???) name.validNec
        else    NameError.invalidNec

def validatePass(pass: String): ValidationResult[String] =
        if(???) pass.validNec
        else    PassError.invalidNec

def validateAge(age: Int): ValidationResult[Int] =
        if(???) age.validNec
        else    AgeError.invalidNec
					

def validateForm(name: String, pass: String, age: Int) =
  (
    validateName(name),
    validatePass(pass),
    validateAge(age)
  ).mapN(RegistrationData)

// ValidationResult[RegistrationData]
					

Что в итоге?

sealed trait ValidationError { def err: String }
type ValidationResult[RegistrationData] =
       Validated[NonEmptyChain[ValidationError], RegistrationData]
  • Удобный тип для выражения результата
  • Гарантирован непустой список ошибок
  • Собраны все ошибки

Библиотека Circe

* Название может произноситься как "SUR-see"
или на греческий манер "KEER-kee"

Переводится как Церцея.
В честь волшебницы из древнегреческой мифологии,
которая помогала Аргонавтам.

Особенности библиотеки:

  • Форк библиотеки Argonaut (основана на Scalaz)
  • Ядро Circe зависит от Cats
  • Json-парсер зависит от Jawn
  • Generic зависит от Shapeless

sealed abstract class Json
private[circe] final case object JNull
private[circe] final case class JBoolean(value: Boolean)
private[circe] final case class JNumber(value: JsonNumber)
private[circe] final case class JString(value: String)
private[circe] final case class JArray(value: Vector[Json])
private[circe] final case class JObject(value: JsonObject)
sealed abstract class Json {
  def isNull: Boolean
  def isBoolean: Boolean
  def isNumber: Boolean
  def isString: Boolean
  def isArray: Boolean
  def isObject: Boolean
}
sealed abstract class Json {
  def asNull: Option[Unit]
  def asBoolean: Option[Boolean]
  def asNumber: Option[JsonNumber]
  def asString: Option[String]
  def asArray: Option[Vector[Json]]
  def asObject: Option[JsonObject]
}
import io.circe._, io.circe.parser._

val json: String =
  """{
       "id": "c730433b-082c-4984-9d66-855c243266f0",
       "name": "Foo",
       "counts": [1, 2, 3],
       "values": {
         "bar": true,
         "baz": 100.001,
         "qux": ["a", "b"]
       }
     }"""

val doc: Json = parse(json).getOrElse(Json.Null)
import io.circe._
val json = Json.obj(
 ("id",Json.fromString("c730433b-082c-4984-9d66-855c243266f0")),
 ("name", Json.fromString("Foo")),
 ("counts", Json.fromValues(Seq[Json](
      Json.fromInt(1), Json.fromInt(2), Json.fromInt(3)
 ))),
 ("values", Json.obj(
      ("bar", Json.fromBoolean(true)),
      ("baz", Json.fromDouble(100.01).getOrElse(0)),
      ("qux", Json.fromValues(Seq[Json](
          Json.fromString("a"), Json.fromString("b")
      ))
 ))
))
json.noSpaces
json.spaces2SortKeys

def parse(input: String): Either[ParsingFailure, Json]
					

final case class ParsingFailure(message: String,
                                underlying: Throwable)
                        extends Error {

  final override def getMessage: String = message
}
val json: String =
  """{
       "id": "c730433b-082c-4984-9d66-855c243266f0",
       "name": "Foo",
       "counts": [1, 2, 3],
       "values": []
     }"""
val doc = parse(json)
// Left(io.circe.ParsingFailure:
//     expected } or , got '"value...' (line 6, column 13)
// )

HCursor


 val doc: Json = parse(json).getOrElse(Json.Null)

 val cursor: HCursor = doc.hcursor
					

 cursor.top       // перейти к "корню"
 cursor.up        // перейти к "родителю"

 cursor.field("myField")      // перейти к "брату" с ключом
 cursor.downField("myField")  // перейти к "потомку" с ключом

 cursor.left      //только для array
 cursor.right     //только для array
					

 def delete: ACursor  // возвращает "родителя"

 def withFocus(f: Json => Json): ACursor

 def withFocusM[F[_]](f: Json => F[Json])
                     (implicit F: Applicative[F]): F[ACursor]
					

cursor.value    // получить значение из текущей позиции как Json
cursor.as[Double]  // получить значение как число

cursor.get[Double]("fieldName")     // получить значение из поля
cursor.downField("fieldName").as[Double]           // тоже самое
					

def as[A](implicit d: Decoder[A]): Decoder.Result[A]

def get[A](k: String)(implicit d: Decoder[A]): Decoder.Result[A]
					

 type Result[A] = Either[DecodingFailure, A]
					

 type AccumulatingResult[A] = ValidatedNel[DecodingFailure, A]
					

Encoding & Decoding


 trait Encoder[A] {
   def apply(a: A): Json
   ...
 }
					

 trait Decoder[A] {
   def apply(c: HCursor): Result[A]
   def decodeAccumulating(c: HCursor): AccumulatingResult[A]

   def decodeJson(j: Json): Result[A]
   ...
 }
					

 trait Encoder[A] {
   ...
   def contramap[B](f: B => A): Encoder[B]
   def mapJson(f: Json => Json): Encoder[A]
 }
					

 trait Decoder[A] {
   ...
   def map[B](f: A => B): Decoder[B]
   def flatMap[B](f: A => Decoder[B]): Decoder[B]
 }
					

Encoder & Decoder
instances

  • для всех стандартных типов Int, String, Long ...
  • для Option[A], List[A], ... при наличии инстанса для A

 trait Codec[A] extends Decoder[A] with Encoder[A]
					

 implicit class EncoderOps[A](val v: A) extends AnyVal {
   def asJson(implicit encoder: Encoder[A]): Json = encoder(v)
 }
					

 import io.circe.syntax._
 val intsJson = List(1, 2, 3).asJson
 // EncoderOps(List(1, 2, 3)).asJson(encodingList(encodeInt))
					

Custom encoder


 import io.circe.Encoder

 case class Foo(a: String, b: Int)

 implicit val encoderFoo: Encoder[Foo] = new Encoder[Foo] {
    def apply(f: Foo): Json = Json.obj(
                           ("foo", Json.fromString(f.a)),
                           ("bar", Json.fromInt(f.b))
    )
 }
					

Custom encoder
(shorter)


 import io.circe.Encoder

 case class Foo(a: String, b: Int)

 implicit val encoderFoo: Encoder[Foo] = (f: Foo) =>
   Json.obj(
     ("a", Json.fromString(f.a)),
     ("b", Json.fromInt(f.b))
   )
					

Semi-automatic


 import io.circe.Encoder

 case class Foo(a: String, b: Int)

 implicit val encoderFoo: Encoder[Foo] =
      Encoder.forProduct2("foo", "bar")(f => (f.a, f.b))
					

Automatic


 import io.circe.Encoder

 case class Foo(a: String, b: Int)

 implicit val encoderFoo: Encoder[Foo] = deriveEncoder
					

Custom decoder
(monadic)


 import io.circe.{Decoder, HCursor}

 case class Foo(a: String, b: Int)

 implicit val decoderFoo: Decoder[Foo] = new Decoder[Foo] {
   def apply(c: HCursor): Decoder.Result[Foo] =
     for {
       a <- c.get[String]("a")
       b <- c.get[Int]("b")
     } yield Foo(a, b)
 }
					

Custom decoder
(monadic, shorter)


 import io.circe.{Decoder, HCursor}

 case class Foo(a: String, b: Int)

 implicit val decoderFoo: Decoder[Foo] = (c: HCursor) =>
   for {
     a <- c.get[String]("a")
     b <- c.get[Int]("b")
   } yield Foo(a, b)
					

Custom decoder
(applicative)


 import io.circe.Decoder
 import cats.syntax.apply._

 case class Foo(a: String, b: Int)

 implicit val decoderFoo: Decoder[Foo] = (
     Decoder.instance(_.downField("a").as[String]),
     Decoder.instance(_.downField("b").as[Int])
   ).mapN(Foo.apply)
					

 decodeAccumulating[Foo]( """{"a":55, "b":"foo"}""" )

 // res: ValidatedNel[circe.Error, Foo] =
 //    Invalid( NonEmptyList(
 //          DecodingFailure(String, List(DownField(a))),
 //          DecodingFailure(Int, List(DownField(b)))
 //    ))
					

 sealed trait Event

 case class Foo(i: Int) extends Event
 case class Bar(ls: List[String]) extends Event
					

 implicit val encodeFoo = Encoder.forProduct1("i")(f => f.i)
 implicit val encodeBar = Encoder.forProduct1("ls")(f => f.ls)
					

 implicit val encodeEvent: Encoder[Event] = Encoder.instance {
   case foo: Foo => foo.asJson
   case bar: Bar => bar.asJson
 }
					

 implicit val decodeEvent: Decoder[Event] =
    List[Decoder[Event]](
      Decoder[Foo].widen,
      Decoder[Bar].widen
    ).reduceLeft(_ or _)
					

 val fooFirst = List[Decoder[Event]](
    Decoder[Foo].widen, Decoder[Bar].widen
 ).reduceLeft(_ or _)

 val barFirst = List[Decoder[Event]](
    Decoder[Bar].widen, Decoder[Foo].widen
 ).reduceLeft(_ or _)

 val input = """{"ls": ["a", "b"], "i": 1000}"""

 parser.decode[Event](input)(fooFirst) // Right(Foo(1000))
 parser.decode[Event](input)(barFirst) // Right(Bar(List(a, b)))
					

 implicit val encodeFoo: Encoder[Foo] =
          Encoder.forProduct2("i", "type")(f => (f.i, "Foo"))

 implicit val encodeBar: Encoder[Bar] =
          Encoder.forProduct2("s", "type")(f => (f.ls, "Bar"))
					

 implicit val genDevConfig: Configuration =
              Configuration.default.withDiscriminator("type")

 val inFoo = """{"ls": ["a", "b"], "i": 1000, "type": "Foo"}"""
 val inBar = """{"ls": ["a", "b"], "i": 1000, "type": "Bar"}"""

 parser.decode[Event](inFoo)  // Right(Foo(1000))
 parser.decode[Event](inBar)  // Right(Bar(List(a, b)))
					

Напоминаю: