Конспект по Scala with Cats

Scala with Cats – это свободно доступная книга, написанная Noel Welsh и Dave Gurnell из Underscore. Охватывает все основы использования библиотеки cats.

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

Ниже краткий насколько возможно конспект по Type Class'ам из книги.

Show

  • вывести на печать любой тип
  • syntax: .show

Eq

  • типобезопасное сравнение
  • syntax:
    • a === b
    • a =!= b

Monoid

  • операции
    • .combine(a1, a2)
    • .empty[A]
  • законы:
    • combine ассоциативный
    • empty должен быть нейтральным элементом (identity element)

Semigroup

  • объединение двух значений
  • только combine из Monoid
  • там где требуется Semigroup, всегда можно использовать Monoid
  • нужен так как не всегда существует нейтральный элемент, пример – NonEmptyList
  • syntax: a |+| b

Functor

  • что-то у чего есть map
  • не стоит думать об этом только, как о проходе по списку, это может быть и работа с внутренним состоянием "контейнера" (в контексте этого "контейнера"), без извлечения значения из "контейнера".
  • операции:
    • .lift(f)
  • syntax:
    • f.map(g)

Contravariant (Functor)

  • из F[B] имея A => B получить F[A]
  • syntax: .contramap(f)

Invariant

  • имея A => B, B => A и Monoid[B] получить Monoid[A]
  • syntax: Monoid[B].imap(f)(g)

Monad

  • 1000 разных подходов к определению, в книге такое: монада – механизм для последовательных вычислений
  • операции
    • .pure(a) – создание монадического контекста из сырого значения
    • .flatMap(f) – извлечение значения из контекста и создание следующего контекста в последовательности
  • любая монада – функтор, map легко построить из pure+flatMap
  • laws:
    • левоассоциативность: pure(a).flatMap(f) == f(a). тут важно помнить об эффектах. именно по этой причине Try не Monad, так как если "снять" с него контекст монады при обычном вызове получим эффект – исключение, а если не снять то получим Failure.
    • правоассоциативность: m.flatMap(pure) == m.
    • ассоциативность: m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))
  • syntax:
    • .pure[T] (из cats.syntax.applicative)
    • .map(f) (из cats.syntax.functor)
    • .flatMap(f) (из cats.syntax.flatMap)
    • можно использовать for comprehensions из scala
  • при определении своих монад
    • .flatMap
    • .pure
    • .tailRecM – оптимизация для вложенных .flatMap вызовов. метод можно делать @tailrec

Id

  • Identity Monad
  • позволяет использовать в коде, где ожидаются монады немонадные значения
  • мощный инструмент для тестов кода построенного на монадах
  • например
    • 123 : Id[Int]
    • "abc" : Id[String]

Either

  • обычный тип из scala
  • операции:
    • .orElse(Either)
    • .ensure(e)(predicate)
    • .recover(partialFunction)
    • .recoverWith(partialFunction)
    • .leftMap(f)
    • .bimap(f, g)
    • .swap
  • syntax:
    • right bias (flatMap/map/filter работающие на правой части Either, есть из коробки в scala 2.12+)
    • a.asRight
    • a.asLeft

MonadError

  • абстракция поверх Either для работы с ошибками
  • параметризирован типом монады и типом ошибки
  • операции
    • .raiseError(e)
    • .handleError(a)(f)
    • .ensure(a)(e)(predicate)
  • syntax
    • .ensure(e)(predicate)

Eval

  • абстракция над выполнением
  • в частности: позволяет делать стекобезопасные рекурсивные вычисления
  • операции
    • .now(f)
    • .later(f)
    • .always(f)
    • .memoize
    • .defer(=> Eval)

Writer

  • сбор лога при вычислении
  • операции
    • val (log, result) = writer.run
    • .mapWritten(f)
    • .bimap(f, g)
    • .mapBoth( (log, res) => ...)
    • .reset
    • .swap
  • syntax:
    • a.pure[T] (требует Monoid[T] в implicit scope)
    • a.tell
    • a.writer(seq)

Reader

  • построение цепочки операций на основе входных параметров
  • ex: DI, encoders
  • операции:
    • .flatMap – позволяет объединить несколько reader от одного входного значения
    • .map

State

  • передача дополнительного состояния в вычисления
  • моделирование мутабельного состояния в pure functional подходе
  • State[S, A] это S => (S, A)
  • операции
    • val (state, res) = a.run(v).value
    • a.runS(v).value
    • a.runA(v).value
  • стандартные преобразования
    • State.get
    • State.set
    • State.pure
    • State.inspect
    • State.modify
  • синтаксис
    • for comprehension

Monad transformers

  • позволяют комбинировать монады
  • комбинация невозможна без знаний об одной из двух монад
  • например, EitherT позволяет комбинировать любую монаду с Either
  • из коробки: cats.data.{OptionT, EitherT, ReaderT, WriterT, StateT, IdT}
  • соглашение: трансформер определяет внутреннюю монаду, а первый его тип - внешнюю. например, OptionT[List, A] – создаст монаду для List[Option[A]]
  • syntax:
    • если нужно часто определять alias для типов в стеках монад, поможет scala compiler plugin king projector. с ним можно писать так: 123.pure[EitherT[Option, String, ?]]
    • .pure
    • .value
  • использование для реализации:
    • многие монады реализованы через трансформеры
    • type ReaderT[F[_], A, B] = Kleisli[F, A, B]
    • type Writer[W, A] = WriterT[Id, W, A]

Kleisli

  • объединение функций A => F[B], B => F[C] в A => F[C]

Semigroupal

  • объединяет два контекста
  • в литературе иногда называют Monoidal
  • операции:
    • .map2 ... .map22
    • .tuple2 ... .tuple22
    • .contramap2 ... .contramap22
    • .imap2 ... .imap22
  • syntax
    • (a, b, c, ...).tupled
    • (a, b, c ...).mapN
    • (Monoid[A], Monoid[B], ...).imapN(toF)(fromF)
  • Intellij Idea не понимает тип выражения после mapN, это недавно поправили
  • Иногда получаемый результат не сразу очевиден. Например Semigroupal от двух списков будет прямое произведение (каждый с каждым), а не zip списков.

Validated

  • аналог Either, но с накоплением всех ошибок
  • требует Semigroup для типа ошибки
  • операции
    • Validated.valid[E, A](a)
    • Validated.invalid[E, A](e)
    • Validated.catchOnly[Throwable](f)
    • Validated.catchNonFatal(f)
    • Validated.fromTry(a)
    • Validated.fromEither[E, A](a)
    • Validated.fromOption[E, A](a, e)
    • .map(f)
    • .leftMap(f)
    • .bimap(f, g)
    • .toEither
    • .withEither(f)
    • .withValidated(f)
    • .getOrElse(a)
    • .fold(f, g)
  • syntax
    • a.valid[E]
    • a.invalid[A]
    • .pure[Validated[F[E], A]]
    • .raiseError[Validated[F[E], A], E]
    • .tupled

Apply

  • Semigroupal + Functor
  • альтернативный к Semigroupal способ закодировать объединение контекстов
  • операции
    • .ap(f)(a)
    • .product(a, b)

Applicative

  • Apply + pure, который позволяет создать новый Applicative instance из сырого значения
  • Monad = Applicative + FlatMap
  • операции
    • .pure(a)

NonEmpty*

  • аналоги стандартных коллекций, но имеющие строго один или более элементов
  • NonEmptyList
  • NonEmptyVector

Foldable

  • абстракция для foldLeft / foldRight
  • операции
    • .foldLeft(a, i)(f)
    • .foldRight(a, i: Eval[B])(f): Eval[B] (stack safe)
    • привычные методы коллекций, поверх foldLeft: .find, .exists, .forall, .toList, .isEmpty, .nonEmpty ...
    • .combineAll(a) (aka .fold, требует Monoid)
    • .foldMap(a)(f) (требует Monoid)
    • .compose(other)
  • syntax:
    • все операции доступны как синтаксис
    • a.foldLeft(i)(f) и т. д.

Traverse

  • имея набор из F[A], получить F от набора А
  • например, List[Future[A]]Future[List[A]]
  • операции
    • .traverse(a)(f)
    • .sequence(a)
  • syntax:
    • a.traverse(f)
    • a.sequence