Страничка курса: https://maxcom.github.io/scala-course-2020/
Дополнительный блок параметров функции
def getUser(id: Int)(implicit session: DatabaseSession): User
значения которого можно явно не задавать
Без implicit значения пришлось бы
Значение должно существовать в области видимости
DB localTx { implicit session ⇒
getUser(10)
}
Где это применяется? Для передачи контекста
implicit значения ищутся в
Неоднозначности в поиске - ошибка компиляции
Преобразование одного класса в другой
implicit class RichString(str: String)
автоматическое преобразование там где необходимо
"Pimp my library" - прием из динамических языков, например Ruby
В Ruby код может "улучшить" любой класс, даже стандартную библиотеку
Это называется "Monkey patch", и является опасной техникой
Опасности:
Implicit class - безопасная замена этой техники
Пример - добавляем метод в String
implicit class RichString(str: String) {
def letters = str.count(_.isLetter)
}
"scala 2.12".letters // = 5
В отличие от monkey patching
Более правильная версия примера:
implicit class RichString(val str: String) extends AnyVal {
def letters = str.count(_.isLetter)
}
класс существует только в compile time
Технику применяют для "красоты", например:
import scala.concurrent.duration._
val timeout: Duration = 5 minutes
// замена Duration(5, TimeUnit.MINUTES)
еще пример - specs2
Typeclass - шаблон проектирования, построенный на использовании implicit преобразований.
Рассмотрим реализацию .sum для List[Int]:
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
Как её обобщить на наш собственный тип значений?
case class BusValue(amount: Int)
Нужно два элемента:
Проблема с ООП:
// упрощенная реализация на основе исходников Cats
def combineAll[A](fa: Seq[A])(implicit m: Monoid[A]): A =
fa.foldLeft(m.empty) { (acc, a) =>
m.combine(acc, a)
}
implicit val busMonoid: Monoid[BusValue] =
new Monoid[BusValue] {
override val empty = BusValue(0)
override def combine(x: BusValue, y: BusValue): BusValue =
BusValue(x.amount + y.amount)
}
// ассоциативность
combine(x, combine(y, z)) = combine(combine(x, y), z)
combine(x, empty) = combine(empty, x) = x
Примеры:
Примеры:
import cats.implicits._
// pimp my library
Vector(BusValue(1), BusValue(9000)).combineAll
Для Seq не работает!
BusValue(1) |+| BusValue(9000)
val one = Map("first" -> BusValue(1), "second" -> BusValue(2))
val two = Map("second" -> BusValue(3), "third" -> BusValue(3))
one |+| two
// Map("first" -> BusValue(1),
// "second" -> BusValue(5),
// "third" -> BusValue(3))
// неявное преобразование
implicit def monoidForMap[K, V]
(implicit ev: Monoid[V]): Monoid[Map[K, V]] =
new MapMonoid[K, V]
Немного другой синтаксис:
// context bounds
implicit def monoidForMap[K, V: Semigroup]: Monoid[Map[K, V]] =
new MapMonoid[K, V]
Тайплассы можно объявить для более интересных конструкций.
Функтор - абстракция контейнера с операцией map
источник: Functors and Applicatives
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
Требования к реализации функтора:
fa.map(f).map(g) = fa.map(v => g(f(v)))
fa.map(identity) = fa
После перерыва рассмотрим некоторое практическое применение.
Текстовый формат обмена данными.
Пришел на замену XML, который оказался слишком сложным и слишком медленным.
Использует синтаксис объявления данных JavaScript
{
"firstName": "Иван",
"lastName": "Иванов",
"address": {
"streetAddress": "Московское ш., 101, кв.101",
"city": "Ленинград",
"postalCode": 101101
},
"phoneNumbers": [
"812 123-1234",
"916 123-4567"
]
}
Формально описан в rfc8259
Весь стандарт - 16 страниц текста, включая оглавления, ссылки и т.п.
Json легко читается (если отформатировать)
Библиотеки для работы с ним есть для большинства языков программирования.
Формат JSON был разработан
Дугласом Крокфордом.
Видео: The JSON Saga
Используем библиотеку Play JSON.
Часть play-framework; можно использовать отдельно.
Документация:
Подключаем библиотеку в проект
libraryDependencies +=
"com.typesafe.play" %% "play-json" % "2.8.1"
JsValue - алгебраический тип
Запись JSON
import play.api.libs.json._
val v: JsValue = JsObject(Seq(
"id" -> JsNumber(1),
"name" -> JsString("Vasya"),
"marks" -> JsArray(Seq(JsNumber(2), JsNumber(3)))
))
Json.stringify(v) // компактный формат
Json.prettyPrint(v) // с форматированием
Большие структуры так выводить не удобно.
Нужно разбить создание JSON на компоненты и комбинировать их.
Какие есть варианты?
Pattern matching?
Не походит - мало операций,
много вариаций данных.
Объектный полиморфизм? Возможно
trait Writable {
def toJson: JsValue
}
Однако:
Reflection? Да, это "Java-way".
В runtime запрашиваем структуру объекта средствами JVM и выводим её.
Однако:
Решение - тайпкласс, как еще один вариант полиморфизма.
// упрощенный код
trait Writes[A] {
def writes(o: A): JsValue
}
использование
def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue =
tjs.writes(o)
Для примитивных типов готовая реализация
Json.toJson("some string")
реализация
// из play-json
implicit object StringWrites extends Writes[String] {
def writes(o: String) = JsString(o)
}
Для Seq нужно преобразование
implicit def traversableWrites[A](implicit w: Writes[A]):
Writes[Traversable[A]] = {
Writes[Traversable[A]] { as =>
val builder = mutable.ArrayBuilder.make[JsValue]()
as.foreach { a =>
builder += w.writes(a)
}
JsArray(builder.result())
}
}
Json.toJson(Seq("some string", "second string"))
Выводим case-класс
import play.api.libs.json._
case class User(id: UUID, name: String, title: Option[String])
object User { // объявляем в companion object
// OWrites вместо Writes
// используем Writes.apply
implicit val writes: OWrites[User] = OWrites { user ⇒
Json.obj( // сам использует Writes для значений
"id" -> user.id,
"name" -> user.name,
"title" -> user.title
)
}
}
Для простых случаев есть макрос
object User {
implicit val writes: OWrites[User] = Json.writes[User]
}
Writes - не всегда простое преобразование
// writes зависит от языка пользователя
implicit def writes(implicit lang: Lang): OWrites[User]
Такой полиморфизм работает в компиляторе, типы должны быть точно известны.
// один Writes для всего ADT
object Expr {
implicit val writes: Writes[Expr] = Writes {
case Number(value) ⇒
JsNumber(value)
case Plus(lhs, rhs) ⇒
Json.obj("op" -> "plus", "left" -> lhs, "right" -> rhs)
case Minus(lhs, rhs) ⇒
Json.obj("op" -> "minus", "left" -> lhs, "right" -> rhs)
}
}
Итого мы получили:
Тайпкласс Reads для чтения
trait Reads[A] {
def reads(json: JsValue): JsResult[A]
}
JsResult - алгебраический тип
val v: JsValue = Json.arr("str1", "str2")
v.validate[Seq[String]]
// JsSuccess(List("str1", "str2"))
v.as[Seq[String]] // значение или Exception
// List("str1", "str2")
Case class: плохой вариант
case class User(id: UUID, name: String, title: Option[String])
object User {
implicit val reads: Reads[User] = Reads[User] { json ⇒
JsSuccess(User(
id = (json \ "id").as[UUID],
name = (json \ "name").as[String],
title = (json \ "title").asOpt[String]
))
}
}
Почему плохой?
JsResult - монада
implicit val reads: Reads[User] = Reads[User] { json ⇒
for {
id <- (json \ "id").validate[UUID]
name <- (json \ "name").validate[String]
title <- (json \ "title").validateOpt[String]
} yield {
User(id, name, title)
}
}
Исключений больше нет,
но диагностика еще хуже
Проблема: при операции поиска теряется путь
Вместо работы над значениями будем конструировать функцию
val v: JsValue = ???
val r: JsLookupResult = v \ "id"
// результат поиска
val path: JsPath = __ \ "id"
// путь, не привязанный к значению
Строим Reads
// метод JsPath
def read[T](implicit r: Reads[T]): Reads[T]
Использование
val idReads: Reads[UUID] = (__ \ "id").read[UUID]
при ошибке путь будет добавлен в JsError
Reads - тоже монада
implicit val reads: Reads[User] =
for {
id <- (__ \ "id").read[UUID]
name <- (__ \ "name").read[String]
title <- (__ \ "title").readNullable[String]
} yield {
User(id, name, title)
}
Чего мы получили:
Что не так?
Монада - абстракция цепочки вычислений.
У нас же все вычисления независимые,
кроме шага сборки.
Еще плохо - ошибка только первая;
надо выдать все ошибки
Монада тут - плохая абстракция.
На помощь приходит
аппликативный функтор
Это функтор с дополнительными операциями
def pure[A](a: A): F[A]
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
Функция ap:
источник: Functors and Applicatives
Вместо ap можно определить product - комбинирует два функтора в функтор от пары
def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
Реализации функторов должны следовать нескольким законам.
Подробнее см. The underrated applicative functor и Applicative (Cats).
Reads - не только монада, но и аппликатив.
При сборке ошибки "складываются".
Синтаксис:
import play.api.libs.json._
import play.api.libs.functional.syntax._
val combined: Reads[(UUID, String, Option[String])] = (
(__ \ "id").read[UUID] and
(__ \ "name").read[String] and
(__ \ "title").readNullable[String]).tupled
val userReads: Reads[User] =
combined.map((User.apply _).tupled)
Более удобный синтаксис:
val userReads: Reads[User] = (
(__ \ "id").read[UUID] and
(__ \ "name").read[String] and
(__ \ "title").readNullable[String])(User.apply _)
Итого что мы получили:
Полезные инструменты:
Используем готовые Reads
Reads.of[Int].map(Number.apply)
Валидация значений:
import play.api.libs.json.{JsonValidationError ⇒ JsErr}
Reads.of[Int].filter(JsErr("must be positive"))(_ > 0)
Разбор алгебраического типа
import play.api.libs.json.{JsonValidationError ⇒ JsErr}
sealed trait Numbers
object One extends Numbers
object Two extends Numbers
object Three extends Numbers
val reads: Reads[Numbers] =
Reads.of[String].collect(JsErr("bad number")) {
case "one" ⇒ One
case "two" ⇒ Two
case "three" ⇒ Three
}
Создаем Reads для vk.com API
newsfeed.get
документация на newsfeed.get
users.get
документация на users.get
Код добавляем в наш большой проект.
Практика по использованию play-json.
Напоминаю: