В предыдущем посте о производных в Scala я объяснил суть идеи и показал, как мы можем извлечь из нее пользу, используя производные, предоставляемые библиотеками. В этот раз копнем глубже и реализуем нашу собственную деривацию с помощью Magnolia.
Примечание: Этот пост предназначен для пользователей Scala среднего (intermediate) уровня. Если же вы еще не знакомы с данной темой, я рекомендую начать с введения для начинающих.
Резюме
Примечание: Вы можете пропустить данный раздел, если помните предыдущий пост, это просто краткое изложение.
В предыдущем посте мы работали с классом типа Show, который выглядит следующим образом:
trait Show[A] { def show(a: A): String }
Мы предоставили собственную реализацию класса типа для некоторых существующих типов и собственный кейс-класс Person.
object Show { given Show[String] = new Show[String] { def show(a: A): String } given Show[Int] = new Show[Int] { def show(a: Int): String = a.toString() } extension [A: Show](a: A) { def show: String = Show[A].show(a) } } val ShowPerson = new Show[Person] { def show(a: Person): String = s"Person(name = ${a.name.show}, surname = ${a.surname.show}, age = ${a.age.show})" }
Вскоре стало очевидно, что имеется шаблон для генерации этих реализаций show, и можно найти способ, чтобы заставить компилятор осуществить имплементацию. Мы воспользовались существующей реализацией из библиотеки kittens.
Magnolia
В этом посте мы изучим Magnolia и попробуем реализовать деривацию Show самостоятельно. Что такое Magnolia? Цитирую ее readme:
Magnolia — это универсальный макрос для автоматической материализации классов типов для типов данных, состоящих из типов продуктов (например, кейс-классов) и типов копродуктов (например, перечислений).
Звучит сложно? С практической точки зрения это означает, что мы собираемся реализовать что-то вроде:
object givens extends AutoDerivation[Show] { // generate Show instance for case classes override def join[T](caseClass: CaseClass[Show, T]): Show[T] = ??? // generate Show instance for sealed traits override def split[T](sealedTrait: SealedTrait[Show, T]): Show[T] = ??? }
extends AutoDerivation[Show] означает, что Magnolia должна сгенерировать экземпляры класса типа для show, предполагая, что мы предоставили join и split.
Генерирование Show для кейс-классов
Давайте начнем с реализации join, взглянув на то, что демонстрирует интерфейс CaseClass:
abstract class CaseClass[Typeclass[_], Type]( val typeInfo: TypeInfo, val isObject: Boolean, val isValueClass: Boolean, val parameters: IArray[CaseClass.Param[Typeclass, Type]], val annotations: IArray[Any], val inheritedAnnotations: IArray[Any] = IArray.empty[Any], val typeAnnotations: IArray[Any] )
При генерации Show для любого кейс-класса мы хотели бы выполнить следующие шаги:
-
Получить простое имя кейс-класса и запомнить его как name
-
Получить список параметров и сериализовать их в список
s"${parameter.name} = ${Show.show(parameter.value)}«. -
Объединить их в строку
Чтобы получить имя нашего кейс-класса, мы можем взглянуть на поле typeInfo, его тип определяется как
case class TypeInfo( owner: String, short: String, typeParams: Iterable[TypeInfo] )
Имя short — это то, что мы ищем. Давайте воспользуемся полученными сведениями и приступим к реализации join
override def join[T](caseClass: CaseClass[Show, T]): Show[T] = new Show[T] { def show(value: T) = { val name = caseClass.typeInfo.short val serializedParams = ??? s"$name($serializedParams)" } }
Мы прошли половину пути, теперь давайте разберемся с serializedParam. Необходимую информацию мы получим из поля parameters. Давайте взглянем на интерфейс CaseClass.Param
trait Param[Typeclass[_], Type]( val label: String, val index: Int, val repeated: Boolean, val annotations: IArray[Any], val typeAnnotations: IArray[Any] )
Первую часть, имя параметра, получить очень легко, она доступна в поле label. А как насчет значения параметра? Это не свойственный параметр для поля кейс-класса, поэтому его нет в конструкторе. Интерфейс Param предоставляет метод deref:
/** * Get the value of this param out of the supplied instance of the case class. * * @param value an instance of the case class * @return the value of this parameter in the case class */ def deref(param: Type): PType
Похоже, мы имеем именно то, что нам нужно для получения значения параметра, при условии, что мы можем предоставить экземпляр кейс-класса в качестве param. Давайте воспользуемся этим API, чтобы заполнить пробел в serializedParams:
override def join[T](caseClass: CaseClass[Show, T]): Show[T] = new Show[T] { def show(value: T) = { val name = caseClass.typeInfo.short val serializedParams = caseClass.parameters.map { parameter => s"${parameter.label} = ${parameter.typeclass.show(parameter.deref(value))}" }.mkString(", ") s"$name($serializedParams)" } }
Вот и все. Мы осуществили маппинг параметров в массив строк key = value с помощью
s"${parameter.label} = ${parameter.typeclass.show(parameter.deref(value))}"
и затем объединили их вместе с помощью mkString. Это очень похоже на план из трех пунктов, описанный выше.
Со split разобрались, перейдем к join.
Генерация Show для запечатанных трейтов и перечислений
Метод, который мы собираемся реализовать, имеет следующую сигнатуру:
override def split[T](sealedTrait: SealedTrait[Show, T]): Show[T] = ???
Давайте еще раз посмотрим, что мы можем выяснить, исследуя SealedTrait
/** * Represents a Sealed-Trait or a Scala 3 Enum. * * In the terminology of Algebraic Data Types (ADTs), sealed-traits/enums are termed * 'sum types'. */ case class SealedTrait[Typeclass[_], Type]( typeInfo: TypeInfo, subtypes: IArray[SealedTrait.Subtype[Typeclass, Type, _]], annotations: IArray[Any], typeAnnotations: IArray[Any], isEnum: Boolean, inheritedAnnotations: IArray[Any] )
Интересный подход
Видим здесь знакомые лица, ведь мы уже использовали typeInfo. Первое, что я сделал, изучая Magnolia, это реализовал split вот так:
override def split[T](sealedTrait: SealedTrait[Show, T]): Show[T] = sealedTrait.typeInfo.short
Выглядит весьма заманчиво и привлекательно. Но таким образом мы получаем только имя трейта верхнего уровня. Например, если бы у нас было перечисление типа:
enum Animal { case Dog case Cat case Other(kind: String) }
и мы бы осуществили вызов нашего производного Show следующим образом:
summon[Show[Animal]].show(Animal.Dog)
то получили бы Animal, когда ожидали Dog.
Правильный подход
Поэтому нам необходимо выяснить, с каким именно подтипом мы работаем. К счастью, наряду с полями, которые мы уже видели, SealedTrait предоставляет метод choose:
/** * Provides a way to recieve the type info for the explicit subtype that * 'value' is an instance of. So if 'Type' is a Sealed Trait or Scala 3 * Enum like 'Suit', the 'handle' function will be supplied with the * type info for the specific subtype of 'value', eg 'Diamonds'. * * @param value must be instance of a subtype of typeInfo * @param handle function that will be passed the Subtype of 'value' * @tparam Return whatever type the 'handle' function wants to return * @return whatever the 'handle' function returned! */ def choose[Return](value: Type)(handle: Subtype[_] => Return): Return
Он делает именно то, что мы ищем — получает подтип нашего ADT (абстрактный тип данных). Как должен выглядеть наш метод handle? Он должен вызывать метод show на подтипе, потому что это может быть кейс-класс, и show должен быть рекурсивным.
В псевдокоде мы ищем что-то вроде:
sealedTrait.choose(value){ subtype => Show[subtype.Type].show(value) }
Это означает, что нам нужно научиться запрашивать экземпляр класса типа для нашего подтипа. Для этого необходимо вызвать subtype.typeclass. Таким образом, наша следующая итерация будет такой
sealedTrait.choose(value){ subtype => subtype.typeclass.show(value) }
Это еще не работает, компилятор выдает ошибку:
Found: (value : T) Required: subtype.S & T
Так произошло потому, что класс типа для подтипа работает только для подмножества нашего исходного ADT. Поскольку данный метод вызывается только в том случае, если предоставленное нами значение соответствует данному подтипу, мы можем безопасно выполнить приведение значения. Это можно сделать с помощью subtype.cast(value)
sealedTrait.choose(value){ subtype => subtype.typeclass.show(subtype.cast(value)) }
Используя полученные сведения вот как мы можем реализовать метод join, применив то, чему мы научились
override def split[T](sealedTrait: SealedTrait[Show, T]): Show[T] = new Show[T] { def show(value: T): String = sealedTrait.choose(value){ subtype => subtype.typeclass.show(subtype.cast(value)) } }
Соедините их вместе
Поскольку наши строительные блоки готовы, давайте их соединим:
import magnolia1.* object Show { object givens extends AutoDerivation[Show] { given Show[String] = value => value given [A](using Numeric[A]): Show[A] = _.toString // generate Show instance for case classes override def join[T](caseClass: CaseClass[Show, T]): Show[T] = new Show[T] { def show(value: T) = { val name = caseClass.typeInfo.short val serializedParams = caseClass.parameters.map { parameter => s"${parameter.label} = ${parameter.typeclass.show(parameter.deref(value))}" }.mkString(", ") s"$name($serializedParams)" } } // generate Show instance for sealed traits override def split[T](sealedTrait: SealedTrait[Show, T]): Show[T] = new Show[T] { def show(value: T): String = sealedTrait.choose(value){ subtype => subtype.typeclass.show(subtype.cast(value)) } } } }
Наряду с двумя методами, необходимыми для Magnolia, я добавил гивены (контекстные параметры) для String и Numerics в качестве основы для построения более сложных типов.
Мы можем протестировать созданный код, добавив метод main и несколько тестовых структур данных.
case class MyCaseClass(number: Long, name: String) enum Animal { case Dog case Cat case Other(kind: String) } @main def main() = { import Show.givens.given println( summon[Show[MyCaseClass]].show(MyCaseClass(number = 5, name = "test")) ) println( summon[Show[Animal]].show(Animal.Dog) ) println( summon[Show[Animal]].show(Animal.Other("snake")) ) }
Обратите внимание, что мы никогда не предоставляем явную имплементацию Show для наших пользовательских типов, поскольку в import Show.givens.given все уже предусмотрено.
Код примера также доступен по адресу https://github.com/majk-p/derive-show-with-magnolia.
Как скрестить http4s и ZIO? Разберемся завтра на открытом уроке в OTUS. На этой встрече мы:
-
узнаем oб основных компонентах REST-сервиса;
-
сформируем представление о http4s (http-библиотека) и ZIO (библиотека асинхронного функционального эффекта);
-
попрактикуемся в создании полноценного простого http-сервиса — сервер, эндпоинты, логика.
Урок будет полезен всем, кто уже знаком со Scala и хочет писать веб-сервисы, используя наиболее популярные решения из экосистемы функционального программирования.
Записаться на открытый урок можно на странице курса «Scala-разработчик».
ссылка на оригинал статьи https://habr.com/ru/companies/otus/articles/743094/
Добавить комментарий