Страничка курса: https://maxcom.github.io/scala-course-2022/
Абстрактные котики, функциональный подход и теория категорий
* Название является шутливым сокращением слова "категория"
Архитектурные строительные блоки, которые появляются в программах снова и снова.
т.е. являются для ФП эквивалентом шаблонов проектирования ООП
некоторые из них находятся в собственных репозиториях
Полезные ссылки:
Рассмотрим наиболее полезные классы
Определена операция
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
Тип 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 и других коллекций
Полугруппа с "нулём"
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
Большинство полугрупп легко расширяются до моноида.
""
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)))
Моноид над конструкторами
типов с одним аргументом
В частности - над коллекциями
Метод 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)
Полугруппа над конструкторами
типов с одним аргументом
Добавляет операцию 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)
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
}
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)))
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)
Используется в конструкции
for { ... } yield { ... }
Об этом рассказывали в прошлых лекциях
На самом деле иерархия типов в cats гораздо сложнее
Изучайте документацию и исходники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]
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
final case class Nested[F[_], G[_], A](value: F[G[A]])
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)))
import cats.data.OneAnd //OneAnd[F[_],A]
type NonEmptyList[A] = OneAnd[List, A]
type NonEmptyStream[A] = OneAnd[Stream, A]
Отношение ”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[F[_], A, B] ~ F[Ior[A, B]]
EitherT[F[_], A, B] ~ F[Either[A, B]]
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]
Аналогично NonEmptyList
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]
* Название может произноситься как "SUR-see"
или на греческий манер "KEER-kee"
Переводится как Церцея.
В честь волшебницы из древнегреческой мифологии,
которая помогала Аргонавтам.
Особенности библиотеки:
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]
}
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))
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))
)
}
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))
)
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))
import io.circe.Encoder
case class Foo(a: String, b: Int)
implicit val encoderFoo: Encoder[Foo] = deriveEncoder
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)
}
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)
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)))
Напоминаю: