Scala with Cats – это свободно доступная книга, написанная Noel Welsh и Dave Gurnell из Underscore. Охватывает все основы использования библиотеки cats.
Книгу рекомендую читать с выполнением заданий, без этого лично мне полного понимания достичь не удалось.
Ниже краткий насколько возможно конспект по Type Class'ам из книги.
Show
- вывести на печать любой тип
- syntax:
.show
Eq
- типобезопасное сравнение
- syntax:
a === ba =!= 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.asRighta.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.tella.writer(seq)
Reader
- построение цепочки операций на основе входных параметров
- ex: DI, encoders
- операции:
.flatMap– позволяет объединить несколько reader от одного входного значения.map
State
- передача дополнительного состояния в вычисления
- моделирование мутабельного состояния в pure functional подходе
State[S, A]этоS => (S, A)- операции
val (state, res) = a.run(v).valuea.runS(v).valuea.runA(v).value
- стандартные преобразования
State.getState.setState.pureState.inspectState.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
- если нужно часто определять alias для типов в стеках монад, поможет scala compiler plugin king projector. с ним можно писать так:
- использование для реализации:
- многие монады реализованы через трансформеры
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*
- аналоги стандартных коллекций, но имеющие строго один или более элементов
NonEmptyListNonEmptyVector
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