Страничка курса: https://maxcom.github.io/scala-course-2022/
Последняя лекция про язык Scala
"Неявные" значения и преобразования
Дополнительный блок параметров функции
def getUser(id: Int)(implicit session: DatabaseSession): User
значения которого можно явно не задавать
Без implicit значения пришлось бы
Значение должно существовать в области видимости
DB localTx { implicit session =>
getUser(10)
}
при необходимости можно передать явно
// implicit тут не нужен, но не мешает
DB localTx { implicit session =>
getUser(10)(session)
}
Где это применяется?
Для передачи контекста
for может использовать flatMap/map с
implicit параметром
implicit значения ищутся в
implicit контекст обычно в области видимости
Неоднозначности при поиске на одном уровне - ошибка компиляции
Преобразование одного класса в другой
implicit class RichString(str: String)
автоматическое преобразование там, где необходимо
"Pimp my library" - прием из динамических языков, например Ruby
В Ruby код может "улучшить" любой класс, даже стандартную библиотеку
"Monkey patching" - весьма опасная техника
Опасности:
Implicit class - безопасная замена этой техники
Пример - добавляем метод в String
// дальше будет более эффективная реализация
// название класса значения не имеет
implicit class RichString(str: String) {
def letters = str.count(_.isLetter)
}
"scala 2.12".letters // = 5
Более правильная версия примера:
implicit class RichString(val str: String) extends AnyVal {
def letters = str.count(_.isLetter)
}
класс существует только в compile time
AnyVal можно использовать для оптимизации, но осторожно.
Есть другие варианты, например tagged types
Технику применяют для "красоты", например:
import scala.concurrent.duration._
val timeout: Duration = 5.minutes
// замена Duration(5, TimeUnit.MINUTES)
еще пример - DSL в scalatest/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)
}
Monoid есть в Cats и др. библиотеках,
можете сделать свой
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)
}
monoid может объявить для любого типа,
// ассоциативность
combine(x, combine(y, z)) = combine(combine(x, y), z)
combine(x, empty) = combine(empty, x) = x
коммутативность не требуется!
Примеры:
Примеры:
import cats.implicits._
implicit val busMonoid: Monoid[BusValue] = ???
// pimp my library
Vector(BusValue(1), BusValue(9000)).combineAll
для Seq нужна новая версия Cats
NonEmptyList - не моноид, а полугруппа (Semigroup)
Обладает ассоциативным методом combine
BusValue(1) |+| BusValue(9000)
// Monoid - это наследник Semigroup,
// так что работает и для моноидов
val one = Map("first" -> BusValue(1), "second" -> BusValue(2))
val two = Map("second" -> BusValue(3), "third" -> BusValue(3))
one ++ two // "second" первого заменен на второй
one |+| two // "second" складывается
// Map("first" -> BusValue(1),
// "second" -> BusValue(5),
// "third" -> BusValue(3))
Как это работает?
Еще один механизм - неявное преобразование
// неявное преобразование моноида для значения
// в моноид для Map
implicit def monoidForMap[K, V]
(implicit ev: Monoid[V]): Monoid[Map[K, V]] =
new MapMonoid[K, V]
Если значение не Monoid, то и Map нет.
Вывод преобразований - рекурсивный
Еще раз посмотрим на этот код
// неявное преобразование моноида для значения
// в моноид для Map
implicit def monoidForMap[K, V]
(implicit ev: Monoid[V]): Monoid[Map[K, V]] =
new MapMonoid[K, V]
ev не используем, а только передаем
// context bounds
implicit def monoidForMap[K, V : Semigroup]: Monoid[Map[K, V]] =
new MapMonoid[K, V]
Тайпклассы можно объявить для
более интересных конструкций.
6-я лекция: Cats и Circe
далее работа с тайпклассами на примере play-json
Что же такое тайпклассы?
Способ реализации некой абстрактной функциональности вне данных, с которыми она связана.
Текстовый формат обмена данными.
Пришел на замену 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
"The Software shall be used for Good, not Evil."
Используем библиотеку Play JSON.
Часть play-framework; можно использовать отдельно.
Circe, другая библиотека, будет на лекции про Cats
Документация:
Подключаем библиотеку в проект
libraryDependencies +=
"com.typesafe.play" %% "play-json" % "2.8.2"
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) // с форматированием
pretty print используем только для отладки
Большие структуры так выводить не удобно.
Хотим выводить JSON для наших case-классов, коллекций и т.п.
Какие есть варианты?
Pattern matching?
Не походит - мало операций,
много вариаций данных.
Объектный полиморфизм? Возможно
trait Writable {
def toJson: JsValue
}
Reflection? Да, это "Java-way".
В runtime запрашиваем структуру объекта средствами JVM и выводим её.
Однако:
Решение - тайпкласс, как еще один вариант полиморфизма.
// упрощенный код
trait Writes[A] {
def writes(o: A): JsValue
}
использование
object Json {
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 нужно преобразование
// реализация из play-json
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"))
// ["some string", "second string"]
Типобезопасно - работает только если для элементов есть Writes
Выводим case-класс
import play.api.libs.json._
case class User(id: UUID, name: String, title: Option[String])
object User { // объявляем в companion object
// OWrites вместо Writes (возвращает JsObject)
// используем 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)
}
}
(макрос из play умеет простые ADT)
Итого мы получили:
Тайпкласс Reads для чтения
trait Reads[A] {
def reads(json: JsValue): JsResult[A]
}
есть макрос Json.writes[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]
))
}
}
Exception вместо JsError;
диагностика только по номерам строк
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)
}
JsError, никаких исключений.Что не так?
Монада - абстракция цепочки вычислений.
У нас же все вычисления независимые,
кроме шага сборки.
Еще плохо - ошибка только первая;
надо выдать все ошибки.
Монада тут - плохая абстракция.
На помощь приходит
аппликативный функтор.
О нем поговорим на лекции про Cats.
Reads имеет операцию сборки: пару Reads можно собрать в 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)
// аналог
implicitly[Reads[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
case object One extends Numbers
case object Two extends Numbers
case object Three extends Numbers
val reads: Reads[Numbers] =
Reads.of[String].collect(JsErr("bad number")) {
case "one" ⇒ One
case "two" ⇒ Two
case "three" ⇒ Three
}
Напоминаю: