Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 767
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
8 .6 . Частично примененные функции
1 ... 15 16 17 18 19 20 21 22 ... 64
175
то функцию sum можно применить к аргументам
1
,
2
и
3
таким образом:
sum(1, 2, 3) // 6
Когда вы используете синтаксис заполнителя, в котором каждое подчерки
вание используется для пересылки параметра в метод, вы пишете частично применяемую функцию. Частично примененная функция — выражение, в котором не содержатся все аргументы, необходимые функции. Вместо этого в ней есть лишь некоторые из них или вообще нет никаких необходимых аргументов. Например, чтобы создать выражение частично применяемой функции, включающее сумму, в которой вы не указываете ни один из трех обязательных аргументов, вы можете использовать подчеркивание для каждого параметра. Полученную функцию затем можно сохранить в пере
менной. Вот пример:
val a = sum(_, _, _) // имеет тип (Int, Int, Int) => Int
Если данный код есть, то компилятор Scala создает экземпляр функцио
нального значения, который получает три целочисленных параметра, не указанных в выражении частично примененной функции, sum
(_,
_,
_)
, и присваивает ссылку на это новое функциональное значение переменной a
Когда к этому новому значению применяются три аргумента, оно развернет
ся и вызовет sum
, передав в нее те же три аргумента:
a(1, 2, 3) // 6
Происходит следующее: переменная по имени a
ссылается на объект функ
ционального значения. Это функциональное значение является экземпля
ром класса, сгенерированного автоматически компилятором Scala из sum
(_,
_,
_)
— выражения частично примененной функции. Класс, сгенери
рованный компилятором, имеет метод apply
, получающий три аргумента
1
Имеющийся метод apply получает три аргумента, поскольку это и есть коли
чество аргументов, отсутствующих в выражении sum
(_,
_,
_)
. Компилятор
Scala транслирует выражение a(1,
2,
3)
в вызов метода apply
, принадлежа
щего объекту функционального значения, передавая ему три аргумента:
1
,
2
и
3
. Таким образом, a(1,
2,
3)
— краткая форма следующего кода:
a.apply(1, 2, 3) // 6
Этот метод apply
, который определен в автоматически генерируемом ком
пилятором Scala классе из выражения sum
(_,
_,
_)
, просто передает дальше
1
Сгенерированный класс является расширением трейта
Function3
, в котором объ
явлен метод apply
, предусматривающий использование трех аргументов.
176 Глава 8 • Функции и замыкания эти три отсутствовавших параметра функции sum и возвращает результат.
В данном случае метод apply вызывает sum(1,
2,
3)
и возвращает то, что возвращает функция sum
, то есть число
6
Данный вид выражений, в которых знак подчеркивания используется для представления всего списка параметров, можно представить себе и по
другому — в качестве способа преобразования def в функциональное зна
чение. Например, если имеется локальная функция, скажем, sum(a:
Int,
b:
Int,
c:
Int):
Int
, то ее можно завернуть в функциональное значение, чей метод apply имеет точно такие же типы списка параметров и результата. Это функциональное значение, будучи примененным к неким аргументам, в свою очередь, применяет sum для тех же самых аргументов и возвращает результат.
Вы не можете присвоить переменной метод или вложенную функцию или передать их в качестве аргументов другой функции. Однако все это можно сделать, если завернуть метод или вложенную функцию в функциональное значение, поместив знак подчеркивания.
Теперь, несмотря на то что sum
(_,
_,
_)
действительно является частично примененной функцией, вам может быть не вполне понятно, почему она называется именно так. Это потому, что она применяется не ко всем своим аргументам. Что касается sum
(_,
_,
_)
, то она не применяется ни к одному из своих аргументов. Но вы также можете выразить частично примененную функцию, предоставив ей только некоторые из требуемых аргументов. Рас
смотрим пример:
val b = sum(1, _, 3) // b имеет тип Int => Int
В данном случае функции sum предоставлены первый и последний аргумен
ты, а средний аргумент не указан. Поскольку пропущен только один аргу
мент, то компилятор Scala сгенерирует новый функциональный класс, чей метод apply получает один аргумент. При вызове с этим одним аргументом метод apply сгенерированной функции вызывает функцию sum
, передавая ей
1
, затем аргумент, переданный функции, и, наконец,
3
. Рассмотрим два примера:
b(2) // 6
b(5) // 9
В первом случае b.apply вызывает sum(1,
2,
3)
, а во втором случае b.apply вызывает sum(1,
5,
3)
Если функция требуется в конкретном месте кода, то при написании выраже
ния частично примененной функции, в котором не указан ни один параметр,
8 .7 . Замыкания 177
например sum
(_,
_,
_)
, его можно записать более кратко для всего списка параметров. Вот пример:
val c = sum // c имеет тип (Int, Int, Int) => Int
Поскольку sum
— это имя метода, а не переменной, которая ссылается на значение, компилятор создаст значение функции с той же сигнатурой, что и метод, заключающий в себе его вызов. Этот процесс называется eta
расширением. Другими словами, sum
— это более краткий способ записи sum
(_,
_,
_)
. Вот пример вызова функции:
c(10, 20, 30) // 60 8 .7 . Замыкания
Все рассмотренные до сих пор в этой главе примеры функциональных ли
тералов ссылались только на передаваемые параметры. Так, в выражении
(x:
Int)
=>
x
>
0
в теле функции x
>
0
использовалась только одна перемен
ная, x
, которая объявлена как параметр функции. Но вы можете ссылаться на переменные, объявленные и в других местах:
(x: Int) => x + more // На сколько больше?
Эта функция прибавляет значение переменной more к своему аргументу, но что такое more
? С точки зрения данной функции more
— свободная пере-
менная, поскольку в самом функциональном литерале значение ей не при
сваивается. В отличие от нее переменная x
является связанной, поскольку в контексте функции имеет значение: определена как единственный пара
метр функции, имеющий тип
Int
. Если попытаться воспользоваться этим функциональным литералом в чистом виде, без какихлибо определений в его области видимости, то компилятор выразит недовольство:
scala> (x: Int) => x + more
1 |(x: Int) => x + more
| ˆˆˆˆ
| Not found: more
С другой стороны, тот же функциональный литерал будет нормально рабо
тать, пока будет доступно нечто с именем more
:
var more = 1
val addMore = (x: Int) => x + more addMore(10) // 11
178 Глава 8 • Функции и замыкания
Функциональное значение (объект), создаваемое во время выполнения программы из этого функционального литерала, называется замыканием.
Данное название появилось изза «замыкания» функционального литерала путем «захвата» привязок его свободных переменных. Функциональный литерал, не имеющий свободных переменных, например
(x:
Int)
=>
x
+
1
, называется замкнутым термом, где терм — это фрагмент исходного кода.
Таким образом, функциональное значение, созданное во время выполнения программы из этого функционального литерала, строго говоря, не является замыканием, поскольку функциональный литерал
(x:
Int)
=>
x
+
1
всегда замкнут уже по факту его написания. Но любой функциональный литерал со свободными переменными, например
(x:
Int)
=>
x
+
more
, является от-
крытым термом. Поэтому любое функциональное значение, созданное во время выполнения программы из
(x:
Int)
=>
x
+
more
, будет по определению требовать, чтобы привязка его свободной переменной, more
, была захвачена.
Получившееся функциональное значение, в котором может содержаться ссылка на захваченную переменную more
, называется замыканием, поскольку функциональное значение — конечный продукт замыкания открытого терма,
(x:
Int)
=>
x
+
more
Этот пример вызывает вопрос: что случится, если значение more изменится после создания замыкания? В Scala можно ответить, что замыкание видит изменение, например:
more = 9999
addMore(10) // 10009
На интуитивном уровне понятно, что замыкания в Scala перехватывают сами переменные, а не значения, на которые те ссылаются
1
. Как показано в предыдущем примере, замыкание, созданное для
(x:
Int)
=>
x
+
more
, видит изменение more за пределами замыкания. То же самое справедливо и в об
ратном направлении. Изменения, вносимые в захваченную переменную, видимы за пределами замыкания. Рассмотрим пример:
val someNumbers = List(-11, -10, -5, 0, 5, 10)
var sum = 0
someNumbers.foreach(sum += _)
sum // -11 1
Для сравнения: лямбдавыражения Java не позволяют вам обращаться к локальным переменным в окружающих областях, если они не являются окончательными или фактически окончательными, поэтому нет никакой разницы между захватом пере
менной и захватом ее текущего значения.
8 .7 . Замыкания 179
Здесь используется обходной способ сложения чисел в списке типа
List
Переменная sum находится в области видимости, охватывающей функцио
нальный литерал sum
+=
_
, который прибавляет числа к sum
. Несмотря на то что замыкание модифицирует sum во время выполнения программы, получающийся конечный результат
-11
попрежнему виден за пределами замыкания.
А что, если замыкание обращается к некой переменной, у которой во время выполнения программы есть несколько копий? Например, что, если за
мыкание использует локальную переменную некой функции и последняя вызывается множество раз? Какой из экземпляров этой переменной будет задействован при каждом обращении?
С остальной частью языка согласуется только один ответ: задействуется тот экземпляр, который был активен на момент создания замыкания. Рас
смотрим, к примеру, функцию, создающую и возвращающую замыкания прироста значения:
def makeIncreaser(more: Int) = (x: Int) => x + more
При каждом вызове эта функция будет создавать новое замыкание. Каждое замыкание будет обращаться к той переменной more
, которая была активна при создании замыкания.
val inc1 = makeIncreaser(1)
val inc9999 = makeIncreaser(9999)
При вызове makeIncreaser(1)
создается и возвращается замыкание, захва
тывающее в качестве привязки к more значение
1
. По аналогии с этим при вызове makeIncreaser(9999)
возвращается замыкание, захватывающее для more значение
9999
. Когда эти замыкания применяются к аргументам (в дан
ном случае имеется только один передаваемый аргумент, x
), получаемый результат зависит от того, как переменная more была определена в момент создания замыкания:
inc1(10) // 11
inc9999(10) // 10009
И неважно, что more в данном случае — параметр вызова метода, из которого уже произошел возврат. В подобных случаях компилятор Scala осуществляет реорганизацию, которая дает возможность захваченным параметрам продол
жать существовать в динамической памяти (куче), а не в стеке и пережить таким образом создавший их метод. Данная реорганизация происходит
180 Глава 8 • Функции и замыкания в автоматическом режиме, поэтому вам о ней не стоит беспокоиться. За
хватывайте какую угодно переменную: val или var или любой параметр
1 8 .8 . Специальные формы вызова функций
Большинство встречающихся вам функций и вызовов функций будут анало
гичны уже увиденным в этой главе. У функции будет фиксированное число параметров, у вызова будет точно такое же количество аргументов, и аргумен
ты будут указаны в точно таких же порядке и количестве, что и параметры.
Но поскольку вызовы функций в программировании на Scala весьма важ
ны, то для обеспечения некоторых специальных потребностей в язык было добавлено несколько специальных форм определений и вызовов функций.
В Scala поддерживаются повторяющиеся параметры, именованные аргумен
ты и аргументы со значениями по умолчанию.
Повторяющиеся параметры
В Scala допускается указание на то, что последний параметр функции может повторяться. Это позволяет клиентам передавать функции список аргу
ментов переменной длины. Чтобы обозначить повторяющийся параметр, поставьте после типа параметра знак звездочки, например:
scala> def echo(args: String*) =
for arg <- args do println(arg)
def echo(args: String*): Unit
Определенная таким образом функция echo может быть вызвана с нулем и большим количеством аргументов типа
String
:
scala> echo()
scala> echo("one")
one scala> echo("hello", "world!")
hello world!
1
С другой стороны, в функциональном программировании вы будете учитывать только vals
. Кроме того, в императивном программировании при параллельной разработке захват vars может привести к ошибкам параллелизма изза несинхро
низированного доступа к общему изменяемому состоянию.
8 .8 . Специальные формы вызова функций 181
Внутри функции типом повторяющегося параметра является
Seq из элемен
тов объявленного типа параметра. Таким образом, типом переменной args внутри функции echo
, которая объявлена как тип
String*
, фактически явля
ется
Seq[String]
. Несмотря на это, если у вас имеется массив подходящего типа, при попытке передать его в качестве повторяющегося параметра будет получена ошибка компиляции:
scala> val seq = Seq("What's", "up", "doc?")
val seq: Seq[String] = List(What's, up, doc?)
scala> echo(seq)
1 |echo(seq)
| ˆˆˆ
| Found: (seq : Seq[String])
| Required: String
Чтобы добиться успеха, нужно после аргументамассива поставить двоето
чие, знак подчеркивания и знак звездочки:
scala> echo(seq*)
What’s up doc?
Эта форма записи заставит компилятор передать в echo каждый элемент массива seq в виде самостоятельного аргумента, а не передавать его целиком в виде одного аргумента.
Именованные аргументы
При обычном вызове функции аргументы в вызове поочередно сопоставля
ются в указанном порядке с параметрами вызываемой функции:
def speed(distance: Float, time: Float) = distance / time speed(100, 10) // 10.0
В данном вызове
100
сопоставляется с distance
, а
10
— с time
. Сопоставление
100
и
10
производится в том же порядке, в котором перечислены формальные параметры.
Именованные аргументы позволяют передавать аргументы функции в ином порядке. Синтаксис просто предусматривает, что перед каждым аргументом указывается имя параметра и знак равенства. Например, следующий вызов функции speed эквивалентен вызову speed(100,10)
:
speed(distance = 100, time = 10) // 10.0
182 Глава 8 • Функции и замыкания
При вызове с именованными аргументами эти аргументы можно поменять местами, не изменяя их значения:
speed(time = 10, distance = 100) // 10.0
Можно также смешивать позиционные и именованные аргументы. В этом случае сначала указываются позиционные аргументы. Именованные чаще всего используются в сочетании со значениями параметров по умолчанию.
Значения параметров по умолчанию
Scala позволяет указать для параметров функции значения по умолчанию.
Аргумент для такого параметра может быть произвольно опущен из вызова функции, в таком случае соответствующий аргумент будет заполнен значе
нием по умолчанию.
Например, если вам нужно создать объекткомпаньон для класса
Rational
, показанного в листинге 6.5, вы можете определить фабричный метод apply
, как показано в листинге 8.3. Функция apply имеет два параметра: denom
, для которого значение по умолчанию равно
1
, и numer
Если вызвать функцию
Rational(42)
, то есть без указания аргумента, исполь
зуемого для denom
, то для этого параметра будет установлено его значение по умолчанию
1
. Можно также вызвать функцию с явно указанным знаме
нателем. Например, установите знаменатель равным 83, вызвав функцию как
Rational(42,
83)
1
Листинг 8.3. Параметр со значением по умолчанию
// в том же исходнике, что и класс Rational object Rational:
def apply(numer: Int, denom: Int = 1) =
new Rational(numer, denom)
Параметры по умолчанию особенно полезны, когда применяются в сочета
нии с именованными параметрами. В листинге 8.4 у функции point имеют
ся два необязательных параметра: x
и y
, значение которых по умолчанию равно
0 1
Значение по умолчанию, равное 1, можно было бы также использовать для параме
тра d
класса
Rational в листинге 6.5, как в class Rational(n:
Int,
d:
Int
=
1
), вместо использования вспомогательного конструктора, который заполняет 1 для d