Главная страница
Навигация по странице:

  • Листинг 23.13.

  • Листинг 23.14.

  • Листинг 23.16.

  • Листинг 23.17.

  • Листинг 23.18.

  • Листинг 23.19.

  • Листинг 23.20.

  • Листинг 23.22.

  • Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста


    Скачать 6.24 Mb.
    НазваниеОдерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
    Дата27.04.2023
    Размер6.24 Mb.
    Формат файлаpdf
    Имя файлаScala. Профессиональное программирование 2022.pdf
    ТипДокументы
    #1094967
    страница53 из 64
    1   ...   49   50   51   52   53   54   55   56   ...   64
    501
    Включив неявные преобразования, вы можете написать следующее (при условии, что гивен streetToString находится в области видимости в качестве единого идентификатора):
    val streetStr: String = street
    Здесь компилятор встречает
    Street в контексте, в котором должен быть указан тип
    String
    , и воспринимает это как обычную ошибку типизации.
    Но прежде, чем сдаваться, он ищет неявное преобразование
    Street в
    String
    В данном случае он находит streetToString
    . После этого компилятор авто­
    матически вставляет streetToString в приложение. При этом внутри код принимает следующий вид:
    val streetStr: String = streetToString(street)
    Это неявное преобразование в буквальном смысле этого слова. Здесь нет явного запроса на приведение типов. Вместо этого вы пометили метод streetToString как доступное неявное преобразование, разместив его в об­
    ласти видимости и определив его как given
    . В результате компилятор будет автоматически использовать его каждый раз, когда
    Street необходимо при­
    вести к
    String
    Если же вы хотите определить неявное преобразование, позаботьтесь о том, чтобы оно всегда было подходящим. Например, приведение
    Double к
    Int неявным образом может вызвать недоумение, поскольку автомати­
    ческая потеря точности значения является сомнительной идеей. Поэтому мы на самом деле рекомендуем не преобразование. Намного логичнее двигаться в обратном направлении: от более конкретного типа к более общему. Например,
    Int можно преобразовать в
    Double без потери точно­
    сти, поэтому неявное приведение
    Int к
    Double имеет смысл. На самом деле именно это и происходит. Объект scala.Predef
    , который автоматически импортируется любой программой на Scala, определяет неявные преоб­
    разования «меньших» числовых типов в «большие», в том числе и при­
    ведение
    Int к
    Double
    Вот почему в Scala значения
    Int можно хранить в переменных типа
    Double
    В системе типов для этого не предусмотрено отдельного правила; это просто применение неявного преобразования
    1 1
    Тем не менее компилятор Scala относится к этому преобразованию по­особому, переводя его в специальный байт­код i2d
    . Благодаря этому скомпилированный образ получается таким же, как и в Java.

    502 Глава 23 • Классы типов
    23 .6 . Пример использования класса типов: сериализация JSON
    В разделе 23.1 мы упоминали сериализацию в качестве примера поведения, применимого к типам, которые в остальном не имеют ничего общего и, сле­
    довательно, являются хорошими кандидатами на попадание в класс типов.
    Напоследок в этой главе мы хотели бы проиллюстрировать использование класса типов для поддержки сериализации в JSON. Чтобы не усложнять этот пример, мы проигнорируем десериализацию, хотя обычно оба эти процесса реализуются в одной и той же библиотеке.
    JSON — широко используемый формат обмена данными между клиентами на JavaScript и серверными приложениями
    1
    . Он определяет форматы для представления строк, чисел, логических значений, массивов и объектов.
    Таким образом, все, что вы хотите сериализовать в JSON, должно быть вы­
    ражено в одном из этих пяти типов данных. Строки JSON выглядят как строковые литералы Scala, такие как "tennis"
    . Числа JSON, представля­
    ющие целочисленные значения, аналогичны литералам
    Int в Scala, таким как
    10
    . Логическое значение JSON может быть равно либо true
    , либо false
    Объект JSON — это набор пар «ключ — значение», разделенных запятыми и заключенных в фигурные скобки; ключ представляет собой строковое имя.
    Массив JSON — это список типов данных JSON, разделенных запятыми и за­
    ключенных в квадратные скобки. В JSON также определено значение null
    Вот пример объекта, содержащего по одному члену каждого из остальных четырех типов JSON, плюс член null
    :
    {
    "style": "tennis",
    "size": 10,
    "inStock": true,
    "colors": ["beige", "white", "blue"],
    "humor": null
    }
    В этом примере мы сериализуем значения
    String
    Scala в строки JSON,
    Int и
    Long в числа JSON,
    Boolean в логические значения JSON,
    List в массивы
    JSON и несколько других типов в объекты JSON. Необходимость сериализа­
    ции типов из стандартной библиотеки Scala, таких как
    Int
    , подчеркивает то, насколько трудно было бы решить эту задачу путем примеси трейта в класс,
    1
    JSON расшифровывается как JavaScript Object Notation.

    23 .6 . Пример использования класса типов: сериализация JSON 503
    который вы хотите сериализовать. Вы можете определить такой трейт и на­
    звать его, скажем,
    JsonSerializable
    . Он может предоставлять метод toJson
    , который генерирует текст JSON для этого объекта. Затем вы могли бы при­
    мешивать
    JsonSerializable в свои собственные классы и реализовывать метод toJson
    . Однако с такими типами, как
    String
    ,
    Int
    ,
    Long
    ,
    Boolean или
    List
    , это не сработает, так как их нельзя изменять.
    Подход на основе класса типов лишен этой проблемы. Вы можете опре­
    делить иерархию классов, целиком ориентированную на сериализацию объектов абстрактного типа
    T
    в JSON, не требуя при этом, чтобы классы, которые вы хотите сериализовать, наследовали общий супертрейт. Вместо этого можно определить given­экземпляр трейта класса типов для каждого типа, предназначенного для сериализации в JSON. Такой трейт, с именем
    JsonSerializer
    , показан в листинге 23.13. Он принимает один параметр типа,
    T
    , и предлагает метод serialize
    , который берет экземпляр
    T
    и преоб­
    разует его в строку JSON.
    Листинг 23.13. Класс типов для сериализации в JSON
    trait JsonSerializer[T]:
    def serialize(o: T): String
    Чтобы у ваших пользователей была возможность вызывать метод toJson из сериализуемых классов, можно определить метод расширения. Как уже об­
    суждалось в разделе 22.5, подходящим местом для размещения этого метода является трейт самого класса типов. Если вы выберете этот вариант, метод toJson будет доступен в типе
    T
    всегда, когда
    JsonSerializer[T]
    находится в области видимости. Трейт
    JsonSerializer
    , улучшенный с помощью этого метода расширения, показан в листинге 23.14.
    Листинг 23.14. Класс типов для сериализации в JSON с методом расширения trait JsonSerializer[T]:
    def serialize(o: T): String extension (a: T)
    def toJson: String = serialize(a)
    Следующим шагом было бы логично определить given­экземпляры класса типов для
    String
    ,
    Int
    ,
    Long и
    Boolean
    . Подходящим местом для их размеще­
    ния будет объект­компаньон
    JsonSerializer
    , поскольку, как описывалось в разделе 21.2, компилятор ищет в нем нужный given­экземпляр, если его не удалось найти в области видимости. Эти гивены можно определить так, как показано в листинге 23.15.

    504 Глава 23 • Классы типов
    Листинг 23.15. Объект-компаньон сериализатора JSON с гивенами object JsonSerializer:
    given stringSerializer: JsonSerializer[String] with def serialize(s: String) = s"\"$s\""
    given intSerializer: JsonSerializer[Int] with def serialize(n: Int) = n.toString given longSerializer: JsonSerializer[Long] with def serialize(n: Long) = n.toString given booleanSerializer: JsonSerializer[Boolean] with def serialize(b: Boolean) = b.toString
    Импорт метода расширения
    Вам может пригодиться возможность импортировать метод расшире­
    ния, добавляющий метод toJson в любые типы
    T
    , для которых доступен
    JsonSerializer[T]
    . Метод расширения, определенный в листинге 23.14, на это не способен, так как он делает toJson доступным для
    T
    только в случае, если
    JsonSerializer[T]
    находится в области видимости. В про­
    тивном случае он не сработает, даже если
    JsonSerializer[T]
    присутствует в объекте­компаньоне для
    T
    . Чтобы упростить импорт метода расширения, можете поместить его в объект­одиночку, такой как показан в листин­
    ге 23.16. Этот метод содержит инструкцию using
    , которая требует, чтобы гивен
    JsonSerializer[T]
    был доступен для типа
    T
    , к которому этот метод применяется.
    Листинг 23.16. Метод расширения для удобного импорта object ToJsonMethods:
    extension [T](a: T)(using jser: JsonSerializer[T])
    def toJson: String = jser.serialize(a)
    Имея в своем распоряжении объект
    ToJsonMethods
    , вы можете поэкспери­
    ментировать с сериализаторами в REPL. Вот несколько примеров их ис­
    пользования:
    import ToJsonMethods.*
    "tennis".toJson // "tennis"
    10.toJson // 10
    true.toJson // true
    Будет полезно сравнить два метода расширения: один в объекте
    ToJsonMethods из листинга 23.16, а другой — в трейте
    JsonSerializer из ли­
    стинга 23.14. Метод расширения
    ToJsonMethods принимает
    JsonSerializer[T]
    в качестве параметра using
    , а метод расширения в
    JsonSerializer этого не делает, так как он по определению является членом
    JsonSerializer[T]
    Таким образом, если toJson в
    ToJsonMethods вызывает serialize из пере­

    23 .6 . Пример использования класса типов: сериализация JSON 505
    данной ссылки
    JsonSerializer с именем jser
    , то метод toJson в трейте
    JsonSerializer вызывает serialize из this
    Сериализация объектов предметной области
    Теперь представьте, что вам нужно сериализовать в JSON экземпляры опре­
    деленных классов в модели вашей предметной области, включая адресную книгу, показанную в листинге 23.17. Эта книга содержит список контактов, у каждого из которых есть произвольное количество адресов и телефонных номеров (от 0 и больше)
    1
    Листинг 23.17. Классы-образцы для адресной книги case class Address(
    street: String,
    city: String,
    state: String,
    zip: Int
    )
    case class Phone(
    countryCode: Int,
    phoneNumber: Long
    )
    case class Contact(
    name: String,
    addresses: List[Address],
    phones: List[Phone]
    )
    case class AddressBook(contacts: List[Contact])
    Строка JSON для адресной книги формируется из строк JSON ее вложенных объектов. Таким образом, чтобы сгенерировать строку JSON для адресной книги, каждый из ее вложенных объектов должен поддерживать преоб­
    разование в формат JSON. Например, каждый экземпляр
    Contact в поле contacts должен быть представлен в формате JSON этого контакта. Каждый экземпляр
    Address контакта должен быть преобразован в JSON этого адреса.
    Следовательно, для сериализации
    AddressBook необходимо сериализовать в JSON каждый объект, из которого состоит адресная книга. Поэтому будет логично определить сериализаторы для всех объектов предметной области.
    1
    Для атрибутов этих классов было бы лучше определить крошечные типы, как описывалось в разделе 17.4. Но, чтобы не усложнять этот пример, мы будем ис­
    пользовать типы
    String и
    Int

    506 Глава 23 • Классы типов
    Хорошим местом размещения given­экземпляров
    JsonSerializer для ваших объектов предметной области являются их объекты­компаньоны. В листин­
    ге 23.18 показано, как вы можете, к примеру, определить сериализаторы для
    Address и
    Phone
    . В методах serialize мы импортируем и используем метод расширения toJson из объекта
    ToJsonMethods
    , показанного в листинге 23.16, но переименовываем его в asJson
    . Это необходимо, чтобы избежать кон­
    фликта с одноименным методом расширения toJson
    , унаследованным от
    JsonSerializer
    (см. листинг 23.14).
    Листинг 23.18. Сериализаторы JSON для Address и Phone object Address:
    given addressSerializer: JsonSerializer[Address] with def serialize(a: Address) =
    import ToJsonMethods.{toJson as asJson}
    s"""|{
    | "street": ${a.street.asJson},
    | "city": ${a.city.asJson},
    | "state": ${a.state.asJson},
    | "zip": ${a.zip.asJson}
    |}""".stripMargin object Phone:
    given phoneSerializer: JsonSerializer[Phone] with def serialize(p: Phone) =
    import ToJsonMethods.{toJson as asJson}
    s"""|{
    | "countryCode": ${p.countryCode.asJson},
    | "phoneNumber": ${p.phoneNumber.asJson}
    |}""".stripMargin
    Сериализация списков
    Два других объекта предметной области,
    Contact и
    AddressBook
    , содержат списки. Поэтому для их сериализации было бы полезно иметь общую про­
    цедуру преобразования типов
    List
    Scala в массивы JSON. Массив JSON представляет собой список типов данных JSON, разделенных запятыми и заключенных в квадратные скобки, поэтому
    List[T]
    можно сериализо­
    вать для любого типа
    T
    , при условии существования
    JsonSerializer[T]
    В листинге 23.19 показан гивен
    JsonSerializer для списков, который будет генерировать массив JSON из
    List
    , если для типа элементов списка существу­
    ет
    JsonSerializer
    Листинг 23.19. Given-сериализатор JSON для списков object JsonSerializer:
    // гивены для строк, целых чисел и логических значений…
    given listSerializer[T](using

    23 .6 . Пример использования класса типов: сериализация JSON 507
    JsonSerializer[T]): JsonSerializer[List[T]] with def serialize(ts: List[T]) =
    s"[${ts.map(t => t.toJson).mkString(", ")}]"
    Чтобы выразить зависимость от сериализатора для типа элементов спи­
    ска, гивен listSerializer принимает в качестве параметра using сериали­
    затор, способный сгенерировать JSON для элементов этого типа. Напри­
    мер, чтобы преобразовать
    List[Address]
    в массив JSON, необходимо иметь given­сериализатор для самого типа
    Address
    . Если сериализатор
    Address недоступен, программа не скомпилируется. Например, поскольку гивен
    JsonSerializer[Int]
    находится в объекте­компаньоне
    JsonSerializer
    , вы можете сериализовать
    List[Int]
    в JSON, как показано ниже:
    import ToJsonMethods.*
    List(1, 2, 3).toJson // [1, 2, 3]
    С другой стороны, мы еще не определили
    JsonSerializer[Double]
    , поэтому попытка сериализовать
    List[Double]
    в JSON приведет к ошибке компиляции:
    scala> List(1.0, 2.0, 3.0).toJson
    1 |List(1.0, 2.0, 3.0).toJson
    |ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
    |value toJson is not a member of List[Double].
    |An extension method was tried, but could not be fully
    |constructed:
    |
    | ToJsonMethods.toJson[List[Double]](
    | List.apply[Double]([1.0d,2.0d,3.0d : Double]*)
    | )(JsonSerializer.listSerializer[T](
    | /* missing */summon[JsonSerializer[Double]]))
    | failed with
    |
    | no implicit argument of type JsonSerializer[List[Double]]
    | was found for parameter json of method toJson in
    | object ToJsonMethods.
    | I found:
    |
    | JsonSerializer.listSerializer[T](
    | /* missing */summon[JsonSerializer[Double]])
    |
    | But no implicit values were found that match type
    | JsonSerializer[Double].
    Этот пример иллюстрирует важное преимущество использования классов типов для сериализации объектов Scala: классы типов позволяют компиля­
    тору убедиться в том, что все классы, из которых состоит
    AddressBook
    , можно преобразовать в JSON. Например, если не предоставить given­экземпляр
    Address
    , программа не скомпилируется. Для сравнения: если в Java глубоко

    508 Глава 23 • Классы типов вложенный объект не реализует
    Serializable
    , вы получите исключение на этапе выполнения. Эта ошибка может произойти в обоих языках, но если в Java она происходит во время работы программы, то в Scala благодаря классам типов она проявляется на этапе компиляции.
    Напоследок стоит отметить, что возможность вызова toJson в теле функции, переданной в map
    (
    "toJson"
    в t
    =>
    t.toJson
    ), объясняется наличием в об­
    ласти видимости гивена
    JsonSerializer[T]
    : анонимного параметра using
    , переданного в listSerializer
    . Метод расширения, который используется в этом случае, объявлен в самом трейте
    JsonSerializer
    , представленном в листинге 23.14.
    Собираем все вместе
    Теперь, имея в своем распоряжении способ сериализации списков, вы може­
    те применить его в сериализаторах для
    Contact и
    AddressBook
    . Это показано в листинге 23.20. Как и прежде, при импорте метода расширения toJson нужно переименовать в asJson
    , чтобы избежать конфликта имен.
    Листинг 23.20. Given-сериализаторы JSON для Contact и AddressBook object Contact:
    given contactSerializer: JsonSerializer[Contact] with def serialize(c: Contact) =
    import ToJsonMethods.{toJson as asJson}
    s"""|{
    | "name": ${c.name.asJson},
    | "addresses": ${c.addresses.asJson},
    | "phones": ${c.phones.asJson}
    |}""".stripMargin object AddressBook:
    given addressBookSerializer: JsonSerializer[AddressBook] with def serialize(a: AddressBook) =
    import ToJsonMethods.{toJson as asJson}
    s"""|{
    | "contacts": ${a.contacts.asJson}
    |}""".stripMargin
    У нас все готово для сериализации адресной книги в JSON. В качестве при­
    мера возьмем экземпляр
    AddressBook
    , показанный в листинге 23.21, на ко­
    торый ссыла ется переменная addressBook
    . Импортировав из
    ToJsonMethods метод расширения toJson
    , вы сможете сериализовать эту адресную книгу с помощью вызова:
    addressBook.toJson

    23 .6 . Пример использования класса типов: сериализация JSON 509
    Результат в формате JSON показан в листинге 23.22.
    Листинг 23.21. AddressBook val addressBook =
    AddressBook(
    List(
    Contact(
    "Bob Smith",
    List(
    Address(
    "12345 Main Street",
    "San Francisco",
    "CA",
    94105
    ),
    Address(
    "500 State Street",
    "Los Angeles",
    "CA",
    90007
    )
    ),
    List(
    Phone(
    1,
    5558881234
    ),
    Phone(
    49,
    5558413323
    )
    )
    )
    )
    )
    Листинг 23.22. Адресная книга, представленная в формате JSON
    {
    "contacts": [{
    "name": "Bob Smith",
    "addresses": [{
    "street": "12345 Main Street",
    "city": "San Francisco",
    "state": "CA",
    "zip": 94105
    }, {
    "street": "500 State Street",
    "city": "Los Angeles",
    "state": "CA",

    510 Глава 23 • Классы типов "zip": 90007
    }],
    "phones": [{
    "countryCode": 1,
    "phoneNumber": 5558881234
    }, {
    "countryCode": 49,
    "phoneNumber": 5558413323
    }]
    }]
    }
    Конечно, настоящая библиотека для работы с JSON была бы намного слож­
    нее того, что вы увидели в этом примере. Вам, скорее всего, следовало бы вос­
    пользоваться средствами метапрограммирования Scala, чтобы автоматизи­
    ровать генерацию экземпляров
    JsonSerializer за счет вывода класса типов.
    Резюме
    В этой главе вы познакомились с классами типов и рассмотрели несколько примеров. Классы типов — это основополагающий способ реализации спе­
    циального полиморфизма в Scala. Тот факт, что для классов типов в Scala предусмотрен синтаксический сахар в виде границ контекста, говорит о том, насколько важным является данный подход к проектированию в этом языке.
    Вам встречалось несколько способов применения классов типов: для глав­
    ных методов, безопасных проверок на равенство, неявных преобразований и сериализации JSON. Надеемся, эти примеры дали вам представление о том, в каких ситуациях классы типов являются подходящим архитектур­
    ным решением. В следующей главе мы сменим тему и подробно рассмотрим библиотеку коллекций Scala.

    1   ...   49   50   51   52   53   54   55   56   ...   64


    написать администратору сайта