Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 784
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
192 Глава 9 • Управляющие абстракции быть вам еще не совсем привычен. Поэтому поясним, как применяются за
местители: функциональный литерал
_.endsWith(_)
, используемый в методе filesEnding
, означает то же самое, что и следующий код:
(fileName: String, query: String) => fileName.endsWith(query)
Поскольку filesMatching получает функцию, требующую два
String
аргу
мента, то типы аргументов указывать не нужно — можно просто воспользо
ваться кодом
(filename,
query)
=>
filename.endsWith(query)
. А так как в теле функции каждый из параметров используется только раз (то есть первый параметр, filename
, применяется в теле первым, второй параметр, query
, — вторым), можно прибегнуть к синтаксису заместителей —
_.endsWith(_)
Первый знак подчеркивания станет заместителем для первого параметра — имени файла, а второй — заместителем для второго параметра — строки запроса.
Этот код уже упрощен, но может стать еще короче. Обратите внимание: ар
гумент query передается filesMatching
, но данная функция ничего с ним не делает, за исключением того, что передает его обратно переданной функции matcher
. Такая передача тудасюда необязательна, поскольку вызывающий код с самого начала знает о query
! Это позволяет удалить параметр query как из filesMatching
, так и из matcher
, упростив код до состояния, показанного в листинге 9.1.
Листинг 9.1. Использование замыканий для сокращения повторяемости кода object FileMatcher:
private def filesHere = (new java.io.File(".")).listFiles private def filesMatching(matcher: String => Boolean) =
for file <- filesHere if matcher(file.getName)
yield file def filesEnding(query: String) =
filesMatching(_.endsWith(query))
def filesContaining(query: String) =
filesMatching(_.contains(query))
def filesRegex(query: String) =
filesMatching(_.matches(query))
В этом примере показан способ, позволяющий функциям первого класса помочь вам избавиться от дублирующегося кода, что без них сделать было бы очень трудно. В Java, к примеру, можно создать интерфейс, который со
держит метод, получающий одно
String
значение и возвращающий значение
9 .2 . Упрощение клиентского кода 193
типа
Boolean
, а затем создать и передать функции filesMatching экземпля
ры анонимного внутреннего класса, реализующие этот интерфейс. Такой подход позволит избавиться от дублирующегося кода, чего, собственно, вы и добивались, но в то же время приведет к добавлению чуть ли не большего количества нового кода. Стало быть, цена вопроса сведет на нет все преиму
щества и лучше будет, вероятно, смириться с повторяемостью кода.
Кроме того, данный пример показывает, как замыкания способны помочь сократить повторяемость кода. Функциональные литералы, использованные в предыдущем примере, такие как
_.endsWith(_)
и
_.contains(_)
, во время выполнения программы становятся экземплярами функциональных значе
ний, которые не являются замыканиями, поскольку не захватывают никаких свободных переменных. К примеру, обе переменные, использованные в вы
ражении
_.endsWith(_)
, представлены в виде знаков подчеркивания, следо
вательно, берутся из аргументов функции. Таким образом, в
_.endsWith(_)
задействуются не свободные, а две связанные переменные. В отличие от этого, в функциональном литерале
_.endsWith(query)
, использованном в са
мом последнем примере, содержатся одна связанная переменная, а именно аргумент, представленный знаком подчеркивания, и одна свободная перемен
ная по имени query
. Возможность убрать параметр query из filesMatching в этом примере, тем самым еще больше упростив код, появилась у вас только потому, что в Scala поддерживаются замыкания.
9 .2 . Упрощение клиентского кода
В предыдущем примере было показано, что применение функций высшего порядка способствует сокращению повторяемости кода по мере реализации
API. Еще один важный способ использовать функции высшего порядка — поместить их в сам API с целью повысить лаконичность клиентского кода.
Хорошим примером могут послужить методы организации циклов специ
ального назначения, принадлежащие имеющимся в Scala типам коллекций
1
Многие из них перечислены в табл. 3.1, но сейчас, чтобы понять, почему эти методы настолько полезны, внимательно рассмотрите только один пример.
Рассмотрим exists
— метод, определяющий факт наличия переданного значения в коллекции. Разумеется, искать элемент можно, инициализировав var
переменную значением false и выполнив перебор элементов коллекции,
1
Эти специализированные методы циклической обработки определены в трейте
Iterable
, который является расширением классов
List
,
Set и
Map
. Более подробно данный вопрос рассматривается в главе 15.
1 ... 17 18 19 20 21 22 23 24 ... 64
194 Глава 9 • Управляющие абстракции проверяя каждый из них и присваивая var
переменной значение true
, если будет найден предмет поиска. Метод, в котором такой подход используется с целью определить, имеется ли в переданном списке
List отрицательное число, выглядит следующим образом:
def containsNeg(nums: List[Int]): Boolean =
var exists = false for num <- nums do if num < 0 then exists = true exists
Если определить этот метод в REPL, то его можно вызвать следующими коман дами:
containsNeg(List(1, 2, 3, 4)) // false containsNeg(List(1, 2, -3, 4)) // true
Но более лаконичный способ определения метода предусматривает вызов в отношении списка
List функции высшего порядка exists
:
def containsNeg(nums: List[Int]) = nums.exists(_ < 0)
Эта версия containsNeg выдает те же результаты, что и предыдущая:
containsNeg(Nil) // false containsNeg(List(0, -1, -2)) // true
Метод exists представляет собой управляющую абстракцию. Это специали
зированная циклическая конструкция, которая не встроена в язык Scala, как while или for
, а предоставляется библиотекой Scala. В предыдущем разделе функция высшего порядка filesMatching позволила сократить повторяемость кода в реализации объекта
FileMatcher
. Метод exists обеспечивает такое же преимущество, но поскольку это публичный метод в API коллекций Scala, то сокращение повторяемости относится к клиентскому коду этого API. Если бы метода exists не было и потребовалось бы написать метод выявления на
личия в списке четных чисел containsOdd
, то это можно было бы сделать так:
def containsOdd(nums: List[Int]): Boolean =
var exists = false for num <- nums do if num % 2 == 1 then exists = true exists
Сравнивая тело метода containsNeg с телом метода containsOdd
, можно заме
тить повторяемость во всем, за исключением условия проверки в выражении
9 .3 . Карринг 195
expression
. С помощью метода exists вместо этого можно воспользоваться следующим кодом:
def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)
Тело кода в этой версии также практически идентично телу соответству
ющего метода containsNeg
(той его версии, в которой используется exists
), за исключением того, что условие, по которому выполняется поиск, иное.
И тем не менее объем повторяющегося кода значительно уменьшился, по
скольку вся инфраструктура организации цикла убрана в метод exists
В стандартной библиотеке Scala имеется множество других методов для ор
ганизации цикла. Как и exists
, они зачастую могут сократить объем вашего кода, если появится возможность их применения.
9 .3 . Карринг
В главе 1 говорилось, что Scala позволяет создавать новые управляющие абстракции, которые воспринимаются как естественная языковая поддержка.
Хотя показанные до сих пор примеры фактически и были управляющими абстракциями, вряд ли ктото смог бы воспринять их как естественную поддержку со стороны языка. Чтобы понять, как создаются управляющие абстракции, больше похожие на расширения языка, сначала нужно разо
браться с приемом функцио нального программирования, который называ
ется каррингом.
Каррированная функция применяется не к одному, а к нескольким спискам аргументов. В листинге 9.2 показана обычная, некаррированная функция, складывающая два
Int
параметра, x
и y
Листинг 9.2. Определение и вызов обычной функции def plainOldSum(x: Int, y: Int) = x + y plainOldSum(1, 2) // 3
В листинге 9.3 показана аналогичная, но уже каррированная функция. Вме
сто списка из двух параметров типа
Int эта функция применяется к двум спискам, в каждом из которых содержится по одному параметру типа
Int
Листинг 9.3. Определение и вызов каррированной функции def curriedSum(x: Int)(y: Int) = x + y curriedSum(1)(2) // 3
196 Глава 9 • Управляющие абстракции
Здесь при вызове curriedSum вы фактически получаете два обычных вызова функции, следующих непосредственно друг за другом. Первый получает единственный параметр
Int по имени x
и возвращает функциональное зна
чение для второй функции. А та получает
Int
параметр y
. Здесь действие функции по имени first соответствует тому, что должно было происходить при вызове первой традиционной функции curriedSum
:
def first(x: Int) = (y: Int) => x + y
Применение первой функции к числу
1
, иными словами, вызов первой функ
ции и передача ей значения
1
, образует вторую функцию:
val second = first(1) // second имеет тип Int => Int
Применение второй функции к числу
2
дает результат:
second(2) //3
Функции first и second всего лишь показывают процесс карринга. Они не связаны непосредственно с функцией curriedSum
. И тем не менее это способ получить фактическую ссылку на вторую функцию из curriedSum
. Чтобы воспользоваться curriedSum в выражении частично примененной функцией, можно обратиться к форме записи с заместителем:
val onePlus = curriedSum(1) // onePlus имеет тип Int => Int
Знак подчеркивания в curriedSum(1)_
является заместителем для второго списка, используемого в качестве Впараметра. В результате получается ссылка на функцию, при вызове которой единица прибавляется к ее един
ственному
Int
аргументу, и возвращается результат:
onePlus(2) //3
А вот как можно получить функцию, прибавляющую число
2
к ее единствен
ному
Int
аргументу:
val twoPlus = curriedSum(2)
twoPlus(2) // 4 9 .4 . Создание новых управляющих конструкций
В языках, использующих функции первого класса, даже если синтаксис язы
ка устоялся, есть возможность эффективно создавать новые управляющие
9 .4 . Создание новых управляющих конструкций 197
конструкции. Нужно лишь создать методы, получающие функции в виде аргументов.
Например, во фрагменте кода ниже показана удваивающая управляющая конструкция — она повторяет операцию два раза и возвращает результат:
def twice(op: Double => Double, x: Double) = op(op(x))
twice(_ + 1, 5) // 7.0
Типом op в данном примере является
Double
=>
Double
. Это значит, что функция получает одно
Double
значение в качестве аргумента и возвращает другое
Double
значение.
Каждый раз, замечая шаблон управления, повторяющийся в разных ча
стях вашего кода, вы должны задуматься о его реализации в виде новой управляющей конструкции. Ранее в этой главе был показан filesMatching
, узкоспециализированный шаблон управления. Теперь рассмотрим более широко применяющийся шаблон программирования: открытие ресурса, ра
боту с ним, а затем закрытие ресурса. Все это можно собрать в управляющую абстракцию, прибегнув к методу, показанному ниже:
def withPrintWriter(file: File, op: PrintWriter => Unit) =
val writer = new PrintWriter(file)
try op(writer)
finally writer.close()
При наличии такого метода им можно воспользоваться так:
withPrintWriter(
new File("date.txt"),
writer => writer.println(new java.util.Date)
)
Преимущества применения этого метода состоят в том, что закрытие файла в конце работы гарантируется withPrintWriter
, а не пользователь
ским кодом. Поэтому забыть закрыть файл просто невозможно. Данная технология называется шаблоном временного пользования (loan pattern), поскольку функция управляющей абстракции, такая как withPrintWriter
, открывает ресурс и отдает его функции во временное пользование. Так, в предыдущем примере withPrintWriter отдает во временное пользо
вание
PrintWriter функции op
. Когда функция завершает работу, она сигнализирует, что ей уже не нужен одолженный ресурс. Затем в блоке finally ресурс закрывается; это гарантирует его безусловное закрытие независимо от того, как завершилась работа функции — успешно или с генерацией исключения.
198 Глава 9 • Управляющие абстракции
Один из способов придать клиентскому коду вид, который делает его по
хожим на встроенную управляющую конструкцию, предусматривает за
ключение списка аргументов в фигурные, а не в круглые скобки. Если в Scala при каждом вызове метода ему передается строго один аргумент, то можно заключить его не в круглые, а в фигурные скобки. Например, вместо val s = "Hello, world!"
s.charAt(1) // 'e'
можно написать:
s.charAt { 1 } // 'e'
Во втором примере аргумент для charAt вместо круглых скобок заключен в фигурные. Но такой прием использования фигурных скобок будет работать только при передаче одного аргумента. Попытка нарушить это правило при
водит к следующему результату:
s.substring { 7, 9 }
1 |s.substring { 7, 9 }
| ˆ
| end of statement expected but ',' found
1 |s.substring { 7, 9 }
| ˆ
| ';' expected, but integer literal found
Поскольку была предпринята попытка передать функции substring два ар
гумента, то при их заключении в фигурные скобки выдается ошибка. Вместо фигурных в данном случае нужно использовать круглые скобки:
s.substring(7, 9) // "wo"
Назначение такой возможности заменить круглые скобки фигурными при передаче одного аргумента — позволить программистамклиентам записать в фигурных скобках функциональный литерал. Тем самым можно сделать вызов метода похожим на управляющую абстракцию. В качестве примера можно взять определенный ранее метод withPrintWriter
. В своем самом последнем виде метод withPrintWriter получает два аргумента, поэтому использовать фигурные скобки нельзя. Тем не менее, поскольку функция, переданная withPrintWriter
, является последним аргументом в списке, можно воспользоваться каррингом, чтобы переместить первый аргумент типа
File в отдельный список аргументов. Тогда функция останется един
ственным параметром второго списка параметров. Способ переопределения withPrintWriter показан в листинге 9.4.
9 .5 . Передача параметров по имени 199
Листинг 9.4. Применение шаблона временного пользования для записи в файл def withPrintWriter(file: File)(op: PrintWriter => Unit) =
val writer = new PrintWriter(file)
try op(writer)
finally writer.close()
Новая версия отличается от старой всего лишь тем, что теперь есть два спи
ска параметров, по одному параметру в каждом, а не один список из двух параметров. Загляните между двумя параметрами. В показанной здесь преж
ней версии withPrintWriter вы видите
...File,
op...
. Но в этой версии вы видите
...File)(op...
. Благодаря определению, приведенному ранее, метод можно вызвать с помощью более привлекательного синтаксиса:
val file = new File("date.txt")
withPrintWriter(file) { writer =>
writer.println(new java.util.Date)
}
В этом примере первый список аргументов, в котором содержится один ар
гумент типа
File
, заключен в круглые скобки. А второй список аргументов, содержащий функциональный аргумент, заключен в фигурные скобки.
9 .5 . Передача параметров по имени
Метод withPrintWriter
, рассмотренный в предыдущем разделе, отличает
ся от встроенных управляющих конструкций языка, таких как if и while
, тем, что тело управляющей абстракции (код между фигурными скобками) получает аргумент. Функция, переданная withPrintWriter
, требует одного аргумента типа
PrintWriter
. Этот аргумент показан в следующем коде как writer
=>
:
withPrintWriter(file) { writer =>
writer.println(new java.util.Date)
}
А что нужно сделать, если понадобится реализовать нечто больше похожее на if или while
, где в теле нет значения для передачи в код? Помочь справиться с подобными ситуациями могут имеющиеся в Scala параметры, передавае-
мые по имени (byname parameters).