Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 792
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
21
Гивены
Поведение функции зачастую зависит от контекста, в котором она вы
зывается. Например, она может менять свое поведение в зависимости от контекстных данных, таких как системные свойства, разрешения безопас
ности, аутентифицированный пользователь, транзакция базы данных или заданное время ожидания. Функция также может зависеть от контекст
ного поведения — алгоритма, имеющего смысл в контексте, в котором эта функция вызывается. Например, функция сортировки может зависеть от алгоритма сравнения, который определяет, как упорядочивать сортиру
емые элементы. Разные контексты могут требовать разных алгоритмов сравнения.
Для предоставления функции такой контекстной информации и поведения существует множество приемов, однако в функциональном программиро
вании решение традиционно сводится к одному: передавать все в качестве параметров. И хотя это вполне рабочий подход, у него есть недостаток: чем больше вы передаете функции данных и алгоритмов, тем более общей и по
лезной она становится, но при этом увеличивается количество аргументов, которые нужно указывать при каждом ее вызове. К сожалению, передача всего в виде параметров может быстро сделать ваш код повторяющимся и шаблонным.
В этой главе описываются контекстные параметры, которые часто называют гивенами (given). Они позволяют вам опускать некоторые аргументы при вызове функций, давая возможность компилятору подставить подходящие значения для каждого контекста в зависимости от типа.
448 Глава 21 • Гивены
21 .1 . Как это работает
Компилятор иногда меняет someCall(a)
на someCall(a)(b)
или
SomeClass(a)
на new
SomeClass(a)(b)
, добавляя тем самым один или несколько недостающих списков параметров, чтобы сделать вызов функции завершенным. Предостав
ляются не отдельные параметры, а целые их каррированные списки. Напри
мер, если недостающий список параметров someCall состоит из трех значений, компилятор может подставить someCall(a)(b,
c,
d)
вместо someCall(a)
. В этом случае подставленные идентификаторы, такие как b
, c
и d
в
(b,
c,
d)
, должны быть помечены как заданные (given) в месте их определения, а сам список параметров в определении someCall или someClass должен начинаться с using
Представьте, к примеру, что у вас есть множество методов, принимающих приглашение командной строки (например,
"$
"
или ">
"
), которое предпо
читает текущий пользователь. Вы можете сократить количество шаблонного кода, сделав запрос контекстным параметром. Для начала нужно создать спе
циальный тип, инкапсулирующий строку с предпочитаемым приглашением:
class PreferredPrompt(val preference: String)
Далее нужно отредактировать каждый метод, который принимает приглаше
ние, заменив параметр отдельным списком параметров с ключевым словом using
. Например, у следующего объекта
Greeter есть метод greet
, который принимает
PreferredPrompt в качестве контекстного параметра:
object Greeter:
def greet(name: String)(using prompt: PreferredPrompt) =
println(s"Welcome, $name. The system is ready.")
println(prompt.preference)
Чтобы компилятор мог неявно подставлять контекстный параметр, вы долж
ны определить givenэкземпляр ожидаемого типа (в данном случае
Prefer- redPrompt
) с использованием ключевого слова given
. Это можно сделать в объекте настроек, как показано далее:
object JillsPrefs:
given prompt: PreferredPrompt =
PreferredPrompt("Your wish> ")
Теперь компилятор может автоматически подставлять этот экземпляр
Pre- fer redPrompt
, но только при условии, что тот находится в области видимости:
scala> Greeter.greet("Jill")
1 |Greeter.greet("Jill")
| ˆ
|no implicit argument of type PreferredPrompt was found
|for parameter prompt of method greet in object Greeter
21 .1 . Как это работает 449
Если сделать этот объект доступным, он будет использоваться для предо
ставления недостающего списка параметров:
scala> import JillsPrefs.prompt scala> Greeter.greet("Jill")
Welcome, Jill. The system is ready.
Your wish>
Поскольку приглашение командной строки объявлено в качестве контекст
ного параметра, оно не скомпилируется, если вы попытаетесь передать ар
гумент как обычно, явным образом:
scala> Greeter.greet("Jill")(JillsPrefs.prompt)
1 |Greeter.greet("Jill")(JillsPrefs.prompt)
|ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
|method greet in object Greeter does not take more
|parameters
Вместо этого вам следует указать, что вы хотите явно подставить контекст
ный параметр, используя в момент вызова ключевое слово using
, как по
казано ниже:
scala> Greeter.greet("Jill")(using JillsPrefs.prompt)
Welcome, Jill. The system is ready.
Your wish>
Обратите внимание на то, что ключевое слово using относится не к от
дельным параметрам, а ко всему списку. В листинге 21.1 показан пример, в котором второй список параметров метода greet из объекта
Greeter
(ко
торый опять же помечен как using
) состоит из двух элементов: prompt
(типа
PreferredPrompt
) и drink
(типа
PreferredDrink
).
Листинг 21.1. Неявный список с несколькими параметрами class PreferredPrompt(val preference: String)
class PreferredDrink(val preference: String)
object Greeter:
def greet(name: String)(using prompt: PreferredPrompt,
drink: PreferredDrink) =
println(s"Welcome, $name. The system is ready.")
print("But while you work, ")
println(s"why not enjoy a cup of ${drink.preference}?")
println(prompt.preference)
object JoesPrefs:
given prompt: PreferredPrompt =
PreferredPrompt("relax> ")
given drink: PreferredDrink =
PreferredDrink("tea")
450 Глава 21 • Гивены
Объектодиночка объявляет два givenэкземпляра: prompt типа
Prefer- redPrompt и drink типа
PreferredDrink
. Но, как и прежде, они не будут ис
пользоваться для подстановки недостающего списка параметров в greet
, если они находятся вне области видимости:
scala> Greeter.greet("Joe")
1 |Greeter.greet("Joe")
| ˆ
|no implicit argument of type PreferredPrompt was found
|for parameter prompt of method greet in object Greeter
Вы можете сделать оба givenэкземпляра из листинга 21.1 доступными с по
мощью инструкции import
:
scala> import JoesPrefs.{prompt, drink}
Поскольку и prompt
, и drink теперь находятся в области видимости в каче
стве отдельных идентификаторов, вы можете использовать их для явного предоставления последнего списка параметров:
scala> Greeter.greet("Joe")(using prompt, drink)
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
relax>
И поскольку ваши контекстные параметры теперь удовлетворяют всем правилам, вы можете также позволить компилятору Scala подставить prompt и drink автоматически, целиком опустив весь список параметров:
scala> Greeter.greet("Joe")
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
relax>
Одной из особенностей предыдущих примеров является то, что мы не ис
пользовали
String в качестве типа для prompt или drink
, хотя в итоге оба этих значения предоставили именно
String через свои поля preference
. По
скольку компилятор выбирает контекстные параметры путем сопоставле
ния типов параметров и типов givenэкземпляров, контекстные параметры должны иметь достаточно редкие, или особенные типы, которые делают случайное совпадение маловероятным. Например, типы
PreferredPrompt и
PreferredDrink в листинге 21.1 были определены исключительно для контекстных параметров. В результате givenэкземпляры этих типов, ско
рее всего, не будут существовать, если только они не предназначены для использования в качестве контекстных параметров для таких методов, как greet
21 .2 . Параметризованные given-типы 451
21 .2 . Параметризованные given-типы
Контекстные параметры, наверное, чаще всего используются для предо
ставления информации о типе, явно указанном в предыдущем списке параметров, подобно классам типов (type class) в Haskell. Это важный ме
ханизм достижения специального полиморфизма (ad hoc polymorphism) при написании функций в Scala: ваши функции можно применять к значениям с подходящими типами, но при использовании для значений любых других типов код не скомпилируется. Представьте, к примеру, двухстрочную со
ртировку вставками, показанную в листинге 14.1. Это определение isort работает только для списка целых чисел. Чтобы сортировать списки других типов, вам нужно сделать тип аргумента isort более общим. Для этого первым делом можно ввести параметр типа,
T
, и подставить его вместо
Int в параметре типа
List
:
// Не компилируется def isort[T](xs: List[T]): List[T] =
if xs.isEmpty then Nil else insert(xs.head, isort(xs.tail))
def insert[T](x: T, xs: List[T]): List[T] =
if xs.isEmpty || x <= xs.head then x :: xs else xs.head :: insert(x, xs.tail)
Но, попытавшись скомпилировать isort после внесения этого изменения, вы получите от компилятора следующее сообщение:
6 | if xs.isEmpty || x <= xs.head then x :: xs
| ˆˆˆˆ
| value <= is not a member of T, ...
Если класс
Int определяет метод
<=
, устанавливающий, является ли одно целое число меньше или равно другому, то для других типов могут потре
боваться альтернативные стратегии сравнения или же их и вовсе нельзя сравнивать. Чтобы метод isort мог работать со списками, элементы которых имеют типы, отличные от
Int
, ему нужно предоставить чуть больше инфор
мации, позволяющей определить способ сравнения двух элементов.
Чтобы решить эту проблему, методу isort можно передать функцию «мень
ше или равно», подходящую для типа
List
. Эта функция должна принимать два экземпляра
T
и возвращать значение
Boolean
, указывающее на то, явля
ется ли первый экземпляр
T
меньше или равным второму:
def isort[T](xs: List[T])(lteq: (T, T) => Boolean): List[T] =
if xs.isEmpty then Nil
452 Глава 21 • Гивены else insert(xs.head, isort(xs.tail)(lteq))(lteq)
def insert[T](x: T, xs: List[T])
(lteq: (T, T) => Boolean): List[T] =
if xs.isEmpty || lteq(x, xs.head) then x :: xs else xs.head :: insert(x, xs.tail)(lteq)
Теперь вместо
<=
вспомогательная функция insert использует параметр lteq для сравнения двух элементов во время сортировки. Это позволяет сортиро
вать список любого типа
T
, главное — предоставить методу isort функцию сравнения, которая подходит для
T
. Например, с помощью этой версии isort можно сортировать списки
Int
,
String и класса
Rational
, представленного в листинге 6.5:
isort(List(4, -10, 10))((x: Int, y: Int) => x <= y)
// List(-10, 4, 10)
isort(List("cherry", "blackberry", "apple", "pear"))
((x: String, y: String) => x.compareTo(y) <= 0)
// List(apple, blackberry, cherry, pear)
isort(List(Rational(7, 8), Rational(5, 6), Rational(1, 2)))
((x: Rational, y: Rational) =>
x.numer * y.denom <= x.denom * y.numer)
// List(1/2, 5/6, 7/8)
Как уже описывалось в разделе 14.10, компилятор Scala последовательно определяет типы параметров в каждом списке, продвигаясь слева направо.
Таким образом, он может определить типы x
и y
, указанные во втором спи
ске параметров, исходя из типа элемента
T
экземпляра
List[T]
, переданного в первом списке параметров:
isort(List(4, -10, 10))((x, y) => x <= y)
// List(-10, 4, 10)
isort(List("cherry", "blackberry", "apple", "pear"))
((x, y) => x.compareTo(y) < 1)
// List(apple, blackberry, cherry, pear)
isort(List(Rational(7, 8), Rational(5, 6), Rational(1, 2)))
((x, y) => x.numer * y.denom <= x.denom * y.numer)
// List(1/2, 5/6, 7/8)
Теперь функция isort полезна в более общем смысле, однако за эту обоб
щенность приходится платить потерей лаконичности: при каждом вызове необходимо указывать функцию сравнения, которую определение isort теперь должно передавать каждому рекурсивному вызову isort
, а также
21 .2 . Параметризованные given-типы 453
каждому вызову вспомогательной функции insert
. Эта версия isort больше не является простым выражением сортировки, как прежде.
Вы можете сделать более лаконичной как реализацию метода isort
, так и его вызовы, если оформите функцию сравнения в виде контекстного параметра.
Вы могли бы использовать контекстный параметр
(Int,
Int)
=>
Boolean
, но этот тип слишком общий, что делает его не самым оптимальным решением.
У вашей программы, к примеру, может быть много функций, которые при
нимают целочисленные параметры и возвращают логическое значение, но при этом не имеют ничего общего с сортировкой. Поскольку поиск given
значений происходит по типу, вы должны позаботиться о том, чтобы тип вашего givenэкземпляра выражал его назначение.
Определение типов с определенным назначением, таким как сортировка, обычно является хорошим решением, но, как упоминалось ранее, некоторые типы становятся особенно полезными при использовании контекстных па
раметров. Помимо гарантии использования подходящего givenэкземпляра, тщательно определенные типы могут помочь вам более ясно выразить ваши намерения. Это позволяет вам развивать ваши программы постепенно, рас
ширяя типы за счет дополнительного функционала, но не нарушая при этом существующие между ними контракты. Вы можете определить тип, чтобы выбрать, в каком порядке должны размещаться два элемента:
trait Ord[T]:
def compare(x: T, y: T): Int def lteq(x: T, y: T): Boolean = compare(x, y) < 1
Этот трейт реализует функцию «меньше или равно» в виде более общего абстрактного метода compare
. Контракт этого метода состоит в том, что он возвращает
0
, если два параметра равны, положительное целое число, если первый параметр больше второго, и отрицательное целое число, если второй параметр больше первого. Теперь, имея это определение, вы можете указать стратегию сравнения для
T
, используя
Ord[T]
в качестве контекстного пара
метра, как показано в листинге 21.2.
Листинг 21.2. Контекстные параметры, передаваемые с помощью using def isort[T](xs: List[T])(using ord: Ord[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: Ord[T]): List[T] =
if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs else xs.head :: insert(x, xs.tail)
454 Глава 21 • Гивены
Как уже описывалось ранее, чтобы параметры можно было передавать не
явно, перед ними нужно указать using
. После этого вам больше не нужно предоставлять эти параметры вручную при вызове функции: если доступно значение подходящего типа, компилятор возьмет его и передаст вашей функ
ции. Чтобы сделать значение givenэкземпляром типа, его следует объявить с помощью ключевого слова given
Хорошим местом для размещения givenэкземпляров, представляющих
«естественный» вариант использования типа, такой как сортировка целых чисел в порядке возрастания, является объекткомпаньон «вовлеченного» типа. Например, естественный givenэкземпляр
Ord[Int]
можно было бы разместить в объектекомпаньоне для
Ord или
Int
— двух типов, «фигури
рующих» в
Ord[Int]
. Если компилятор не найдет givenэкземпляр
Ord[Int]
в лексической области видимости, он проведет дополнительный поиск в этих двух объектахкомпаньонах. Поскольку компаньон
Int не подлежит изменению, лучшим выбором является компаньон
Ord
:
object Ord:
// (Пока что не является устоявшимся решением)
given intOrd: Ord[Int] =
new Ord[Int]:
def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then 1 else 1
Все примеры givenобъявлений, показанные до сих пор в этой главе, на
зываются псевдонимными (alias). Имя по левую сторону от знака равенства является псевдонимом значения, указанного справа. Поскольку при объяв
лении псевдонимного givenэкземпляра справа от знака равенства зачастую определяют анонимный экземпляр трейта или класса, Scala предлагает со
кращенный синтаксис, который позволяет подставить вместо знака равен
ства и «имени нового класса» ключевое слово with
1
. В листинге 21.3 показано более компактное определение intOrd
Листинг 21.3. Объявление естественного given-экземпляра в компаньоне object Ord:
// Общепринятое решение given intOrd: Ord[Int] with def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then 1 else 1 1
Этот способ использования with отличается от того, который был описан в главе 11 и предназначался для объединения трейтов.
21 .3 . Анонимные given-экземпляры 455
Теперь, когда в объекте
Ord имеется givenэкземпляр
Ord[Int]
, сортировка с использованием isort снова становится лаконичной:
isort(List(10, 2, -10))
// List(-10, 2, 10)
Если опустить второй параметр isort
, компилятор начнет искать для него заданное значение с учетом его типа. Если речь идет о сортировке значе
ний
Int
, этим типом будет
Ord[Int]
. Вначале компилятор поищет given
экземпляр
Ord[Int]
в лексической области видимости, и, если его там не обнаружится, он пройдется по объектамкомпаньонам вовлеченных типов
Ord и
Int
. Поскольку в листинге 21.3 заданное значение intOrd имеет явно указанный тип, компилятор подставит intOrd вместо недостающего списка параметров.
Для сортировки строк достаточно предоставить givenэкземпляр для пара
метра, предназначенного для сравнения строковых значений:
// Добавлено в объект Ord given stringOrd: Ord[String] with def compare(s: String, t: String) = s.compareTo(t)
Теперь, когда в компаньоне
Ord определен givenэкземпляр
Ord[String]
, вы можете использовать isort для сортировки списков строк:
isort(List("mango", "jackfruit", "durian"))
// List(durian, jackfruit, mango)
Если заданное объявление не принимает параметризованные значения, givenэкземпляр инициализируется при первом к нему обращении, что похо
же на ленивые значения. Эта инициализация проводится потокобезопасным образом. Если же givenэкземпляр принимает параметры, он создается зано
во при каждом обращении, подобно тому как ведет себя def
. Действительно, компилятор Scala преобразует givenэкземпляры в val или def
, дополнитель
но делая их доступными для параметров using
21 .3 . Анонимные given-экземпляры
Заданное объявление можно считать частным случаем ленивого val или def
, однако оно обладает одной важной особенностью. При объявлении val
, к примеру, нужно задать выражение, указывающее на значение val
:
val age = 42
Гивены
Поведение функции зачастую зависит от контекста, в котором она вы
зывается. Например, она может менять свое поведение в зависимости от контекстных данных, таких как системные свойства, разрешения безопас
ности, аутентифицированный пользователь, транзакция базы данных или заданное время ожидания. Функция также может зависеть от контекст
ного поведения — алгоритма, имеющего смысл в контексте, в котором эта функция вызывается. Например, функция сортировки может зависеть от алгоритма сравнения, который определяет, как упорядочивать сортиру
емые элементы. Разные контексты могут требовать разных алгоритмов сравнения.
Для предоставления функции такой контекстной информации и поведения существует множество приемов, однако в функциональном программиро
вании решение традиционно сводится к одному: передавать все в качестве параметров. И хотя это вполне рабочий подход, у него есть недостаток: чем больше вы передаете функции данных и алгоритмов, тем более общей и по
лезной она становится, но при этом увеличивается количество аргументов, которые нужно указывать при каждом ее вызове. К сожалению, передача всего в виде параметров может быстро сделать ваш код повторяющимся и шаблонным.
В этой главе описываются контекстные параметры, которые часто называют гивенами (given). Они позволяют вам опускать некоторые аргументы при вызове функций, давая возможность компилятору подставить подходящие значения для каждого контекста в зависимости от типа.
448 Глава 21 • Гивены
21 .1 . Как это работает
Компилятор иногда меняет someCall(a)
на someCall(a)(b)
или
SomeClass(a)
на new
SomeClass(a)(b)
, добавляя тем самым один или несколько недостающих списков параметров, чтобы сделать вызов функции завершенным. Предостав
ляются не отдельные параметры, а целые их каррированные списки. Напри
мер, если недостающий список параметров someCall состоит из трех значений, компилятор может подставить someCall(a)(b,
c,
d)
вместо someCall(a)
. В этом случае подставленные идентификаторы, такие как b
, c
и d
в
(b,
c,
d)
, должны быть помечены как заданные (given) в месте их определения, а сам список параметров в определении someCall или someClass должен начинаться с using
Представьте, к примеру, что у вас есть множество методов, принимающих приглашение командной строки (например,
"$
"
или ">
"
), которое предпо
читает текущий пользователь. Вы можете сократить количество шаблонного кода, сделав запрос контекстным параметром. Для начала нужно создать спе
циальный тип, инкапсулирующий строку с предпочитаемым приглашением:
class PreferredPrompt(val preference: String)
Далее нужно отредактировать каждый метод, который принимает приглаше
ние, заменив параметр отдельным списком параметров с ключевым словом using
. Например, у следующего объекта
Greeter есть метод greet
, который принимает
PreferredPrompt в качестве контекстного параметра:
object Greeter:
def greet(name: String)(using prompt: PreferredPrompt) =
println(s"Welcome, $name. The system is ready.")
println(prompt.preference)
Чтобы компилятор мог неявно подставлять контекстный параметр, вы долж
ны определить givenэкземпляр ожидаемого типа (в данном случае
Prefer- redPrompt
) с использованием ключевого слова given
. Это можно сделать в объекте настроек, как показано далее:
object JillsPrefs:
given prompt: PreferredPrompt =
PreferredPrompt("Your wish> ")
Теперь компилятор может автоматически подставлять этот экземпляр
Pre- fer redPrompt
, но только при условии, что тот находится в области видимости:
scala> Greeter.greet("Jill")
1 |Greeter.greet("Jill")
| ˆ
|no implicit argument of type PreferredPrompt was found
|for parameter prompt of method greet in object Greeter
21 .1 . Как это работает 449
Если сделать этот объект доступным, он будет использоваться для предо
ставления недостающего списка параметров:
scala> import JillsPrefs.prompt scala> Greeter.greet("Jill")
Welcome, Jill. The system is ready.
Your wish>
Поскольку приглашение командной строки объявлено в качестве контекст
ного параметра, оно не скомпилируется, если вы попытаетесь передать ар
гумент как обычно, явным образом:
scala> Greeter.greet("Jill")(JillsPrefs.prompt)
1 |Greeter.greet("Jill")(JillsPrefs.prompt)
|ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
|method greet in object Greeter does not take more
|parameters
Вместо этого вам следует указать, что вы хотите явно подставить контекст
ный параметр, используя в момент вызова ключевое слово using
, как по
казано ниже:
scala> Greeter.greet("Jill")(using JillsPrefs.prompt)
Welcome, Jill. The system is ready.
Your wish>
Обратите внимание на то, что ключевое слово using относится не к от
дельным параметрам, а ко всему списку. В листинге 21.1 показан пример, в котором второй список параметров метода greet из объекта
Greeter
(ко
торый опять же помечен как using
) состоит из двух элементов: prompt
(типа
PreferredPrompt
) и drink
(типа
PreferredDrink
).
Листинг 21.1. Неявный список с несколькими параметрами class PreferredPrompt(val preference: String)
class PreferredDrink(val preference: String)
object Greeter:
def greet(name: String)(using prompt: PreferredPrompt,
drink: PreferredDrink) =
println(s"Welcome, $name. The system is ready.")
print("But while you work, ")
println(s"why not enjoy a cup of ${drink.preference}?")
println(prompt.preference)
object JoesPrefs:
given prompt: PreferredPrompt =
PreferredPrompt("relax> ")
given drink: PreferredDrink =
PreferredDrink("tea")
450 Глава 21 • Гивены
Объектодиночка объявляет два givenэкземпляра: prompt типа
Prefer- redPrompt и drink типа
PreferredDrink
. Но, как и прежде, они не будут ис
пользоваться для подстановки недостающего списка параметров в greet
, если они находятся вне области видимости:
scala> Greeter.greet("Joe")
1 |Greeter.greet("Joe")
| ˆ
|no implicit argument of type PreferredPrompt was found
|for parameter prompt of method greet in object Greeter
Вы можете сделать оба givenэкземпляра из листинга 21.1 доступными с по
мощью инструкции import
:
scala> import JoesPrefs.{prompt, drink}
Поскольку и prompt
, и drink теперь находятся в области видимости в каче
стве отдельных идентификаторов, вы можете использовать их для явного предоставления последнего списка параметров:
scala> Greeter.greet("Joe")(using prompt, drink)
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
relax>
И поскольку ваши контекстные параметры теперь удовлетворяют всем правилам, вы можете также позволить компилятору Scala подставить prompt и drink автоматически, целиком опустив весь список параметров:
scala> Greeter.greet("Joe")
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
relax>
Одной из особенностей предыдущих примеров является то, что мы не ис
пользовали
String в качестве типа для prompt или drink
, хотя в итоге оба этих значения предоставили именно
String через свои поля preference
. По
скольку компилятор выбирает контекстные параметры путем сопоставле
ния типов параметров и типов givenэкземпляров, контекстные параметры должны иметь достаточно редкие, или особенные типы, которые делают случайное совпадение маловероятным. Например, типы
PreferredPrompt и
PreferredDrink в листинге 21.1 были определены исключительно для контекстных параметров. В результате givenэкземпляры этих типов, ско
рее всего, не будут существовать, если только они не предназначены для использования в качестве контекстных параметров для таких методов, как greet
21 .2 . Параметризованные given-типы 451
21 .2 . Параметризованные given-типы
Контекстные параметры, наверное, чаще всего используются для предо
ставления информации о типе, явно указанном в предыдущем списке параметров, подобно классам типов (type class) в Haskell. Это важный ме
ханизм достижения специального полиморфизма (ad hoc polymorphism) при написании функций в Scala: ваши функции можно применять к значениям с подходящими типами, но при использовании для значений любых других типов код не скомпилируется. Представьте, к примеру, двухстрочную со
ртировку вставками, показанную в листинге 14.1. Это определение isort работает только для списка целых чисел. Чтобы сортировать списки других типов, вам нужно сделать тип аргумента isort более общим. Для этого первым делом можно ввести параметр типа,
T
, и подставить его вместо
Int в параметре типа
List
:
// Не компилируется def isort[T](xs: List[T]): List[T] =
if xs.isEmpty then Nil else insert(xs.head, isort(xs.tail))
def insert[T](x: T, xs: List[T]): List[T] =
if xs.isEmpty || x <= xs.head then x :: xs else xs.head :: insert(x, xs.tail)
Но, попытавшись скомпилировать isort после внесения этого изменения, вы получите от компилятора следующее сообщение:
6 | if xs.isEmpty || x <= xs.head then x :: xs
| ˆˆˆˆ
| value <= is not a member of T, ...
Если класс
Int определяет метод
<=
, устанавливающий, является ли одно целое число меньше или равно другому, то для других типов могут потре
боваться альтернативные стратегии сравнения или же их и вовсе нельзя сравнивать. Чтобы метод isort мог работать со списками, элементы которых имеют типы, отличные от
Int
, ему нужно предоставить чуть больше инфор
мации, позволяющей определить способ сравнения двух элементов.
Чтобы решить эту проблему, методу isort можно передать функцию «мень
ше или равно», подходящую для типа
List
. Эта функция должна принимать два экземпляра
T
и возвращать значение
Boolean
, указывающее на то, явля
ется ли первый экземпляр
T
меньше или равным второму:
def isort[T](xs: List[T])(lteq: (T, T) => Boolean): List[T] =
if xs.isEmpty then Nil
452 Глава 21 • Гивены else insert(xs.head, isort(xs.tail)(lteq))(lteq)
def insert[T](x: T, xs: List[T])
(lteq: (T, T) => Boolean): List[T] =
if xs.isEmpty || lteq(x, xs.head) then x :: xs else xs.head :: insert(x, xs.tail)(lteq)
Теперь вместо
<=
вспомогательная функция insert использует параметр lteq для сравнения двух элементов во время сортировки. Это позволяет сортиро
вать список любого типа
T
, главное — предоставить методу isort функцию сравнения, которая подходит для
T
. Например, с помощью этой версии isort можно сортировать списки
Int
,
String и класса
Rational
, представленного в листинге 6.5:
isort(List(4, -10, 10))((x: Int, y: Int) => x <= y)
// List(-10, 4, 10)
isort(List("cherry", "blackberry", "apple", "pear"))
((x: String, y: String) => x.compareTo(y) <= 0)
// List(apple, blackberry, cherry, pear)
isort(List(Rational(7, 8), Rational(5, 6), Rational(1, 2)))
((x: Rational, y: Rational) =>
x.numer * y.denom <= x.denom * y.numer)
// List(1/2, 5/6, 7/8)
Как уже описывалось в разделе 14.10, компилятор Scala последовательно определяет типы параметров в каждом списке, продвигаясь слева направо.
Таким образом, он может определить типы x
и y
, указанные во втором спи
ске параметров, исходя из типа элемента
T
экземпляра
List[T]
, переданного в первом списке параметров:
isort(List(4, -10, 10))((x, y) => x <= y)
// List(-10, 4, 10)
isort(List("cherry", "blackberry", "apple", "pear"))
((x, y) => x.compareTo(y) < 1)
// List(apple, blackberry, cherry, pear)
isort(List(Rational(7, 8), Rational(5, 6), Rational(1, 2)))
((x, y) => x.numer * y.denom <= x.denom * y.numer)
// List(1/2, 5/6, 7/8)
Теперь функция isort полезна в более общем смысле, однако за эту обоб
щенность приходится платить потерей лаконичности: при каждом вызове необходимо указывать функцию сравнения, которую определение isort теперь должно передавать каждому рекурсивному вызову isort
, а также
21 .2 . Параметризованные given-типы 453
каждому вызову вспомогательной функции insert
. Эта версия isort больше не является простым выражением сортировки, как прежде.
Вы можете сделать более лаконичной как реализацию метода isort
, так и его вызовы, если оформите функцию сравнения в виде контекстного параметра.
Вы могли бы использовать контекстный параметр
(Int,
Int)
=>
Boolean
, но этот тип слишком общий, что делает его не самым оптимальным решением.
У вашей программы, к примеру, может быть много функций, которые при
нимают целочисленные параметры и возвращают логическое значение, но при этом не имеют ничего общего с сортировкой. Поскольку поиск given
значений происходит по типу, вы должны позаботиться о том, чтобы тип вашего givenэкземпляра выражал его назначение.
Определение типов с определенным назначением, таким как сортировка, обычно является хорошим решением, но, как упоминалось ранее, некоторые типы становятся особенно полезными при использовании контекстных па
раметров. Помимо гарантии использования подходящего givenэкземпляра, тщательно определенные типы могут помочь вам более ясно выразить ваши намерения. Это позволяет вам развивать ваши программы постепенно, рас
ширяя типы за счет дополнительного функционала, но не нарушая при этом существующие между ними контракты. Вы можете определить тип, чтобы выбрать, в каком порядке должны размещаться два элемента:
trait Ord[T]:
def compare(x: T, y: T): Int def lteq(x: T, y: T): Boolean = compare(x, y) < 1
Этот трейт реализует функцию «меньше или равно» в виде более общего абстрактного метода compare
. Контракт этого метода состоит в том, что он возвращает
0
, если два параметра равны, положительное целое число, если первый параметр больше второго, и отрицательное целое число, если второй параметр больше первого. Теперь, имея это определение, вы можете указать стратегию сравнения для
T
, используя
Ord[T]
в качестве контекстного пара
метра, как показано в листинге 21.2.
Листинг 21.2. Контекстные параметры, передаваемые с помощью using def isort[T](xs: List[T])(using ord: Ord[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: Ord[T]): List[T] =
if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs else xs.head :: insert(x, xs.tail)
454 Глава 21 • Гивены
Как уже описывалось ранее, чтобы параметры можно было передавать не
явно, перед ними нужно указать using
. После этого вам больше не нужно предоставлять эти параметры вручную при вызове функции: если доступно значение подходящего типа, компилятор возьмет его и передаст вашей функ
ции. Чтобы сделать значение givenэкземпляром типа, его следует объявить с помощью ключевого слова given
Хорошим местом для размещения givenэкземпляров, представляющих
«естественный» вариант использования типа, такой как сортировка целых чисел в порядке возрастания, является объекткомпаньон «вовлеченного» типа. Например, естественный givenэкземпляр
Ord[Int]
можно было бы разместить в объектекомпаньоне для
Ord или
Int
— двух типов, «фигури
рующих» в
Ord[Int]
. Если компилятор не найдет givenэкземпляр
Ord[Int]
в лексической области видимости, он проведет дополнительный поиск в этих двух объектахкомпаньонах. Поскольку компаньон
Int не подлежит изменению, лучшим выбором является компаньон
Ord
:
object Ord:
// (Пока что не является устоявшимся решением)
given intOrd: Ord[Int] =
new Ord[Int]:
def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then 1 else 1
Все примеры givenобъявлений, показанные до сих пор в этой главе, на
зываются псевдонимными (alias). Имя по левую сторону от знака равенства является псевдонимом значения, указанного справа. Поскольку при объяв
лении псевдонимного givenэкземпляра справа от знака равенства зачастую определяют анонимный экземпляр трейта или класса, Scala предлагает со
кращенный синтаксис, который позволяет подставить вместо знака равен
ства и «имени нового класса» ключевое слово with
1
. В листинге 21.3 показано более компактное определение intOrd
Листинг 21.3. Объявление естественного given-экземпляра в компаньоне object Ord:
// Общепринятое решение given intOrd: Ord[Int] with def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then 1 else 1 1
Этот способ использования with отличается от того, который был описан в главе 11 и предназначался для объединения трейтов.
21 .3 . Анонимные given-экземпляры 455
Теперь, когда в объекте
Ord имеется givenэкземпляр
Ord[Int]
, сортировка с использованием isort снова становится лаконичной:
isort(List(10, 2, -10))
// List(-10, 2, 10)
Если опустить второй параметр isort
, компилятор начнет искать для него заданное значение с учетом его типа. Если речь идет о сортировке значе
ний
Int
, этим типом будет
Ord[Int]
. Вначале компилятор поищет given
экземпляр
Ord[Int]
в лексической области видимости, и, если его там не обнаружится, он пройдется по объектамкомпаньонам вовлеченных типов
Ord и
Int
. Поскольку в листинге 21.3 заданное значение intOrd имеет явно указанный тип, компилятор подставит intOrd вместо недостающего списка параметров.
Для сортировки строк достаточно предоставить givenэкземпляр для пара
метра, предназначенного для сравнения строковых значений:
// Добавлено в объект Ord given stringOrd: Ord[String] with def compare(s: String, t: String) = s.compareTo(t)
Теперь, когда в компаньоне
Ord определен givenэкземпляр
Ord[String]
, вы можете использовать isort для сортировки списков строк:
isort(List("mango", "jackfruit", "durian"))
// List(durian, jackfruit, mango)
Если заданное объявление не принимает параметризованные значения, givenэкземпляр инициализируется при первом к нему обращении, что похо
же на ленивые значения. Эта инициализация проводится потокобезопасным образом. Если же givenэкземпляр принимает параметры, он создается зано
во при каждом обращении, подобно тому как ведет себя def
. Действительно, компилятор Scala преобразует givenэкземпляры в val или def
, дополнитель
но делая их доступными для параметров using
21 .3 . Анонимные given-экземпляры
Заданное объявление можно считать частным случаем ленивого val или def
, однако оно обладает одной важной особенностью. При объявлении val
, к примеру, нужно задать выражение, указывающее на значение val
:
val age = 42
1 ... 43 44 45 46 47 48 49 50 ... 64