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

  • Листинг 23.7.

  • Листинг 23.8.

  • Рис. 23.4

  • Листинг 23.11

  • Листинг 23.12.

  • 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
    страница52 из 64
    1   ...   48   49   50   51   52   53   54   55   ...   64
    492 Глава 23 • Классы типов ходятся в объекте­компаньоне
    FromString
    . Если вы хотите написать главный метод, принимающий нестандартный тип, можете объявить для этого типа given­экземпляр класса типов
    FromString
    Представьте, к примеру, что вам нужно дополнить главный метод repeat третьим параметром командной строки, описывающим одно из трех настрое­
    ний: удивлен, зол и нейтрален. Чтобы описать настроение, можно определить перечисление
    Mood
    , как показано в листинге 23.7.
    Листинг 23.7. Перечисление Mood
    // В файле moody.scala enum Mood:
    case Surprised, Angry, Neutral
    Создав перечисление, вы можете улучшить главный метод repeat так, чтобы он принимал
    Mood в качестве третьего параметра. Это показано в листин­
    ге 23.8.
    Листинг 23.8. Главный метод, принимающий нестандартный тип
    // В файле moody.scala val errmsg =
    "Please enter a word, a positive integer count, and\n" +
    "a mood (one of 'angry', 'surprised', or 'neutral')"
    @main def repeat(word: String, count: Int, mood: Mood) =
    val msg =
    if count > 0 then val words = List.fill(count)(word.trim)
    val punc =
    mood match case Mood.Angry => "!"
    case Mood.Surprised => "?"
    case Mood.Neutral => ""
    val sep = punc + " "
    words.mkString(sep) + punc else errmsg println(msg)
    Теперь остается только научить компилятор преобразовывать строковой параметр командной строки в
    Mood
    . В этой связи для типа
    Mood нужно опре­
    делить экземпляр
    FromString
    . Хорошим местом размещения этого экземп­
    ляра будет объект­компаньон
    Mood
    , так как компилятор заглянет в него при поиске гивена
    FromString[Mood]
    . В листинге 23.9 показан один из вариантов реализации.

    23 .3 . Главные методы 493
    Листинг 23.9. Given-экземпляр FromString для Mood
    // В файле moody.scala object Mood:
    import scala.util.CommandLineParser.FromString given moodFromString: FromString[Mood] with def fromString(s: String): Mood =
    s.trim.toLowerCase match case "angry" => Mood.Angry case "surprised" => Mood.Surprised case "neutral" => Mood.Neutral case _ => throw new IllegalArgumentException(errmsg)
    Имея в своем распоряжении определение given­экземпляра
    F r o m -
    String[Mood]
    , вы можете запустить свое приложение repeat
    :
    $ scalac moody.scala
    $ scala repeat hello 3 neutral hello hello hello
    $ scala repeat hello 3 surprised hello? hello? hello?
    $ scala repeat hello 3 angry hello! hello! hello!
    Решение, основанное на классе типов, хорошо подходит для анализаторов ар­
    гументов командной строки, которые принимают главные методы, так как эта возможность требуется лишь для определенных типов, которые в остальном не имеют друг к другу никакого отношения. Помимо
    String и
    Int
    , объект­
    компаньон
    FromString определяет given­экземпляры
    FromString для
    Byte
    ,
    Short
    ,
    Long
    ,
    Boolean
    ,
    Float и
    Double
    . Если прибавить к этому given­экземпляр
    FromString[Mood]
    , размещенный в объекте­компаньоне
    Mood из листинга 23.9, множество типов, составляющих класс типов
    FromString
    , будет выглядеть как на рис. 23.4.
    Рис. 23.4 . Множество типов T с given-экземплярами FromString[T]

    494 Глава 23 • Классы типов
    23 .4 . Многостороннее равенство
    В Scala 2 было реализовано универсальное равенство, которое позволяет проверять на равенство два любых объекта с использованием
    ==
    и
    !=
    . Поль­
    зователям было легко понять этот подход, и он хорошо сочетался с методом equals из Java, с помощью которого можно сравнить два любых экземпляра
    Object
    . Это также сделало возможной поддержку в Scala 2 совместного ра-
    венства — это когда на равенство проверяются разные взаимодействующие между собой типы. Совместное равенство, к примеру, позволило Scala 2 продолжить традицию Java, состоящую в том, что проверку равенства
    Int и
    Long можно осуществлять без явного приведения первого типа ко второму.
    Тем не менее у универсального равенства был один серьезный недостаток: оно скрывало программные дефекты. Например, в Scala 2 вы могли прове­
    рять на равенство строки и объекты
    Option
    . Например:
    scala> "hello" == Option("hello") // (в Scala 2)
    val res0: Boolean = false
    И хотя на этапе выполнения Scala 2 дает верный ответ (строка "hello"
    дей­
    ствительно не равна
    Option("hello")
    ), не существует такой строки, которая была бы равна какому­либо объекту
    Option
    . Результатом этого сравне­
    ния всегда будет false
    . Таким образом, любая проверка строки и объекта
    Option на равенство, скорее всего, является ошибкой, которую не выявил компилятор Scala 2. Такого рода ошибки можно легко допустить во время рефакторинга — например, вы можете поменять тип переменной с
    String на
    Option[String]
    и не заметить, что теперь в другом месте на равенство про­
    веряются
    String и
    Option[String]
    Для сравнения: попытка проделать то же самое в Scala 3 завершится ошибкой компиляции:
    scala> "hello" == Option("hello") // (в Scala 3)
    1 |”hello” == Option("hello")
    |ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
    |Values of types String and Option[String] cannot be
    | compared with == or !=
    Это улучшение в безопасности Scala 3 достигается за счет новой возможно­
    сти под названием многостороннее равенство. Оно заключается в том, что компилятор по­особому обращается с методами
    ==
    и
    !=
    . Определения этих методов, показанные в листинге 23.10, не изменились в Scala 3 по сравнению со Scala 2. Поменялось лишь поведение компилятора: в Scala 3 универсаль­
    ное сравнение преобразуется в многостороннее.

    23 .4 . Многостороннее равенство 495
    Листинг 23.10. Методы == и != в Scala 2 и Scala 3
    // На примере класса Any:
    final def ==(that: Any): Boolean final def !=(that: Any): Boolean
    Чтобы понять, как работает многостороннее равенство в Scala 3, будет по­
    лезно рассмотреть детали реализации универсального равенства в Scala 2.
    Вот как оно было устроено на уровне JVM: когда компилятор Scala 2 об­
    наруживал вызов
    ==
    и
    !=
    , он сначала проверял, принадлежат ли сравнива­
    емые значения к простым типам Java. Если да, то компилятор генерировал специальный байт­код для эффективной проверки этих простых типов на равенство. В противном случае, если один из типов был простым, а другой — нет, компилятор генерировал код для упаковки простого значения. В итоге оба операнда имели ссылочные типы. Затем компилятор генерировал код, который сначала определял, равен ли левый операнд null
    . Если да, то сге­
    нерированный код проводил ту же проверку для правого операнда, чтобы получить результат типа
    Boolean
    . Это гарантировало, что вызовы
    ==
    и
    !=
    не могут сгенерировать
    NullPointerException
    . В противном случае сгенериро­
    ванный код вызывал из левого операнда, который, как уже было установлено, не равнялся null
    , метод equals
    , передавая ему правый операнд.
    В Scala 3 компилятор выполняет точно такие же действия, но сначала про­
    веряет, допускается ли это сравнение. Для этого он ищет given­экземпляр класса типов с именем
    CanEqual
    . Вот его определение:
    sealed trait CanEqual[-L, -R]
    Трейт
    CanEqual принимает два параметра типов,
    L
    и
    R
    1
    L
    — это тип левого операнда проверки на равенство, а
    R
    — тип правого операнда.
    CanEqual не предоставляет никакого метода для непосредственной проверки на равен­
    ство двух объектов типа
    L
    и
    R
    , поскольку в Scala 3 эта операция по­прежнему выполняется за счет методов
    ==
    и
    !=
    . Если вкратце, то вместо того, чтобы предоставлять операцию проверки типов
    L
    и
    R
    на равенство, как можно было бы ожидать от обычного класса типов,
    CanEqual просто дает разрешение на проведение этой операции с помощью
    ==
    и
    !=
    1
    Класс типов обычно означает множество типов, для которых доступны given­
    экземпляры трейта, принимающего один параметр типа, но
    CanEqual можно считать трейтом, определяющим множество, которое состоит из пар типов. Например, проверка
    String и
    Option[String]
    на равенство не скомпилируется в Scala 3, так как тип (
    String
    ,
    Option[String]
    ) не входит в множество, из которого состоит класс типов
    CanEqual

    496 Глава 23 • Классы типов
    Как описывалось в разделе 18.6, знак минус рядом с параметром типа оз­
    начает, что трейт
    CanEqual является контравариантным как по
    L
    , так и по
    R
    . Ввиду этой контравариантности
    CanEqual[Any,
    Any]
    является подтипом любого другого типа,
    CanEqual[L,
    R]
    , независимо от
    L
    или
    R
    . В результате эк­
    земпляр
    CanEqual[Any,
    Any]
    можно использовать для выдачи разрешения на проверку на равенство любых двух типов. Например, если given­экземпляр
    CanEqual[Int,
    Int]
    нужен, чтобы разрешить проверку на равенство двух значений
    Int
    , given­экземпляра
    CanEqual[Any,
    Any]
    будет достаточно, так как
    CanEqual[Any,
    Any]
    является подтипом
    CanEqual[Int,
    Int]
    . Благодаря этому факту
    CanEqual является запечатанным трейтом всего с одним экземп­
    ляром, который имеет универсально применимый тип
    CanEqual[Any,
    Any]
    Этот объект носит имя derived и объявлен в объекте­компаньоне
    CanEqual
    :
    object CanEqual:
    object derived extends CanEqual[Any, Any]
    Следовательно, чтобы предоставить given­экземпляр
    CanEqual[L,
    R]
    , неза­
    висимо от того, что собой представляют типы
    L
    и
    R
    , вы должны использовать один и только один экземпляр
    CanEqual
    ,
    CanEqual.derived
    В целях обратной совместимости компилятор Scala 3 позволяет проводить некоторые проверки на равенство по умолчанию, даже если для нужного типа недоступен given­экземпляр
    CanEqual
    . Компилятор позволяет про­
    верить, равны ли значения типов
    L
    и
    R
    , даже в случае отсутствия given­
    экземпляра типа
    CanEqual[L,
    R]
    , если выполняется любое из следующих условий.
    1.
    L
    и
    R
    являются одним и тем же типом.
    2. После выполнения подъема типа
    L
    является подтипом
    R
    или наоборот
    1 3. Для типов
    L
    и
    R
    не существует рефлексивного given­экземпляра
    CanEqual
    Рефлексивными являются экземпляры, которые разрешают сравнение типа с самим собой, как в случае с
    CanEqual[L,
    L]
    Третье правило гарантирует, что, как только вы предоставите ре флексивный given­экземпляр
    CanEqual
    , позволив тем самым сравнивать тип с самим со­
    бой, этот тип больше нельзя будет сравнивать ни с каким другим типом — разве что существует given­экземпляр
    CanEqual
    , который позволяет это делать. В целях обратной совместимости Scala 3 фактически возвращается
    1
    Чтобы поднять тип, компилятор заменяет ссылки на абстрактные типы в его кова­
    риантных позициях их верхней границей и подставляет вместо типов уточнения, находящиеся в ковариантных позициях типа, их родителей.

    23 .4 . Многостороннее равенство 497
    к универсальному равенству по умолчанию при сравнении типов, для кото­
    рых не было определено рефлексивных экземпляров
    CanEqual
    Scala 3 предоставляет given­экземпляры для нескольких типов из стан­
    дартной библиотеки, включая рефлексивные экземпляры для строк. Вот почему проверка
    String и
    Option[String]
    на равенство по умолчанию запрещена. Given­экземпляра
    CanEqual[String,
    String]
    , предоставля­
    емого стандартной библиотекой, достаточно для того, чтобы компиля­
    тор Scala 3 по умолчанию не позволял проверять на равенство
    String и
    Option[String]
    Это поведение по умолчанию дает возможность беспроблемного перехода со
    Scala 2 на Scala 3, так как в существующем пользовательском коде, который переносится из Scala 2, нет никаких экземпляров
    CanEqual для этих типов.
    Представьте, к примеру, что ваш имеющийся проект на Scala 2 содержит класс
    Apple
    , определенный следующим образом:
    case class Apple(size: Int)
    И где­то в вашем коде происходит сравнение двух яблок:
    val appleTwo = Apple(2)
    val appleTwoToo = Apple(2)
    appleTwo == appleTwoToo // true
    Это сравнение по умолчанию будет компилироваться и работать в Scala 3, так как левая и правая стороны имеют один и тот же тип. Однако в Scala 3 компилятор по умолчанию все так же позволяет сравнивать типы, для ко­
    торых не существует рефлексивных given­экземпляров
    CanEqual
    , поэтому следующая нежелательная проверка на равенство по­прежнему успешно компилируется:
    case class Orange(size: Int)
    val orangeTwo = Orange(2)
    appleTwo == orangeTwo // false
    Это сравнение
    Apple и
    Orange
    , скорее всего, является ошибочным, так как оно всегда возвращается false
    . Чтобы выполнить полную проверку рабо­
    тоспособности всех сравнений на равенство в Scala 3, даже если для задей­
    ствованных типов не было определено рефлексивных given­экземпляров, вы можете включить «строгое равенство». Для этого нужно либо передать компилятору параметр командной строки
    -language:strictEquality
    , либо добавить следующую инструкцию импорта в исходный файл:
    import scala.language.strictEquality

    498 Глава 23 • Классы типов
    После включения строгого равенства сравнение яблок и апельсинов будет заканчиваться ожидаемой ошибкой компиляции:
    scala> appleTwo == orangeTwo
    1 |appleTwo == orangeTwo
    |ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
    |Values of types Apple and Orange cannot be
    | compared with == or !=
    К сожалению, вместе с этим теперь возникает другая, нежелательная ошибка компиляции, связанная с корректным сравнением двух яблок:
    scala> appleTwo == appleTwoToo
    1 |appleTwo == appleTwoToo
    |ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
    |Values of types Apple and Apple cannot be
    | compared with == or !=
    Чтобы сделать возможным это сравнение в режиме строгого равенства, нуж­
    но предоставить экземпляр
    CanEqual
    , который разрешает проверять яблоки на равенство с другими яблоками. Для этого в объекте­компаньоне
    Apple можно определить явный given­экземпляр, как показано в листинге 23.11
    (хотя это идет вразрез с философией языка).
    Листинг 23.11 . Явно определенный (но не идиоматический) провайдер CanEqual case class Apple(size: Int)
    object Apple:
    given canEq: CanEqual[Apple, Apple] = CanEqual.derived
    Вместо этого лучше указать, что вы хотите вывести для своего экземпляра
    Apple экземпляр класса типов
    CanEqual
    , как показано в листинге 23.12.
    Листинг 23.12. Предоставление CanEqual с помощью инструкции derives case class Apple(size: Int) derives CanEqual // идиоматически
    Этот идиоматический подход основан на выводе класса типов, который по­
    зволяет делегировать определение given­экземпляра класса типов члену с именем derived в объекте­компаньоне этого класса. Это заставляет ком­
    пилятор вставить заданный провайдер (как тот, что был показан в листин­
    ге 23.11) в объект­компаньон
    Apple
    Об инструкции derives можно было бы рассказывать и дальше, так как боль­
    шинство методов derived генерируют экземпляры класса типов с помощью метапрограммирования на этапе компиляции.

    23 .5 . Неявные преобразования 499
    Теперь благодаря тому, что вы определили экземпляр
    CanEqual[Apple,
    Apple]
    с использованием инструкции derives
    , компилятор позволит вам сравнивать яблоки в режиме строгого равенства:
    appleTwo == appleTwoToo // тоже true
    23 .5 . Неявные преобразования
    Неявные преобразования были первой неявной конструкцией в Scala. Они создавались для того, чтобы помочь сделать код яснее и избавиться от шаблонных приведений типов. Например, стандартная библиотека Scala определяет неявное преобразование
    Int в
    Long
    . Если передать
    Int методу, ко­
    торый ожидает
    Long
    , компилятор автоматически приведет тип
    Int к
    Long
    , не требуя явного вызова функции преобразования, такой как toLong
    . Поскольку любое значение
    Int можно безопасно привести к значению
    Long и оба этих типа представляют целые числа в дополнительном коде, это неявное пре­
    образование может облегчить чтение исходного кода за счет устранения повторяющихся участков.
    Однако со временем неявные преобразования вышли из моды, так как они могли делать код не только яснее за счет удаления шаблонных участков, но и более запутанным ввиду отсутствия четкости. В Scala были добавлены другие конструкции, которые являются более удачными альтернативами неявных преобразований, включая методы расширения и контекстные па­
    раметры. В Scala 3 для неявных преобразований осталось всего несколько сценариев применения. И хотя они по­прежнему поддерживаются, для их использования нужно импортировать флаг переключения возможностей, иначе компилятор выдаст предупреждение.
    Вот как они работают. Если компилятор Scala приходит к выводу, что указанный тип не отвечает ожидаемому, он начинает искать неявное пре­
    образование, которое сможет исправить ошибку выбора кандидата. Иными словами, каждый раз, когда компилятор встречает
    X
    там, где требуется
    Y
    , он ищет неявное преобразование, которое приводит
    X
    к
    Y
    . Представьте, к примеру, что у вас есть крошечный тип
    1
    для представления поля «улица» в почтовом адресе:
    case class Street(value: String)
    1
    Крошечные типы обсуждались в разделе 17.4.

    500 Глава 23 • Классы типов
    И у вас имеется экземпляр этого класса:
    val street = Street("123 Main St")
    В этом случае вы не сможете инициализировать переменную типа
    String с помощью
    Street
    :
    scala> val streetStr: String = street
    1 |val streetStr: String = street
    | ˆˆˆˆˆˆ
    | Found: (street : Street)
    | Required: String
    Вместо этого вам придется вручную привести
    Street к
    String путем вызова street.value
    :
    val streetStr: String = street.value // 123 Main St
    Этот код легко понять, но у вас может быть ощущение того, что вызов value из
    String для преобразования значения в
    String является шаблонным и мало­
    информативным кодом. Поскольку тип
    Street всегда можно безопасно при­
    вести к типу
    String
    , который лежит в его основе, вам, возможно, захочется предоставить неявное преобразование из
    Street в
    String
    . В Scala 3 для этого нужно определить given­экземпляр типа
    Conversion[Street,
    String]
    1
    , кото­
    рый является потомком типа функции
    Street
    =>
    String
    . Вот его определение:
    abstract class Conversion[-T, +U] extends (T => U):
    def apply(x: T): U
    Поскольку у трейта
    Conversion есть всего один абстрактный метод, для определения экземпляра зачастую можно использовать функциональный литерал SAM
    2
    . Следовательно, неявное приведение
    Street к
    String можно определить так:
    given streetToString: Conversion[Street, String] = _.value
    Чтобы не получать предупреждения во время компиляции при использова­
    нии неявных преобразований, для их включения следует либо передать ком­
    пилятору параметр
    -language:implicitConversions
    , либо локально указать следующую инструкцию импорта:
    import scala.language.implicitConversions
    1
    В Scala 3 неявное преобразование можно также определить с помощью i mplicit def
    , чтобы сохранить совместимость со Scala 2. В будущем этот подход может быть признан устаревшим.
    2
    Методы SAM были описаны в разделе 8.9.

    23 .5 . Неявные преобразования
    1   ...   48   49   50   51   52   53   54   55   ...   64


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