Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 751
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 7.19. Функциональный способ создания таблицы умножения
// возвращение строчки в виде последовательности def makeRowSeq(row: Int) =
for col <- 1 to 10 yield val prod = (row * col).toString val padding = " " * (4 - prod.length)
padding + prod
// возвращение строчки в виде строкового значения def makeRow(row: Int) = makeRowSeq(row).mkString
// возвращение таблицы в виде строковых значений, по одному значению
// на каждую строчку
166 Глава 7 • Встроенные управляющие конструкции def multiTable() =
val tableSeq = // последовательность строк из строчек таблицы for row <– 1 to 10
yield makeRow(row)
tableSeq.mkString("\n")
На наличие в листинге 7.18 императивного стиля указывают два момента.
Первый — побочный эффект от вызова printMultiTable
— вывод таблицы умножения на стандартное устройство. В листинге 7.19 функция реорганизо
вана таким образом, чтобы возвращать таблицу умножения в виде строкового значения. Поскольку функция больше не занимается выводом на стандарт
ное устройство, то переименована в multiTable
. Как уже упоминалось, одно из преимуществ функций, не имеющих побочных эффектов, — упрощение их модульного тестирования. Для тестирования printMultiTable понадо
билось бы какимто образом переопределять print и println
, чтобы можно было проверить вывод на корректность. А протестировать multiTable гораздо проще — проверив ее строковой результат.
Второй момент, служащий верным признаком императивного стиля в функ
ции printMultiTable
— ее цикл while и var
переменные. В отличие от этого, в функции multiTable для выражений, вспомогательных функций и вызовов mkString используются val
переменные.
Чтобы облегчить чтение кода, мы выделили две вспомогательные функции: makeRowSeq и makeRow
. Первая использует выражение for
, генератор которого перебирает номера столбцов от 1 до 10. В теле этого выражения вычисляется произведение значения строки на значение столбца, определяется отступ, необходимый для произведения, выдается результат объединения строк отступа и произведения. Результатом выражения for будет последователь
ность (один из подклассов
Seq
), содержащая выданные строки в качестве элементов. Вторая вспомогательная функция, makeRow
, просто вызывает ме
тод mkString в отношении результата, возвращенного функцией makeRowSeq
Этот метод объединяет имеющиеся в последовательности строки, возвращая их в виде одной строки.
Метод multiTable сначала инициализирует tableSeq результатом выполне
ния выражения for
, генератор которого перебирает числа от 1 до 10, чтобы для каждого вызова makeRow получалось строковое значение для данной строки таблицы. Именно эта строка и выдается, вследствие чего результатом выполнения данного выражения for будет последовательность строковых значений, представляющих строки таблицы. Остается лишь преобразовать последовательность строк в одну строку. Для выполнения этой задачи вы
7 .9 . Резюме 167
зывается метод mkString
, и, поскольку ему передается значение "\n"
, мы получаем символ конца строки, вставленный после каждой строки. Передав строку, возвращенную multiTable
, функции println
, вы увидите, что выво
дится точно такая же таблица, как и при вызове функции printMultiTable
:
1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 18 27 36 45 54 63 72 81 90 10 20 30 40 50 60 70 80 90 100 7 .9 . Резюме
Перечень встроенных в Scala управляющих конструкций минимален, но они вполне справляются со своими задачами. Их работа похожа на действия их императивных эквивалентов, но, поскольку им свойственно выдавать зна
чение, они поддерживают и функциональный стиль. Не менее важно и то, что управляющие конструкции оставляют поле деятельности для одной из самых эффективных возможностей Scala — функциональных литералов, которые рассматриваются в следующей главе.
8
Функции и замыкания
По мере роста программ появляется необходимость разбивать их на неболь
шие части, которыми удобнее управлять. Разделение потока управления в Scala реализуется с помощью подхода, знакомого всем опытным програм
мистам: разделение кода на функции. Фактически в Scala предлагается не
сколько способов определения функций, которых нет в Java. Кроме методов, представляющих собой функции, являющиеся частью объектов, есть также функции, вложенные в другие функции, функциональные литералы и функ
циональные значения. В данной главе вам предстоит познакомиться со всеми этими разновидностями функций, имеющимися в Scala.
8 .1 . Методы
Наиболее распространенный способ определения функций — включение их в состав объекта. Такая функция называется методом. В качестве примера в листинге 8.1 показаны два метода, которые вместе считывают данные из файла с заданным именем и выводят строки, длина которых превышает заданную. Перед каждой выведенной строкой указывается имя файла, в ко
тором она появляется.
Листинг 8.1. LongLines с приватным методом processLine object Padding:
def padLines(text: String, minWidth: Int): String =
val paddedLines =
for line <- text.linesIterator yield padLine(line, minWidth)
paddedLines.mkString("\n")
8 .2 . Локальные функции 169
private def padLine(line: String, minWidth: Int): String =
if line.length >= minWidth then line else line + " " * (minWidth - line.length)
Метод padLines принимает в качестве параметров text и minWidth
. Он вы
зывает linesIterator для text
, который возвращает итератор строк в типе данных string
, исключая любые символы окончания строки. Выражение for обрабатывает каждую из этих строк, вызывая вспомогательный метод padLine
. Метод padLine принимает два параметра: minWidth и line
. Он срав
нивает длину строки с заданной шириной и, если длина меньше, добавляет соответствующее количество пробелов в конец строки, чтобы их уравнять.
Пока что это очень похоже на то, что вы делаете в любом объектноориен
тированном языке. Однако понятие функции в Scala является более общим, чем метод. Другие способы выражения функций в Scala будут объяснены в следующих разделах.
8 .2 . Локальные функции
Конструкция метода padLines из предыдущего раздела показала важность принципов разработки, присущих функциональному стилю программиро
вания: программы должны быть разбиты на множество небольших функций с четко определенными задачами. Зачастую отдельно взятая функция весьма невелика. Преимущество подобного стиля — в предоставлении програм
мисту множества строительных блоков, позволяющих составлять гибкие композиции для решения более сложных задач. Такие строительные блоки должны быть довольно простыми, чтобы с ними было легче разобраться по отдельности.
Подобный подход выявляет одну проблему: имена всех вспомогательных функций могут засорять пространство имен программы. В REPL это прояв
ляется не так ярко, но по мере упаковки функций в многократно применяе
мые классы и объекты желательно будет скрыть вспомогательные функции от пользователей класса. Зачастую, будучи отдельно взятыми, такие функ
ции не имеют особого смысла, и возникает желание сохранять достаточную степень гибкости, чтобы можно было удалить вспомогательные функции, если позже класс будет переписан.
В Java основной инструмент для этого — приватный метод. Как было по
казано в листинге 8.1, точно такой же подход с использованием приват
ного метода работает и в Scala, но в этом языке предлагается и еще один: можно определить функцию внутри другой функции. Подобно локальным
170 Глава 8 • Функции и замыкания переменным, такие локальные функции видны только в пределах своего приватного блока. Рассмотрим пример:
def padLines(text: String, minWidth: Int): String =
def padLine(line: String, minWidth: Int): String =
if line.length >= minWidth then line else line + " " * (minWidth - line.length)
val paddedLines =
for line <- text.linesIterator yield padLine(line, minWidth)
paddedLines.mkString("\n")
Здесь реорганизована исходная версия
Padding
, показанная в листин
ге 8.1: приватный метод padLine был превращен в локальную функцию для padLines
. Для этого был удален модификатор private
, который мог быть применен (и нужен) только для членов класса, а определение функции padLine было помещено внутрь определения функции padLines
. В качестве локальной функция padLine находится в области видимости внутри функции padLines
, за пределами которой она недоступна.
Но теперь, когда функция padLine определена внутри функции padLines
, появилась возможность улучшить коечто еще. Вы заметили, что minWidth передается вспомогательной функции, как и прежде? В этом нет никакой необходимости, поскольку локальные функции могут получать доступ к па
раметрам охватывающей их функции. Как показано в листинге 8.2, можно просто воспользоваться параметрами внешней функции padLines
Листинг 8.2. LongLines с локальной функцией processLine object Padding:
def padLines(text: String, minWidth: Int): String =
def padLine(line: String): String =
if line.length >= minWidth then line else line + " " * (minWidth - line.length)
val paddedLines =
for line <- text.linesIterator yield padLine(line)
paddedLines.mkString("\n")
Заметили, как упростился код? Такое использование параметров охваты
вающей функции — широко распространенный и весьма полезный пример универсальной вложенности, предоставляемой Scala. Вложенность и области видимости применительно ко всем конструкциям данного языка, включая
8 .3 . Функции первого класса 171
функции, рассматриваются в разделе 7.7. Это довольно простой, но весьма эффективный принцип.
8 .3 . Функции первого класса
В Scala есть функции первого класса. Вы можете не только определить их и вызвать, но и записать в виде безымянных литералов, после чего передать их в качестве значений. Понятие функциональных литералов было введено в главе 2, а их основной синтаксис показан на рис. 2.2.
Функциональный литерал компилируется в дескрипторе методов Java, кото
рый при создании экземпляра во время выполнения программы становится
функциональным значением
1
. Таким образом, разница между функциональ
ными литералами и значениями состоит в том, что первые существуют в ис
ходном коде, а вторые — в виде объектов во время выполнения программы.
Эта разница во многом похожа на разницу между классами (исходным ко
дом) и объектами (создаваемыми во время выполнения программы).
Простой функциональный литерал, прибавляющий к числу единицу, имеет следующий вид:
(x: Int) => x + 1
Сочетание символов
=>
указывает на то, что эта функция превращает сто
ящий слева от данного сочетания параметр (любое целочисленное значе
ние x
) в результат вычисления выражения (
x
+
1
). Таким образом, данная функция отображает на любую целочисленную переменную x
значение x
+
1
Функциональные значения — это объекты, следовательно, при желании их можно хранить в переменных. Они также являются функциями, следова
тельно, вы можете вызывать их, используя обычную форму записи вызова функций с применением круглых скобок. Вот как выглядят примеры обоих действий:
val increase = (x: Int) => x + 1
increase(10) // 11 1
Каждое функциональное значение является экземпляром какогонибудь класса, который представляет собой расширение одного из нескольких трейтов
FunctionN
в пакете Scala, например,
Function0
для функций без параметров,
Function1
для функций с одним параметром и т. д. В каждом трейте
FunctionN
имеется метод apply
, используемый для вызова функции.
172 Глава 8 • Функции и замыкания
Если нужно, чтобы в функциональном литерале использовалось более одной инструкции, то следует заключить его тело в фигурные скобки и поместить каждую инструкцию на отдельной строке, сформировав блок. Как и в случае создания метода, когда вызывается функциональное значение, будут вы
полнены все инструкции и значением, возвращаемым из функции, станет значение, получаемое при вычислении последнего выражения:
val addTwo = (x: Int) =>
val increment = 2
x + increment addTwo(10) // 12
Итак, вы увидели все основные составляющие функциональных литералов и функциональных значений. Возможности их применения обеспечиваются многими библиотеками Scala. Например, методом foreach
, доступным для всех коллекций
1
. Он получает функцию в качестве аргумента и вызывает ее в отношении каждого элемента своей коллекции. А вот как его можно ис
пользовать для вывода всех элементов списка:
scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
val someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)
scala> someNumbers.foreach((x: Int) => println(x))
-11
-10
-5 0
5 10
В другом примере также используется имеющийся у типов коллекций метод filter
. Он выбирает те элементы коллекции, которые проходят вы
полняемую пользователем проверку с применением функции. Например, фильтрацию можно осуществить с помощью функции
(x:
Int)
=>
x
>
0
. Она отображает положительные целые числа в true
, а все остальные числа — в false
. Метод filter можно задействовать следующим образом:
scala> someNumbers.filter((x: Int) => x > 0)
val res4: List[Int] = List(5, 10)
Более подробно о методах, подобных foreach и filter
, поговорим позже.
В главе 14 рассматривается их использование в классе
List
, а в главе 15 — применение с другими типами коллекций.
1
Метод foreach определен в трейте
Iterable
, который является супертрейтом для
List
,
Set
,
Array и
Map
. Подробности — в главе 15.
8 .5 . Синтаксис заместителя 173
8 .4 . Краткие формы функциональных литералов
В Scala есть несколько способов избавления от избыточной информации, позволяющих записывать функциональные литералы более кратко. Не упу
скайте такой возможности, поскольку она позволяет вам убрать из своего кода ненужный хлам. Один из способов писать функциональные литералы более лаконично заключается в отбрасывании типов параметров. При этом предыдущий пример с фильтром может быть написан следующим образом:
scala> someNumbers.filter((x) => x > 0)
val res5: List[Int] = List(5, 10)
Компилятор Scala знает, что переменная x
должна относиться к целым чис
лам, поскольку видит, что вы сразу же применяете функцию для фильтрации списка целых чисел, на который ссылается объект someNumbers
. Это называ
ется целевой типизацией, поскольку целевому использованию выражения — в данном случае в качестве аргумента для someNumbers.filter()
— разрешено влиять на типизацию выражения — в данном случае на определение типа параметра x
. Подробности целевой типизации нам сейчас неважны. Вы мо
жете просто приступить к написанию функционального литерала без типа аргумента и, если компилятор не сможет в нем разобраться, добавить тип.
Со временем вы начнете понимать, в каких ситуациях компилятор сможет решить эту загадку, а в каких — нет.
Второй способ избавления от избыточных символов заключается в отказе от круглых скобок вокруг параметра, тип которого будет выведен автоматиче
ски. В предыдущем примере круглые скобки вокруг x
совершенно излишни:
scala> someNumbers.filter(x => x > 0)
val res6: List[Int] = List(5, 10)
8 .5 . Синтаксис заместителя
Можно сделать функциональный литерал еще короче, воспользовавшись знаком подчеркивания в качестве заместителя для одного или нескольких параметров при условии, что каждый параметр появляется внутри функцио
нального литерала только один раз. Например,
_
>
0
— очень краткая форма записи для функции, проверяющей, что значение больше нуля:
scala> someNumbers.filter(_ > 0)
val res7: List[Int] = List(5, 10)
174 Глава 8 • Функции и замыкания
Знак подчеркивания можно рассматривать как бланк, который следует за
полнить. Он будет заполнен аргументом функции при каждом ее вызове.
Например, при условии, что переменная someNumbers была здесь инициали
зирована значением
List(-11,
-10,
-5,
0,
5,
10)
, метод filter заменит бланк в
_
>
0
сначала значением
-11
, получив
-11
>
0
, затем значением
-10
, получив
-10
>
0
, затем значением
-5
, получив
-5
>
0
, и так далее до конца списка
List
Таким образом, функциональный литерал
_
>
0
является, как здесь показано, эквивалентом немного более пространного литерала x
=>
x
>
0
:
scala> someNumbers.filter(x => x > 0)
val res8: List[Int] = List(5, 10)
Иногда при использовании знаков подчеркивания в качестве заместителей параметров у компилятора может оказаться недостаточно информации для вывода неуказанных типов параметров. Предположим, к примеру, что вы сами написали
_
+
_
:
scala> val f = _ + _
ˆ
error: missing parameter type for expanded function
((x$1:, x$2) => x$1.$plus(x$2))
В таких случаях нужно указать типы, используя двоеточие:
scala> val f = (_: Int) + (_: Int)
val f: (Int, Int) => Int = $$Lambda$1075/1481958694@289fff3c scala> f(5, 10)
val res9: Int = 15
Следует заметить, что
_
+
_
расширяется в литерал для функции, получа
ющей два параметра. Поэтому сокращенную форму можно использовать, только если каждый параметр применяется в функциональном литерале не более одного раза. Несколько знаков подчеркивания означают наличие нескольких параметров, а не многократное использование одного и того же параметра. Первый знак подчеркивания представляет первый параметр, второй знак — второй параметр, третий знак — третий параметр и т. д.
8 .6 . Частично примененные функции
В Scala, когда при вызове функции в нее передаются любые необходимые аргументы, вы применяете эту функцию к этим аргументам. Например, если есть следующая функция:
def sum(a: Int, b: Int, c: Int) = a + b + c
// возвращение строчки в виде последовательности def makeRowSeq(row: Int) =
for col <- 1 to 10 yield val prod = (row * col).toString val padding = " " * (4 - prod.length)
padding + prod
// возвращение строчки в виде строкового значения def makeRow(row: Int) = makeRowSeq(row).mkString
// возвращение таблицы в виде строковых значений, по одному значению
// на каждую строчку
166 Глава 7 • Встроенные управляющие конструкции def multiTable() =
val tableSeq = // последовательность строк из строчек таблицы for row <– 1 to 10
yield makeRow(row)
tableSeq.mkString("\n")
На наличие в листинге 7.18 императивного стиля указывают два момента.
Первый — побочный эффект от вызова printMultiTable
— вывод таблицы умножения на стандартное устройство. В листинге 7.19 функция реорганизо
вана таким образом, чтобы возвращать таблицу умножения в виде строкового значения. Поскольку функция больше не занимается выводом на стандарт
ное устройство, то переименована в multiTable
. Как уже упоминалось, одно из преимуществ функций, не имеющих побочных эффектов, — упрощение их модульного тестирования. Для тестирования printMultiTable понадо
билось бы какимто образом переопределять print и println
, чтобы можно было проверить вывод на корректность. А протестировать multiTable гораздо проще — проверив ее строковой результат.
Второй момент, служащий верным признаком императивного стиля в функ
ции printMultiTable
— ее цикл while и var
переменные. В отличие от этого, в функции multiTable для выражений, вспомогательных функций и вызовов mkString используются val
переменные.
Чтобы облегчить чтение кода, мы выделили две вспомогательные функции: makeRowSeq и makeRow
. Первая использует выражение for
, генератор которого перебирает номера столбцов от 1 до 10. В теле этого выражения вычисляется произведение значения строки на значение столбца, определяется отступ, необходимый для произведения, выдается результат объединения строк отступа и произведения. Результатом выражения for будет последователь
ность (один из подклассов
Seq
), содержащая выданные строки в качестве элементов. Вторая вспомогательная функция, makeRow
, просто вызывает ме
тод mkString в отношении результата, возвращенного функцией makeRowSeq
Этот метод объединяет имеющиеся в последовательности строки, возвращая их в виде одной строки.
Метод multiTable сначала инициализирует tableSeq результатом выполне
ния выражения for
, генератор которого перебирает числа от 1 до 10, чтобы для каждого вызова makeRow получалось строковое значение для данной строки таблицы. Именно эта строка и выдается, вследствие чего результатом выполнения данного выражения for будет последовательность строковых значений, представляющих строки таблицы. Остается лишь преобразовать последовательность строк в одну строку. Для выполнения этой задачи вы
7 .9 . Резюме 167
зывается метод mkString
, и, поскольку ему передается значение "\n"
, мы получаем символ конца строки, вставленный после каждой строки. Передав строку, возвращенную multiTable
, функции println
, вы увидите, что выво
дится точно такая же таблица, как и при вызове функции printMultiTable
:
1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 18 27 36 45 54 63 72 81 90 10 20 30 40 50 60 70 80 90 100 7 .9 . Резюме
Перечень встроенных в Scala управляющих конструкций минимален, но они вполне справляются со своими задачами. Их работа похожа на действия их императивных эквивалентов, но, поскольку им свойственно выдавать зна
чение, они поддерживают и функциональный стиль. Не менее важно и то, что управляющие конструкции оставляют поле деятельности для одной из самых эффективных возможностей Scala — функциональных литералов, которые рассматриваются в следующей главе.
8
Функции и замыкания
По мере роста программ появляется необходимость разбивать их на неболь
шие части, которыми удобнее управлять. Разделение потока управления в Scala реализуется с помощью подхода, знакомого всем опытным програм
мистам: разделение кода на функции. Фактически в Scala предлагается не
сколько способов определения функций, которых нет в Java. Кроме методов, представляющих собой функции, являющиеся частью объектов, есть также функции, вложенные в другие функции, функциональные литералы и функ
циональные значения. В данной главе вам предстоит познакомиться со всеми этими разновидностями функций, имеющимися в Scala.
8 .1 . Методы
Наиболее распространенный способ определения функций — включение их в состав объекта. Такая функция называется методом. В качестве примера в листинге 8.1 показаны два метода, которые вместе считывают данные из файла с заданным именем и выводят строки, длина которых превышает заданную. Перед каждой выведенной строкой указывается имя файла, в ко
тором она появляется.
Листинг 8.1. LongLines с приватным методом processLine object Padding:
def padLines(text: String, minWidth: Int): String =
val paddedLines =
for line <- text.linesIterator yield padLine(line, minWidth)
paddedLines.mkString("\n")
8 .2 . Локальные функции 169
private def padLine(line: String, minWidth: Int): String =
if line.length >= minWidth then line else line + " " * (minWidth - line.length)
Метод padLines принимает в качестве параметров text и minWidth
. Он вы
зывает linesIterator для text
, который возвращает итератор строк в типе данных string
, исключая любые символы окончания строки. Выражение for обрабатывает каждую из этих строк, вызывая вспомогательный метод padLine
. Метод padLine принимает два параметра: minWidth и line
. Он срав
нивает длину строки с заданной шириной и, если длина меньше, добавляет соответствующее количество пробелов в конец строки, чтобы их уравнять.
Пока что это очень похоже на то, что вы делаете в любом объектноориен
тированном языке. Однако понятие функции в Scala является более общим, чем метод. Другие способы выражения функций в Scala будут объяснены в следующих разделах.
8 .2 . Локальные функции
Конструкция метода padLines из предыдущего раздела показала важность принципов разработки, присущих функциональному стилю программиро
вания: программы должны быть разбиты на множество небольших функций с четко определенными задачами. Зачастую отдельно взятая функция весьма невелика. Преимущество подобного стиля — в предоставлении програм
мисту множества строительных блоков, позволяющих составлять гибкие композиции для решения более сложных задач. Такие строительные блоки должны быть довольно простыми, чтобы с ними было легче разобраться по отдельности.
Подобный подход выявляет одну проблему: имена всех вспомогательных функций могут засорять пространство имен программы. В REPL это прояв
ляется не так ярко, но по мере упаковки функций в многократно применяе
мые классы и объекты желательно будет скрыть вспомогательные функции от пользователей класса. Зачастую, будучи отдельно взятыми, такие функ
ции не имеют особого смысла, и возникает желание сохранять достаточную степень гибкости, чтобы можно было удалить вспомогательные функции, если позже класс будет переписан.
В Java основной инструмент для этого — приватный метод. Как было по
казано в листинге 8.1, точно такой же подход с использованием приват
ного метода работает и в Scala, но в этом языке предлагается и еще один: можно определить функцию внутри другой функции. Подобно локальным
170 Глава 8 • Функции и замыкания переменным, такие локальные функции видны только в пределах своего приватного блока. Рассмотрим пример:
def padLines(text: String, minWidth: Int): String =
def padLine(line: String, minWidth: Int): String =
if line.length >= minWidth then line else line + " " * (minWidth - line.length)
val paddedLines =
for line <- text.linesIterator yield padLine(line, minWidth)
paddedLines.mkString("\n")
Здесь реорганизована исходная версия
Padding
, показанная в листин
ге 8.1: приватный метод padLine был превращен в локальную функцию для padLines
. Для этого был удален модификатор private
, который мог быть применен (и нужен) только для членов класса, а определение функции padLine было помещено внутрь определения функции padLines
. В качестве локальной функция padLine находится в области видимости внутри функции padLines
, за пределами которой она недоступна.
Но теперь, когда функция padLine определена внутри функции padLines
, появилась возможность улучшить коечто еще. Вы заметили, что minWidth передается вспомогательной функции, как и прежде? В этом нет никакой необходимости, поскольку локальные функции могут получать доступ к па
раметрам охватывающей их функции. Как показано в листинге 8.2, можно просто воспользоваться параметрами внешней функции padLines
Листинг 8.2. LongLines с локальной функцией processLine object Padding:
def padLines(text: String, minWidth: Int): String =
def padLine(line: String): String =
if line.length >= minWidth then line else line + " " * (minWidth - line.length)
val paddedLines =
for line <- text.linesIterator yield padLine(line)
paddedLines.mkString("\n")
Заметили, как упростился код? Такое использование параметров охваты
вающей функции — широко распространенный и весьма полезный пример универсальной вложенности, предоставляемой Scala. Вложенность и области видимости применительно ко всем конструкциям данного языка, включая
8 .3 . Функции первого класса 171
функции, рассматриваются в разделе 7.7. Это довольно простой, но весьма эффективный принцип.
8 .3 . Функции первого класса
В Scala есть функции первого класса. Вы можете не только определить их и вызвать, но и записать в виде безымянных литералов, после чего передать их в качестве значений. Понятие функциональных литералов было введено в главе 2, а их основной синтаксис показан на рис. 2.2.
Функциональный литерал компилируется в дескрипторе методов Java, кото
рый при создании экземпляра во время выполнения программы становится
функциональным значением
1
. Таким образом, разница между функциональ
ными литералами и значениями состоит в том, что первые существуют в ис
ходном коде, а вторые — в виде объектов во время выполнения программы.
Эта разница во многом похожа на разницу между классами (исходным ко
дом) и объектами (создаваемыми во время выполнения программы).
Простой функциональный литерал, прибавляющий к числу единицу, имеет следующий вид:
(x: Int) => x + 1
Сочетание символов
=>
указывает на то, что эта функция превращает сто
ящий слева от данного сочетания параметр (любое целочисленное значе
ние x
) в результат вычисления выражения (
x
+
1
). Таким образом, данная функция отображает на любую целочисленную переменную x
значение x
+
1
Функциональные значения — это объекты, следовательно, при желании их можно хранить в переменных. Они также являются функциями, следова
тельно, вы можете вызывать их, используя обычную форму записи вызова функций с применением круглых скобок. Вот как выглядят примеры обоих действий:
val increase = (x: Int) => x + 1
increase(10) // 11 1
Каждое функциональное значение является экземпляром какогонибудь класса, который представляет собой расширение одного из нескольких трейтов
FunctionN
в пакете Scala, например,
Function0
для функций без параметров,
Function1
для функций с одним параметром и т. д. В каждом трейте
FunctionN
имеется метод apply
, используемый для вызова функции.
172 Глава 8 • Функции и замыкания
Если нужно, чтобы в функциональном литерале использовалось более одной инструкции, то следует заключить его тело в фигурные скобки и поместить каждую инструкцию на отдельной строке, сформировав блок. Как и в случае создания метода, когда вызывается функциональное значение, будут вы
полнены все инструкции и значением, возвращаемым из функции, станет значение, получаемое при вычислении последнего выражения:
val addTwo = (x: Int) =>
val increment = 2
x + increment addTwo(10) // 12
Итак, вы увидели все основные составляющие функциональных литералов и функциональных значений. Возможности их применения обеспечиваются многими библиотеками Scala. Например, методом foreach
, доступным для всех коллекций
1
. Он получает функцию в качестве аргумента и вызывает ее в отношении каждого элемента своей коллекции. А вот как его можно ис
пользовать для вывода всех элементов списка:
scala> val someNumbers = List(-11, -10, -5, 0, 5, 10)
val someNumbers: List[Int] = List(-11, -10, -5, 0, 5, 10)
scala> someNumbers.foreach((x: Int) => println(x))
-11
-10
-5 0
5 10
В другом примере также используется имеющийся у типов коллекций метод filter
. Он выбирает те элементы коллекции, которые проходят вы
полняемую пользователем проверку с применением функции. Например, фильтрацию можно осуществить с помощью функции
(x:
Int)
=>
x
>
0
. Она отображает положительные целые числа в true
, а все остальные числа — в false
. Метод filter можно задействовать следующим образом:
scala> someNumbers.filter((x: Int) => x > 0)
val res4: List[Int] = List(5, 10)
Более подробно о методах, подобных foreach и filter
, поговорим позже.
В главе 14 рассматривается их использование в классе
List
, а в главе 15 — применение с другими типами коллекций.
1
Метод foreach определен в трейте
Iterable
, который является супертрейтом для
List
,
Set
,
Array и
Map
. Подробности — в главе 15.
8 .5 . Синтаксис заместителя 173
8 .4 . Краткие формы функциональных литералов
В Scala есть несколько способов избавления от избыточной информации, позволяющих записывать функциональные литералы более кратко. Не упу
скайте такой возможности, поскольку она позволяет вам убрать из своего кода ненужный хлам. Один из способов писать функциональные литералы более лаконично заключается в отбрасывании типов параметров. При этом предыдущий пример с фильтром может быть написан следующим образом:
scala> someNumbers.filter((x) => x > 0)
val res5: List[Int] = List(5, 10)
Компилятор Scala знает, что переменная x
должна относиться к целым чис
лам, поскольку видит, что вы сразу же применяете функцию для фильтрации списка целых чисел, на который ссылается объект someNumbers
. Это называ
ется целевой типизацией, поскольку целевому использованию выражения — в данном случае в качестве аргумента для someNumbers.filter()
— разрешено влиять на типизацию выражения — в данном случае на определение типа параметра x
. Подробности целевой типизации нам сейчас неважны. Вы мо
жете просто приступить к написанию функционального литерала без типа аргумента и, если компилятор не сможет в нем разобраться, добавить тип.
Со временем вы начнете понимать, в каких ситуациях компилятор сможет решить эту загадку, а в каких — нет.
Второй способ избавления от избыточных символов заключается в отказе от круглых скобок вокруг параметра, тип которого будет выведен автоматиче
ски. В предыдущем примере круглые скобки вокруг x
совершенно излишни:
scala> someNumbers.filter(x => x > 0)
val res6: List[Int] = List(5, 10)
8 .5 . Синтаксис заместителя
Можно сделать функциональный литерал еще короче, воспользовавшись знаком подчеркивания в качестве заместителя для одного или нескольких параметров при условии, что каждый параметр появляется внутри функцио
нального литерала только один раз. Например,
_
>
0
— очень краткая форма записи для функции, проверяющей, что значение больше нуля:
scala> someNumbers.filter(_ > 0)
val res7: List[Int] = List(5, 10)
174 Глава 8 • Функции и замыкания
Знак подчеркивания можно рассматривать как бланк, который следует за
полнить. Он будет заполнен аргументом функции при каждом ее вызове.
Например, при условии, что переменная someNumbers была здесь инициали
зирована значением
List(-11,
-10,
-5,
0,
5,
10)
, метод filter заменит бланк в
_
>
0
сначала значением
-11
, получив
-11
>
0
, затем значением
-10
, получив
-10
>
0
, затем значением
-5
, получив
-5
>
0
, и так далее до конца списка
List
Таким образом, функциональный литерал
_
>
0
является, как здесь показано, эквивалентом немного более пространного литерала x
=>
x
>
0
:
scala> someNumbers.filter(x => x > 0)
val res8: List[Int] = List(5, 10)
Иногда при использовании знаков подчеркивания в качестве заместителей параметров у компилятора может оказаться недостаточно информации для вывода неуказанных типов параметров. Предположим, к примеру, что вы сами написали
_
+
_
:
scala> val f = _ + _
ˆ
error: missing parameter type for expanded function
((x$1:
В таких случаях нужно указать типы, используя двоеточие:
scala> val f = (_: Int) + (_: Int)
val f: (Int, Int) => Int = $$Lambda$1075/1481958694@289fff3c scala> f(5, 10)
val res9: Int = 15
Следует заметить, что
_
+
_
расширяется в литерал для функции, получа
ющей два параметра. Поэтому сокращенную форму можно использовать, только если каждый параметр применяется в функциональном литерале не более одного раза. Несколько знаков подчеркивания означают наличие нескольких параметров, а не многократное использование одного и того же параметра. Первый знак подчеркивания представляет первый параметр, второй знак — второй параметр, третий знак — третий параметр и т. д.
8 .6 . Частично примененные функции
В Scala, когда при вызове функции в нее передаются любые необходимые аргументы, вы применяете эту функцию к этим аргументам. Например, если есть следующая функция:
def sum(a: Int, b: Int, c: Int) = a + b + c