Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 793
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
456 Глава 21 • Гивены
В этом выражении компилятор должен определить тип age
. Поскольку для инициализации age используется значение
42
, которое, как известно ком
пилятору, имеет тип
Int
, для age будет выбран тот же тип. Вы фактически предоставляете выражение, age
, а компилятор определяет его тип,
Int
С контекстными параметрами все наоборот: вы предоставляете тип, а компи
лятор синтезирует выражение, которое его представляет, с учетом доступных givenэкземпляров, и затем использует это выражение автоматически, когда этот тип необходим. Это называется определением выражения (чтобы не путать с определением типа).
Поскольку компилятор ищет givenэкземпляр по типу и зачастую на него вообще не нужно ссылаться, вы можете объявить свое givenзначение ано
нимно. Вместо кода given revIntOrd: Ord[Int] with def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then -1 else 1
given revStringOrd: Ord[String] with def compare(s: String, t: String) = -s.compareTo(t)
можно написать given Ord[Int] with def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then -1 else 1
given Ord[String] with def compare(s: String, t: String) = -s.compareTo(t)
Компилятор автоматически синтезирует имена для этих анонимных given
экземпляров. Вместо второго параметра функции isort будет подставлено это синтезированное значение, которое затем станет доступным внутри функции. Таким образом, если вам нужно, чтобы givenэкземпляр был не
явно предоставлен в качестве контекстных параметров, вам не нужно объ
являть для него выражение.
21 .4 . Параметризованные given-экземпляры в виде классов типов
Вы можете предоставить гивен
Ord[T]
для любого типа
T
, который вам нуж
но сортировать. Например, вы могли бы сделать доступными для сорти
ровки экземпляры класса
Rational
, показанного в листинге 6.5, определив
21 .4 . Параметризованные given-экземпляры в виде классов типов 457
гивен
Ord[Rational]
. Поскольку объекткомпаньон
Rational представляет естественный способ сортировки рациональных чисел, он послужит подхо
дящим местом размещения этого givenэкземпляра:
object Rational:
given rationalOrd: Ord[Rational] with def compare(x: Rational, y: Rational) =
if x.numer * y.denom < x.denom * y.numer then -1
else if x.numer * y.denom > x.denom * y.numer then 1
else 0
Теперь вы можете сортировать списки элементов
Rational
:
isort(List(Rational(4, 5), Rational(1, 2), Rational(2, 3)))
// List(1/2, 2/3, 4/5)
Согласно принципу подстановки Лисков объект можно заменить его под
типом без изменения желаемых свойств программы. Этот принцип лежит в основе отношений между подтипом и супертипом, характерных для объ
ектноориентированного программирования. В последней версии isort
, показанной в листинге 21.2, все выглядит так, что вы можете подставить вместо списка строк список значений типа
Int или
Rational
, и isort про
должит работать так, как ожидалось. Это является признаком того, что
Int
,
Rational и
String могут иметь общий «сортируемый» супертип
1
. Однако это не так. Более того, определение нового супертипа для типов вроде
Int или
String было бы невозможным, так как они являются частью стандартных библиотек Java и Scala.
Предоставление givenэкземпляров
Ord[T]
делает конкретные типы
T
частью множества «типов, доступных для сортировки», несмотря на от
сутствие какоголибо общего сортируемого супертипа. Это множество называется классом типов (typeclass)
2
. Например, на данном этапе класс типов
Ord состоит из трех типов:
Int
,
String и
Rational
; это множество типов
T
, для которых существуют гивены
Ord[T]
. В главе 23 даются до
полнительные примеры класса типов. Поскольку реализация isort
, пред
ставленная в листинге 21.2, принимает контекстный параметр типа
Ord[T]
, это пример специального полиморфизма: isort может сортировать списки
1
Таким сортируемым типом является трейт
Ordered
, описанный в разделах 11.2 и 18.7.
2
В термине «класс типов» слово «класс» употребляется не в объектноориентиро
ванном смысле. Оно означает множество (или класс в привычном смысле этого слова) типов, для которого существуют экземпляры определенного (объектно
ориентированного) класса или трейта.
458 Глава 21 • Гивены определенных типов
T
, для которых существуют гивены
Ord[T]
, и не компи
лируется ни для каких других типов. Специальный полиморфизм с клас
сами типов — это важная и распространенная методика в идиоматическом программировании на Scala.
В стандартной библиотеке Scala есть готовые классы типов для различных целей, таких как определение равенства или определение порядка разме
щения элементов при сортировке. Класс типов
Ord
, который используется в этой главе, является частично переписанной реализацией класса типов math.Ordering из состава Scala. В библиотеке Scala определены экземпляры
Ordering для таких распространенных типов, как
Int и
String
В листинге 21.4 показана версия isort
, в которой используется класс типов
Ordering из Scala. Обратите внимание на то, что у контекстного параметра в этой версии нет имени. Это просто ключевое слово using
, за которым идет тип параметра,
Ordering[T]
. Такие параметры называются анонимными.
Поскольку данный параметр используется неявно только внутри функции
(он неявно передается функциям insert и isort
), Scala не требует, чтобы ему назначили имя.
В качестве еще одного примера специального полиморфизма можно снова взять метод orderedMergeSort из листинга 18.11. Этот метод может сорти
ровать списки любого типа
T
при условии, что
T
— подтип
Ordered[T]
. Это называется полиморфизмом подтипов (или подтипизацией), и, как про
иллюстрировано в разделе 18.7, верхняя граница
Ordered[T]
означает, что orderedMergeSort нельзя использовать для списков с элементами
Int или
String
. Для сравнения: вы можете сортировать списки
Int и
String с по
мощью функции msort из листинга 21.5, так как нужный ей тип
Ordering[T]
формирует отдельную от
T
иерархию. И хотя тип
Int нельзя изменить так, чтобы он наследовал
Ordering[T]
, вы можете определить и предложить ги
вен
Ordering[Int]
Листинг 21.4. Функция сортировки вставками, использующая Ordering def isort[T](xs: List[T])(using Ordering[T]): List[T] =
if xs.isEmpty then Nil else insert(xs.head, isort(xs.tail))
def insert[T](x: T, xs: List[T])
(using ord: Ordering[T]): List[T] =
if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs else xs.head :: insert(x, xs.tail)
Функции isort
(см. листинг 21.4) и msort
(см. листинг 21.5) являются примерами использования контекстного параметра с целью предоставле
21 .5 . Импорт гивенов 459
ния дополнительной информации о типе, явно упомянутом в предыдущем списке параметров. В частности, контекстный параметр типа
Ordering[T]
предоставляет больше информации о типе
T
— в данном случае как сорти
ровать его экземпляры. Тип
T
упоминается в
List[T]
, типе параметра xs
, который фигурирует в предыдущем списке параметров. Поскольку xs всегда нужно явно указывать при любом вызове isort или msort
, тип
T
будет из
вестен на этапе компиляции, что позволит определить, доступен ли гивен типа
Ordering[T]
. Если да, то компилятор автоматически передаст второй список параметров.
Листинг 21.5. Функция сортировки слиянием, использующая Ordering def msort[T](xs: List[T])(using ord: Ordering[T]): List[T] =
def merge(xs: List[T], ys: List[T]): List[T] =
(xs, ys) match case (Nil, _) => ys case (_, Nil) => xs case (x :: xs1, y :: ys1) =>
if ord.lt(x, y) then x :: merge(xs1, ys)
else y :: merge(xs, ys1)
val n = xs.length / 2
if n == 0 then xs else val (ys, zs) = xs.splitAt(n)
merge(msort(ys), msort(zs))
21 .5 . Импорт гивенов
Предоставляя givenзначения в объектекомпаньоне класса, вы делаете их доступными для поиска. Это хороший подход для организации разумного поведения по умолчанию, которое пользователи, скорее всего, хотели бы по
лучать в исполнении своего гивена — например, естественное упорядочение элементов типа. В противном случае гивены рекомендуется размещать в объ
ектаходиночках, которые не будут найдены автоматически; пользователи, которым нужны эти гивены, должны их предварительно импортировать.
Чтобы вам было легче понять, откуда был взят гивен, в Scala предусмотрен специальный синтаксис импортирования.
Допустим, вы хотите определить объект, как показано в листинге 21.6.
В главе 12 вы уже видели, как импортировать все val и def с помощью под
становочного знака. Однако обычный синтаксис импорта с подстановочным знаком не позволяет импортировать гивены:
460 Глава 21 • Гивены
// Импортирует только favoriteColor и favoriteFood import TomsPrefs.*
Это выражение импортирует все члены
TomsPrefs
, кроме гивенов. Чтобы импортировать гивены, можно явно указать их имена.
Листинг 21.6. Объект настроек object TomsPrefs:
val favoriteColor = "blue"
def favoriteFood = "steak"
given prompt: PreferredPrompt =
PreferredPrompt("enjoy> ")
given drink: PreferredDrink =
PreferredDrink("red wine")
given prefPromptOrd: Ordering[PreferredPrompt] with def compare(x: PreferredPrompt, y: PreferredPrompt) =
x.preference.compareTo(y.preference)
given prefDrinkOrd: Ordering[PreferredDrink] with def compare(x: PreferredDrink, y: PreferredDrink) =
x.preference.compareTo(y.preference)
import TomsPrefs.prompt // импортирует prompt
Если вы хотите импортировать все гивены, используйте специальный под-
становочный шаблон:
// импортирует prompt, drink, prefPromptOrd и prefDrinkOrd import TomsPrefs.given
Поскольку имя гивена зачастую не фигурирует явно в исходном коде, а вме
сто этого используется только его тип, данный механизм позволяет импор
тировать гивены по их типу
1
:
// импортирует drink, так как это гивен PreferredDrink import TomsPrefs.{given PreferredDrink}
Если хотите импортировать prefPromptOrd и prefDrinkOrd по типу, можете явно указать их тип после ключевого слова given
:
// импортирует prefPromptOrd и prefDrinkOrd import TomsPrefs.{given Ordering[PreferredPrompt],
given Ordering[PreferredDrink]}
1
Анонимные гивены, описанные в разделе 21.3, не имеют имен, поэтому их можно импортировать только по типу или с помощью подстановочного шаблона.
21 .6 . Правила для контекстных параметров 461
В качестве альтернативы вместо параметра типа можно подставить знак во
проса (
?
), чтобы импортировать сразу оба гивена:
// импортирует prefPromptOrd и prefDrinkOrd import TomsPrefs.{given Ordering[?]}
21 .6 . Правила для контекстных параметров
Контекстными являются параметры, определенные в инструкции using
Компилятору позволено вставлять их для исправления любых ошибок, вы
званных отсутствием списков параметров. Например, если вызов someCall(a)
не проходит проверку типов, компилятор может поменять его на someCall(a)
(b)
, где недостающий список параметров помечен как given
, а b
является гивеном
1
. Это изменение может исправить программу так, чтобы она успеш
но проверяла типы и работала корректно. Если явная передача b
сводится к шаблонному коду, то отсутствие этого параметра в исходном коде можно считать уточнением.
В целом, контекстные параметры подчиняются следующим правилам.
Правило разметки: доступны только определения, помеченные как given.
Ключевое слово given указывает, какие определения компилятор может использовать в качестве контекстных параметров. Вот пример определения гивена:
given amysPrompt: PreferredPrompt = PreferredPrompt("hi> ")
Компилятор заменит greet("Amy")
на greet("Amy")(amysPrompt)
, только если пометить amysPrompt как given
. Таким образом удается избежать путаницы, которая бы возникла, если бы компилятор выбрал случайные значения, ока
завшиеся в области видимости, и вставил их автоматически. Компилятор вы
бирает только среди тех определений, которые вы явно пометили как given
Правило видимости: вставленный given-экземпляр должен быть доступен
в виде единого идентификатора или связан с типом, который фигурирует
в типе параметра. Компилятор Scala рассматривает только видимые given
экземпляры. Чтобы givenэкземпляр был доступным, его необходимо каким
то образом сделать видимым. Более того, за одним исключением, given
экземпляр должен быть видим в лексической области видимости в качестве
единого идентификатора. Компилятор не вставит givenэкземпляр вида
1
Когда компилятор выполняет это переопределение внутри, перед явно переданным параметром не нужно указывать using
462 Глава 21 • Гивены prefslib.AmysPrefs.amysPrompt
. Например, он не развернет greet("Amy")
в greet("Amy")(prefslib.AmysPrefs.amysPrompt)
. Если вы хотите сделать prefslib.AmysPrefs.amysPrompt givenэкземпляром, вам нужно его импор
тировать, что сделает его доступным в качестве единого идентификатора.
После этого у компилятора будет возможность применять его с помощью этого идентификатора в виде greet("Amy")(amysPrompt)
. На самом деле многие библиотеки включают объект
Preamble с рядом полезных given
экземпляров, для доступа к которым достаточно одного выражения import
Preamble.given
1
У правила единого идентификатора есть одно исключение. Если компилятор не находит в лексической области видимости подходящий givenэкземпляр, он в качестве запасного варианта ищет определения givenэкземпляров в объ
ектекомпаньоне всех типов, фигурирующих в типе контекстного параметра.
Например, если вы попытаетесь вызвать метод, не указав явно аргумент для контекстного параметра типа
Ordering[Rational]
, компилятор начнет искать в объектахкомпаньонах
Ordering
,
Rational и их супертипов. В связи с этим такие givenэкземпляры можно упаковывать в объектекомпаньоне любого из этих классов,
Ordering или
Rational
. Но, поскольку
Ordering является ча
стью стандартной библиотеки, лучшим местом размещения givenэкземпляра будет объекткомпаньон
Rational
:
object Rational:
given rationalOrdering: Ordering[Rational] with def compare(x: Rational, y: Rational) =
if x.numer * y.denom < x.denom * y.numer then 1
else if x.numer * y.denom > x.denom * y.numer then 1
else 0
В данном случае givenэкземпляр rationalOrdering считается привязанным к типу
Rational
. Компилятор будет находить его каждый раз, когда ему нужно синтезировать контекстный параметр типа
Ordering[Rational]
. Вам не нужно импортировать givenэкземпляр отдельно в своей программе.
Правило видимости помогает анализировать код с точки зрения моду
лей. При чтении кода вас должны интересовать только те аспекты других файлов, которые либо импортируются, либо явно упоминаются с ис
пользованием полностью определенного имени. Это преимущество важно для givenэкземпляров не меньше, чем для явно написанного кода. Если
1
Импорт
Preamble.given сделает доступными в лексической области видимости синтезированные имена любых анонимных givenэкземпляров, объявленных в
Preamble
, и они будут иметь вид единых экземпляров.
21 .7 . Когда подходит сразу несколько гивенов 463
бы givenэкземпляры действовали на уровне системы, то для того, чтобы разобраться в файле, вам нужно было бы знать о каждом их определении в программе!
Явные определения имеют повышенный приоритет: если в коде прово-
дится проверка типов в том виде, в котором они написаны, компилятор не
пытается использовать given-экземпляры. Компилятор не станет менять код, который уже работает. Из этого правила следует, что неявно предостав
ленные заданные идентификаторы всегда можно заменить явными, исполь
зуя using
; это сделает код более длинным, но вместе с тем его прояснит. Вы можете искать баланс между этими двумя свойствами в каждом отдельном случае. Если код кажется повторяющимся и многословным, контекстные па
раметры могут сделать его менее однообразным. Если код кажется настолько сжатым, что его сложно понять, вы можете явно передавать аргументы для контекстных параметров с помощью using
. Так или иначе, количество кон
текстных параметров, которые позволено подставлять компилятору, — это вопрос стиля.
Назначение имени гивену
Гивены имеют произвольные имена. Имя гивена важно лишь в двух ситуа
циях: если вы хотите явно его записать при передаче, используя ключевое слово using
, и когда вам нужно определить, какие гивены доступны в том или ином месте программы. Чтобы проиллюстрировать вторую ситуацию, представьте, что вы хотите воспользоваться гивеном prefPromptOrd из объ
ектаодиночки
TomsPrefs
, представленного в листинге 21.6, но при этом вам не нравится предпочитаемый Томом напиток, который используется в prefDrinkOrd
. В этом случае вы можете импортировать лишь один из этих гивенов:
import TomsPrefs.prefPromptOrd
В этом примере наличие имен у гивенов оказалось полезным, позволив вам выборочно импортировать только один из них.
21 .7 . Когда подходит сразу несколько гивенов
Иногда бывает так, что в области видимости есть несколько гивенов, каждый из которых является подходящим. Обычно в таких случаях Scala отказывается заменять контекстный параметр. Контекстные параметры
464 Глава 21 • Гивены работают хорошо, когда опущенный список параметров является совер
шенно очевидным и шаблонным. Если же нам подходит сразу несколько гивенов, выбор получается не настолько простым. Рассмотрим в качестве примера листинг 21.7.
Листинг 21.7. Несколько гивенов class PreferredPrompt(val preference: String)
object Greeter:
def greet(name: String)(using prompt: PreferredPrompt) =
println(s"Welcome, $name. The system is ready.")
println(prompt.preference)
object JillsPrefs:
given jillsPrompt: PreferredPrompt =
PreferredPrompt("Your wish> ")
object JoesPrefs:
given joesPrompt: PreferredPrompt =
PreferredPrompt("relax> ")
Гивен
PreferredPrompt предоставляется сразу двумя объектами, показан
ными в листинге 21.7:
JillsPrefs и
JoesPrefs
. Если импортировать и тот и другой, в лексической области видимости будет два разных идентифика
тора, jillsPrompt и joesPrompt
:
scala> import JillsPrefs.jillsPrompt scala> import JoesPrefs.joesPrompt
Теперь, если вы попытаетесь вызвать
Greeter.greet
, компилятор откажется выбирать между двумя подходящими гивенами.
scala> Greeter.greet("Who's there?")
1 |Greeter.greet("Who’s there?")
| ˆ
|ambiguous implicit arguments: both given instance
|joesPrompt in object JoesPrefs and given instance
|jillsPrompt in object JillsPrefs match type
|PreferredPrompt of parameter prompt of method
|greet in object Greeter
Здесь мы имеем дело с настоящей двусмысленностью. Джилл и Джо пред
почитают совершенно разные приглашения командной строки. В данном случае программист должен явно указать, какую из них следует исполь
зовать. В ситуациях, когда может подойти сразу несколько гивенов, ком
пилятор отказывается выбирать — разве что один из них является более
21 .8 . Отладка гивенов
В этом выражении компилятор должен определить тип age
. Поскольку для инициализации age используется значение
42
, которое, как известно ком
пилятору, имеет тип
Int
, для age будет выбран тот же тип. Вы фактически предоставляете выражение, age
, а компилятор определяет его тип,
Int
С контекстными параметрами все наоборот: вы предоставляете тип, а компи
лятор синтезирует выражение, которое его представляет, с учетом доступных givenэкземпляров, и затем использует это выражение автоматически, когда этот тип необходим. Это называется определением выражения (чтобы не путать с определением типа).
Поскольку компилятор ищет givenэкземпляр по типу и зачастую на него вообще не нужно ссылаться, вы можете объявить свое givenзначение ано
нимно. Вместо кода given revIntOrd: Ord[Int] with def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then -1 else 1
given revStringOrd: Ord[String] with def compare(s: String, t: String) = -s.compareTo(t)
можно написать given Ord[Int] with def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then -1 else 1
given Ord[String] with def compare(s: String, t: String) = -s.compareTo(t)
Компилятор автоматически синтезирует имена для этих анонимных given
экземпляров. Вместо второго параметра функции isort будет подставлено это синтезированное значение, которое затем станет доступным внутри функции. Таким образом, если вам нужно, чтобы givenэкземпляр был не
явно предоставлен в качестве контекстных параметров, вам не нужно объ
являть для него выражение.
21 .4 . Параметризованные given-экземпляры в виде классов типов
Вы можете предоставить гивен
Ord[T]
для любого типа
T
, который вам нуж
но сортировать. Например, вы могли бы сделать доступными для сорти
ровки экземпляры класса
Rational
, показанного в листинге 6.5, определив
21 .4 . Параметризованные given-экземпляры в виде классов типов 457
гивен
Ord[Rational]
. Поскольку объекткомпаньон
Rational представляет естественный способ сортировки рациональных чисел, он послужит подхо
дящим местом размещения этого givenэкземпляра:
object Rational:
given rationalOrd: Ord[Rational] with def compare(x: Rational, y: Rational) =
if x.numer * y.denom < x.denom * y.numer then -1
else if x.numer * y.denom > x.denom * y.numer then 1
else 0
Теперь вы можете сортировать списки элементов
Rational
:
isort(List(Rational(4, 5), Rational(1, 2), Rational(2, 3)))
// List(1/2, 2/3, 4/5)
Согласно принципу подстановки Лисков объект можно заменить его под
типом без изменения желаемых свойств программы. Этот принцип лежит в основе отношений между подтипом и супертипом, характерных для объ
ектноориентированного программирования. В последней версии isort
, показанной в листинге 21.2, все выглядит так, что вы можете подставить вместо списка строк список значений типа
Int или
Rational
, и isort про
должит работать так, как ожидалось. Это является признаком того, что
Int
,
Rational и
String могут иметь общий «сортируемый» супертип
1
. Однако это не так. Более того, определение нового супертипа для типов вроде
Int или
String было бы невозможным, так как они являются частью стандартных библиотек Java и Scala.
Предоставление givenэкземпляров
Ord[T]
делает конкретные типы
T
частью множества «типов, доступных для сортировки», несмотря на от
сутствие какоголибо общего сортируемого супертипа. Это множество называется классом типов (typeclass)
2
. Например, на данном этапе класс типов
Ord состоит из трех типов:
Int
,
String и
Rational
; это множество типов
T
, для которых существуют гивены
Ord[T]
. В главе 23 даются до
полнительные примеры класса типов. Поскольку реализация isort
, пред
ставленная в листинге 21.2, принимает контекстный параметр типа
Ord[T]
, это пример специального полиморфизма: isort может сортировать списки
1
Таким сортируемым типом является трейт
Ordered
, описанный в разделах 11.2 и 18.7.
2
В термине «класс типов» слово «класс» употребляется не в объектноориентиро
ванном смысле. Оно означает множество (или класс в привычном смысле этого слова) типов, для которого существуют экземпляры определенного (объектно
ориентированного) класса или трейта.
458 Глава 21 • Гивены определенных типов
T
, для которых существуют гивены
Ord[T]
, и не компи
лируется ни для каких других типов. Специальный полиморфизм с клас
сами типов — это важная и распространенная методика в идиоматическом программировании на Scala.
В стандартной библиотеке Scala есть готовые классы типов для различных целей, таких как определение равенства или определение порядка разме
щения элементов при сортировке. Класс типов
Ord
, который используется в этой главе, является частично переписанной реализацией класса типов math.Ordering из состава Scala. В библиотеке Scala определены экземпляры
Ordering для таких распространенных типов, как
Int и
String
В листинге 21.4 показана версия isort
, в которой используется класс типов
Ordering из Scala. Обратите внимание на то, что у контекстного параметра в этой версии нет имени. Это просто ключевое слово using
, за которым идет тип параметра,
Ordering[T]
. Такие параметры называются анонимными.
Поскольку данный параметр используется неявно только внутри функции
(он неявно передается функциям insert и isort
), Scala не требует, чтобы ему назначили имя.
В качестве еще одного примера специального полиморфизма можно снова взять метод orderedMergeSort из листинга 18.11. Этот метод может сорти
ровать списки любого типа
T
при условии, что
T
— подтип
Ordered[T]
. Это называется полиморфизмом подтипов (или подтипизацией), и, как про
иллюстрировано в разделе 18.7, верхняя граница
Ordered[T]
означает, что orderedMergeSort нельзя использовать для списков с элементами
Int или
String
. Для сравнения: вы можете сортировать списки
Int и
String с по
мощью функции msort из листинга 21.5, так как нужный ей тип
Ordering[T]
формирует отдельную от
T
иерархию. И хотя тип
Int нельзя изменить так, чтобы он наследовал
Ordering[T]
, вы можете определить и предложить ги
вен
Ordering[Int]
Листинг 21.4. Функция сортировки вставками, использующая Ordering def isort[T](xs: List[T])(using Ordering[T]): List[T] =
if xs.isEmpty then Nil else insert(xs.head, isort(xs.tail))
def insert[T](x: T, xs: List[T])
(using ord: Ordering[T]): List[T] =
if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs else xs.head :: insert(x, xs.tail)
Функции isort
(см. листинг 21.4) и msort
(см. листинг 21.5) являются примерами использования контекстного параметра с целью предоставле
21 .5 . Импорт гивенов 459
ния дополнительной информации о типе, явно упомянутом в предыдущем списке параметров. В частности, контекстный параметр типа
Ordering[T]
предоставляет больше информации о типе
T
— в данном случае как сорти
ровать его экземпляры. Тип
T
упоминается в
List[T]
, типе параметра xs
, который фигурирует в предыдущем списке параметров. Поскольку xs всегда нужно явно указывать при любом вызове isort или msort
, тип
T
будет из
вестен на этапе компиляции, что позволит определить, доступен ли гивен типа
Ordering[T]
. Если да, то компилятор автоматически передаст второй список параметров.
Листинг 21.5. Функция сортировки слиянием, использующая Ordering def msort[T](xs: List[T])(using ord: Ordering[T]): List[T] =
def merge(xs: List[T], ys: List[T]): List[T] =
(xs, ys) match case (Nil, _) => ys case (_, Nil) => xs case (x :: xs1, y :: ys1) =>
if ord.lt(x, y) then x :: merge(xs1, ys)
else y :: merge(xs, ys1)
val n = xs.length / 2
if n == 0 then xs else val (ys, zs) = xs.splitAt(n)
merge(msort(ys), msort(zs))
21 .5 . Импорт гивенов
Предоставляя givenзначения в объектекомпаньоне класса, вы делаете их доступными для поиска. Это хороший подход для организации разумного поведения по умолчанию, которое пользователи, скорее всего, хотели бы по
лучать в исполнении своего гивена — например, естественное упорядочение элементов типа. В противном случае гивены рекомендуется размещать в объ
ектаходиночках, которые не будут найдены автоматически; пользователи, которым нужны эти гивены, должны их предварительно импортировать.
Чтобы вам было легче понять, откуда был взят гивен, в Scala предусмотрен специальный синтаксис импортирования.
Допустим, вы хотите определить объект, как показано в листинге 21.6.
В главе 12 вы уже видели, как импортировать все val и def с помощью под
становочного знака. Однако обычный синтаксис импорта с подстановочным знаком не позволяет импортировать гивены:
460 Глава 21 • Гивены
// Импортирует только favoriteColor и favoriteFood import TomsPrefs.*
Это выражение импортирует все члены
TomsPrefs
, кроме гивенов. Чтобы импортировать гивены, можно явно указать их имена.
Листинг 21.6. Объект настроек object TomsPrefs:
val favoriteColor = "blue"
def favoriteFood = "steak"
given prompt: PreferredPrompt =
PreferredPrompt("enjoy> ")
given drink: PreferredDrink =
PreferredDrink("red wine")
given prefPromptOrd: Ordering[PreferredPrompt] with def compare(x: PreferredPrompt, y: PreferredPrompt) =
x.preference.compareTo(y.preference)
given prefDrinkOrd: Ordering[PreferredDrink] with def compare(x: PreferredDrink, y: PreferredDrink) =
x.preference.compareTo(y.preference)
import TomsPrefs.prompt // импортирует prompt
Если вы хотите импортировать все гивены, используйте специальный под-
становочный шаблон:
// импортирует prompt, drink, prefPromptOrd и prefDrinkOrd import TomsPrefs.given
Поскольку имя гивена зачастую не фигурирует явно в исходном коде, а вме
сто этого используется только его тип, данный механизм позволяет импор
тировать гивены по их типу
1
:
// импортирует drink, так как это гивен PreferredDrink import TomsPrefs.{given PreferredDrink}
Если хотите импортировать prefPromptOrd и prefDrinkOrd по типу, можете явно указать их тип после ключевого слова given
:
// импортирует prefPromptOrd и prefDrinkOrd import TomsPrefs.{given Ordering[PreferredPrompt],
given Ordering[PreferredDrink]}
1
Анонимные гивены, описанные в разделе 21.3, не имеют имен, поэтому их можно импортировать только по типу или с помощью подстановочного шаблона.
21 .6 . Правила для контекстных параметров 461
В качестве альтернативы вместо параметра типа можно подставить знак во
проса (
?
), чтобы импортировать сразу оба гивена:
// импортирует prefPromptOrd и prefDrinkOrd import TomsPrefs.{given Ordering[?]}
21 .6 . Правила для контекстных параметров
Контекстными являются параметры, определенные в инструкции using
Компилятору позволено вставлять их для исправления любых ошибок, вы
званных отсутствием списков параметров. Например, если вызов someCall(a)
не проходит проверку типов, компилятор может поменять его на someCall(a)
(b)
, где недостающий список параметров помечен как given
, а b
является гивеном
1
. Это изменение может исправить программу так, чтобы она успеш
но проверяла типы и работала корректно. Если явная передача b
сводится к шаблонному коду, то отсутствие этого параметра в исходном коде можно считать уточнением.
В целом, контекстные параметры подчиняются следующим правилам.
Правило разметки: доступны только определения, помеченные как given.
Ключевое слово given указывает, какие определения компилятор может использовать в качестве контекстных параметров. Вот пример определения гивена:
given amysPrompt: PreferredPrompt = PreferredPrompt("hi> ")
Компилятор заменит greet("Amy")
на greet("Amy")(amysPrompt)
, только если пометить amysPrompt как given
. Таким образом удается избежать путаницы, которая бы возникла, если бы компилятор выбрал случайные значения, ока
завшиеся в области видимости, и вставил их автоматически. Компилятор вы
бирает только среди тех определений, которые вы явно пометили как given
Правило видимости: вставленный given-экземпляр должен быть доступен
в виде единого идентификатора или связан с типом, который фигурирует
в типе параметра. Компилятор Scala рассматривает только видимые given
экземпляры. Чтобы givenэкземпляр был доступным, его необходимо каким
то образом сделать видимым. Более того, за одним исключением, given
экземпляр должен быть видим в лексической области видимости в качестве
единого идентификатора. Компилятор не вставит givenэкземпляр вида
1
Когда компилятор выполняет это переопределение внутри, перед явно переданным параметром не нужно указывать using
462 Глава 21 • Гивены prefslib.AmysPrefs.amysPrompt
. Например, он не развернет greet("Amy")
в greet("Amy")(prefslib.AmysPrefs.amysPrompt)
. Если вы хотите сделать prefslib.AmysPrefs.amysPrompt givenэкземпляром, вам нужно его импор
тировать, что сделает его доступным в качестве единого идентификатора.
После этого у компилятора будет возможность применять его с помощью этого идентификатора в виде greet("Amy")(amysPrompt)
. На самом деле многие библиотеки включают объект
Preamble с рядом полезных given
экземпляров, для доступа к которым достаточно одного выражения import
Preamble.given
1
У правила единого идентификатора есть одно исключение. Если компилятор не находит в лексической области видимости подходящий givenэкземпляр, он в качестве запасного варианта ищет определения givenэкземпляров в объ
ектекомпаньоне всех типов, фигурирующих в типе контекстного параметра.
Например, если вы попытаетесь вызвать метод, не указав явно аргумент для контекстного параметра типа
Ordering[Rational]
, компилятор начнет искать в объектахкомпаньонах
Ordering
,
Rational и их супертипов. В связи с этим такие givenэкземпляры можно упаковывать в объектекомпаньоне любого из этих классов,
Ordering или
Rational
. Но, поскольку
Ordering является ча
стью стандартной библиотеки, лучшим местом размещения givenэкземпляра будет объекткомпаньон
Rational
:
object Rational:
given rationalOrdering: Ordering[Rational] with def compare(x: Rational, y: Rational) =
if x.numer * y.denom < x.denom * y.numer then 1
else if x.numer * y.denom > x.denom * y.numer then 1
else 0
В данном случае givenэкземпляр rationalOrdering считается привязанным к типу
Rational
. Компилятор будет находить его каждый раз, когда ему нужно синтезировать контекстный параметр типа
Ordering[Rational]
. Вам не нужно импортировать givenэкземпляр отдельно в своей программе.
Правило видимости помогает анализировать код с точки зрения моду
лей. При чтении кода вас должны интересовать только те аспекты других файлов, которые либо импортируются, либо явно упоминаются с ис
пользованием полностью определенного имени. Это преимущество важно для givenэкземпляров не меньше, чем для явно написанного кода. Если
1
Импорт
Preamble.given сделает доступными в лексической области видимости синтезированные имена любых анонимных givenэкземпляров, объявленных в
Preamble
, и они будут иметь вид единых экземпляров.
21 .7 . Когда подходит сразу несколько гивенов 463
бы givenэкземпляры действовали на уровне системы, то для того, чтобы разобраться в файле, вам нужно было бы знать о каждом их определении в программе!
Явные определения имеют повышенный приоритет: если в коде прово-
дится проверка типов в том виде, в котором они написаны, компилятор не
пытается использовать given-экземпляры. Компилятор не станет менять код, который уже работает. Из этого правила следует, что неявно предостав
ленные заданные идентификаторы всегда можно заменить явными, исполь
зуя using
; это сделает код более длинным, но вместе с тем его прояснит. Вы можете искать баланс между этими двумя свойствами в каждом отдельном случае. Если код кажется повторяющимся и многословным, контекстные па
раметры могут сделать его менее однообразным. Если код кажется настолько сжатым, что его сложно понять, вы можете явно передавать аргументы для контекстных параметров с помощью using
. Так или иначе, количество кон
текстных параметров, которые позволено подставлять компилятору, — это вопрос стиля.
Назначение имени гивену
Гивены имеют произвольные имена. Имя гивена важно лишь в двух ситуа
циях: если вы хотите явно его записать при передаче, используя ключевое слово using
, и когда вам нужно определить, какие гивены доступны в том или ином месте программы. Чтобы проиллюстрировать вторую ситуацию, представьте, что вы хотите воспользоваться гивеном prefPromptOrd из объ
ектаодиночки
TomsPrefs
, представленного в листинге 21.6, но при этом вам не нравится предпочитаемый Томом напиток, который используется в prefDrinkOrd
. В этом случае вы можете импортировать лишь один из этих гивенов:
import TomsPrefs.prefPromptOrd
В этом примере наличие имен у гивенов оказалось полезным, позволив вам выборочно импортировать только один из них.
21 .7 . Когда подходит сразу несколько гивенов
Иногда бывает так, что в области видимости есть несколько гивенов, каждый из которых является подходящим. Обычно в таких случаях Scala отказывается заменять контекстный параметр. Контекстные параметры
464 Глава 21 • Гивены работают хорошо, когда опущенный список параметров является совер
шенно очевидным и шаблонным. Если же нам подходит сразу несколько гивенов, выбор получается не настолько простым. Рассмотрим в качестве примера листинг 21.7.
Листинг 21.7. Несколько гивенов class PreferredPrompt(val preference: String)
object Greeter:
def greet(name: String)(using prompt: PreferredPrompt) =
println(s"Welcome, $name. The system is ready.")
println(prompt.preference)
object JillsPrefs:
given jillsPrompt: PreferredPrompt =
PreferredPrompt("Your wish> ")
object JoesPrefs:
given joesPrompt: PreferredPrompt =
PreferredPrompt("relax> ")
Гивен
PreferredPrompt предоставляется сразу двумя объектами, показан
ными в листинге 21.7:
JillsPrefs и
JoesPrefs
. Если импортировать и тот и другой, в лексической области видимости будет два разных идентифика
тора, jillsPrompt и joesPrompt
:
scala> import JillsPrefs.jillsPrompt scala> import JoesPrefs.joesPrompt
Теперь, если вы попытаетесь вызвать
Greeter.greet
, компилятор откажется выбирать между двумя подходящими гивенами.
scala> Greeter.greet("Who's there?")
1 |Greeter.greet("Who’s there?")
| ˆ
|ambiguous implicit arguments: both given instance
|joesPrompt in object JoesPrefs and given instance
|jillsPrompt in object JillsPrefs match type
|PreferredPrompt of parameter prompt of method
|greet in object Greeter
Здесь мы имеем дело с настоящей двусмысленностью. Джилл и Джо пред
почитают совершенно разные приглашения командной строки. В данном случае программист должен явно указать, какую из них следует исполь
зовать. В ситуациях, когда может подойти сразу несколько гивенов, ком
пилятор отказывается выбирать — разве что один из них является более
21 .8 . Отладка гивенов
1 ... 44 45 46 47 48 49 50 51 ... 64