Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 745
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
81
В данном случае вторая строка кода, jetSet
+=
"Lear"
, фактически является сокращенной формой записи следующего кода:
jetSet = jetSet + "Lear"
Следовательно, во второй строке кода листинга 3.5 var
переменной jetSet присваивается новое множество, содержащее "Boeing"
,
"Airbus"
и "Lear"
Наконец, последняя строка листинга 3.5 определяет, содержит ли множество строку "Cessna"
(как и следовало ожидать, результат — false
).
Если нужно изменяемое множество, то следует, как показано в листинге 3.6, воспользоваться инструкцией
import
Листинг 3.6. Создание, инициализация и использование изменяемого множества import scala.collection.mutable val movieSet = mutable.Set("Spotlight", "Moonlight")
movieSet += "Parasite"
// movieSet теперь содержит: "Spotlight", "Moonlight", "Parasite"
В первой строке данного листинга выполняется импорт scala.collecti- on.mu table
. Инструкция import позволяет использовать простое имя, например
Set
, вместо длинного полного имени. В результате при указа
нии mutable.Set во второй строке компилятор знает, что имеется в виду sca la.col lection.mutable.Set
. В этой строке movieSet инициализиру
ется новым изменяемым множеством, содержащим строки "Spotlight"
и "Moonlight"
. В следующей строке к изменяемому множеству добавляется "Parasite"
, для чего в отношении множества вызывается метод
+=
с переда
чей ему строки "Parasite"
. Как уже упоминалось,
+=
— метод, определенный для изменяемых множеств. При желании можете вместо кода movieSet
+=
"Parasite"
воспользоваться кодом movieSet.+=("Shrek")
1
Рассмотренной до сих пор исходной реализации множеств, которые вы
полняются изменяемыми и неизменяемыми фабричными методами
Set
, скорее всего, будет достаточно для большинства ситуаций. Однако време
нами может потребоваться специальный вид множества. К счастью, при
1
Множество в листинге 3.6 изменяемое, поэтому повторно присваивать значение movieSet не нужно, и данная переменная может относиться к val
переменным.
В отличие от этого, использование метода
+=
с неизменяемым множеством в ли
стинге 3.5 требует повторного присваивания значения переменной jetSet
, поэтому она должна быть var
переменной.
82 Глава 3 • Дальнейшие шаги в Scala этом используется аналогичный синтаксис. Следует просто импортировать нужный класс и применить фабричный метод в отношении его объекта
компаньона. Например, если требуется неизменяемый
HashSet
, то можно сделать следующее:
import scala.collection.immutable.HashSet val hashSet = HashSet("Tomatoes", "Chilies")
val ingredients = hashSet + "Coriander"
// ingredients содержит "Tomatoes", "Chilies", "Coriander"
Еще одним полезным трейтом коллекций в Scala является отображение —
Map
. Как и для множеств, Scala предоставляет изменяемые и неизменяемые версии
Map с применением иерархии классов. Как показано на рис. 3.3, иерар
хия классов для отображений во многом похожа на иерархию для множеств.
В пакете scala.collection есть основной трейт
Map и два трейтанаследника отображения
Map
: изменяемый вариант в scala.collection.mutable и неиз
меняемый в scala.collection.immutable
Реализации
Map
, например
HashMap
реализации в иерархии классов, пока
занной на рис. 3.3, расширяются либо в изменяемый, либо в неизменяемый трейт. Отображения можно создавать и инициализировать, используя фа
бричные методы, подобные тем, что применялись для массивов, списков и множеств.
Рис. 3.3. Иерархия классов для отображений Scala
Шаг 10 . Используем множества и отображения 83
Листинг 3.7. Создание, инициализация и использование изменяемого отображения import scala.collection.mutable val treasureMap = mutable.Map.empty[Int, String]
treasureMap += (1 –> "Go to island.")
treasureMap += (2 –> "Find big X on ground.")
treasureMap += (3 –> "Dig.")
val step2 = treasureMap(2) // " Find big X on ground."
Например, в листинге 3.7 показана работа с изменяемым отображением: в первой строке оно импортируется, затем определяется val
переменная treasureMap
, которая инициализируется пустым изменяемым отображе
нием, имеющим целочисленные ключи и строковые значения. Оно пустое, поскольку вызывается фабричный метод с именем empty и указывается
Int в качестве типа ключа и
String в качестве типа значения
1
. В следующих трех строках к отображению добавляются пары «ключ — значение», для чего ис
пользуются методы
–>
и
+=
. Как уже было показано, компилятор Scala преоб
разует выражения бинарных операций вида
1
–>
"Go to island."
в код
(1).–>
("Go to island.")
. Следовательно, когда указывается
1
–>
"Go to island."
, фактически в отношении объекта
1
вызывается метод по имени
–>
, которому передается строка со значением "Go to island."
. Метод
–>
, который можно вызвать в отношении любого объекта в программе Scala, возвращает двух
элементный кортеж, содержащий ключ и значение
2
. Затем этот кортеж пере
дается методу
+=
объекта отображения, на который ссылается treasureMap
И наконец, в последней строке ищется значение, соответствующее клю
чу
2
в treasureMap
. После выполнения этого кода переменная step2
будет ссылаться на "Find big
X
on ground"
Если отдать предпочтение неизменяемому отображению, то ничего импор
тировать не нужно, поскольку это отображение используется по умолчанию.
Пример показан в листинге 3.8.
1
Явная параметризация типа "[Int,
String]"
требуется в листинге 3.7 изза того, что без какоголибо значения, переданного фабричному методу, компилятор не в состоянии выполнить логический вывод типов параметров отображения. В от
личие от этого компилятор может выполнить вывод типов параметров из значений, переданных фабричному методу map, показанному в листинге 3.8, поэтому явного указания типов параметров там не требуется.
2
Механизм Scala, позволяющий вызывать такие методы, как
–>
для объектов, которые не объявляют их напрямую, называется методом расширения. Он будет рассмотрен в главе 22.
84 Глава 3 • Дальнейшие шаги в Scala
Листинг 3.8. Создание, инициализация и использование неизменяемого отображения val romanNumeral = Map(
1 –> "I", 2 –> "II", 3 –> "III", 4 –> "IV", 5 –> "V"
)
val four = romanNumeral(4) // "IV"
Учитывая отсутствие импортирования, при указании
Map в первой строке данного листинга вы получаете используемый по умолчанию экземпляр класса scala.collection.immutable.Map
. Фабричному методу отображения передаются пять кортежей «ключ — значение», а он возвращает неизменяе
мое
Map
отображение, содержащее эти переданные пары. Если запустить код, показанный в листинге 3.8, то переменная
4
будет ссылаться на
IV
Шаг 11 . Учимся распознавать функциональный стиль
Как упоминалось в главе 1, Scala позволяет программировать в императив
ном стиле, но побуждает вас переходить преимущественно к функциональ
ному. Если к Scala вы пришли, имея опыт работы в императивном стиле, к примеру, вам приходилось программировать на Java, то одной из основных возможных сложностей станет программирование в функциональном стиле.
Мы понимаем, что поначалу этот стиль может быть неизвестен, и в данной книге стараемся перевести вас из одного состояния в другое. От вас также потребуются некоторые усилия, которые мы настоятельно рекомендуем при
ложить. Мы уверены, что при наличии опыта работы в императивном стиле изучение программирования в функциональном позволит вам не только стать более квалифицированным программистом на Scala, но и расширит ваш кругозор, сделав вас более ценным программистом в общем смысле.
Сначала нужно усвоить разницу между двумя стилями, отражающуюся в коде. Один верный признак заключается в том, что если код содержит var
переменные, то он, вероятнее всего, написан в императивном стиле.
Если он вообще не содержит var
переменных, то есть включает только val
переменные, то, вероятнее всего, он написан в функциональном стиле.
Следовательно, один из способов приблизиться к последнему — попытаться обойтись в программах без var
переменных.
Обладая багажом императивности, то есть опытом работы с такими языка
ми, как Java, C++ или C#, var
переменные можно рассматривать в качестве обычных, а val
переменные — в качестве переменных особого вида. В то же
Шаг 11 . Учимся распознавать функциональный стиль 85
время, если у вас имеется опыт работы в функциональном стиле на таких языках, как Haskell, OCaml или Erlang, val
переменные можно представ
лять как обычные, а var
переменные — как некое кощунственное обращение с кодом. Но с точки зрения Scala val
и var
переменные — всего лишь два разных инструмента в вашем арсенале средств и оба одинаково полезны и не отвергаемы. Scala побуждает вас к использованию val
переменных, но, по сути, дает возможность применять тот инструмент, который лучше подходит для решаемой задачи. И тем не менее, даже будучи согласными с подобной философией, вы поначалу можете испытывать трудности, связанные с из
бавлением от var
переменных в коде.
Рассмотрим позаимствованный из главы 2 пример цикла while
, в котором используется var
переменная, означающая, что он выполнен в императивном стиле:
def printArgs(args: List[String]): Unit =
var i = 0
while i < args.length do println(args(i))
i += 1
Вы можете преобразовать этот код — придать ему более функциональный стиль, отказавшись от использования var
переменной, например, так:
def printArgs(args: List[String]): Unit =
for arg <- args do println(arg)
или вот так:
def printArgs(args: List[String]): Unit =
args.foreach(println)
В этом примере демонстрируется одно из преимуществ программирования с меньшим количеством var
переменных. Код после рефакторинга (более функциональный) выглядит понятнее, он более лаконичен, и в нем труднее допустить какиелибо ошибки, чем в исходном (более императивном) коде.
Причина навязывания в Scala функционального стиля заключается в том, что он помогает создавать более понятный код, при написании которого труднее ошибиться.
Но вы можете пойти еще дальше. Метод после рефакторинга printArgs нельзя отнести к чисто функциональным, поскольку у него имеются побоч
ные эффекты. В данном случае такой эффект — вывод в поток стандартного устройства вывода. Признаком функции, имеющей побочные эффекты,
86 Глава 3 • Дальнейшие шаги в Scala выступает то, что результирующим типом у нее является
Unit
. Если функция не возвращает никакое интересное значение, о чем, собственно, и свиде
тельствует результирующий тип
Unit
, то единственный способ внести этой функцией какоелибо изменение в окружающий мир — проявить некий побочный эффект. Более функциональным подходом будет определение метода, который форматирует передаваемые аргументы в целях их после
дующего вывода и, как показано в листинге 3.9, просто возвращает отфор
матированную строку.
Листинг 3.9. Функция без побочных эффектов или var-переменных def formatArgs(args: List[String]) = args.mkString("\n")
Теперь вы действительно перешли на функциональный стиль: нет ни побоч
ных эффектов, ни var
переменных. Метод mkString
, который можно вызвать в отношении любой коллекции, допускающей последовательный перебор элементов (включая массивы, списки, множества и отображения), возвра
щает строку, состоящую из результата вызова метода toString в отношении каждого элемента, с разделителями из переданной строки. Таким образом, если args содержит три элемента,
"zero"
,
"one"
и "two"
, то метод formatArgs возвращает "zero\none\ntwo"
. Разумеется, эта функция, в отличие от мето
дов printArgs
, ничего не выводит, но в целях выполнения данной работы ее результаты можно легко передать функции println
:
println(formatArgs(args))
Каждая полезная программа, вероятнее всего, будет иметь какиелибо по
бочные эффекты. Отдавая предпочтение методам без побочных эффектов, вы будете стремиться к разработке программ, в которых такие эффекты све
дены к минимуму. Одним из преимуществ такого подхода станет упрощение тестирования ваших программ.
Например, чтобы протестировать любой из трех показанных ранее в этом разделе методов printArgs
, вам придется переопределить метод println
, перехватить передаваемый ему вывод и убедиться в том, что он соответствует вашим ожиданиям. В отличие от этого функцию formatArgs можно проте
стировать, просто проверяя ее результат:
val res = formatArgs(List("zero", "one", "two"))
assert(res == "zero\none\ntwo")
Имеющийся в Scala метод assert проверяет переданное ему буле
во выражение и, если последнее вычисляется в false
, выдает ошибку
AssertionError
. Если же переданное булево выражение вычисляется
Шаг 12 . Преобразование с отображениями и for-yield 87
в true
, то метод просто молча возвращает управление вызвавшему его коду.
Более подробно о тестах, проводимых с помощью assert
, и тестировании речь пойдет в главе 25.
И всетаки нужно иметь в виду: ни var
переменные, ни побочные эффекты не следует рассматривать как нечто абсолютно неприемлемое. Scala не явля
ется чисто функциональным языком, заставляющим вас программировать в функциональном стиле. Scala — гибрид императивного и функционального языков. Может оказаться, что в некоторых ситуациях для решения текущей задачи больше подойдет императивный стиль, и тогда вы должны прибег
нуть к нему без всяких колебаний. Но чтобы помочь вам разобраться в про
граммировании без использования var
переменных, в главе 7 мы покажем множество конкретных примеров кода с использованием var
переменных и рассмотрим способы их преобразования в val
переменные.
Сбалансированный подход Scala-программистов
Старайтесь отдавать предпочтение val
переменным, неизменяе
мым объектам и методам без побочных эффектов. Используйте var
переменные, изменяемые объекты и методы с побочными эффектами тогда, когда у вас есть конкретная необходимость и обоснование для их использования.
Шаг 12 . Преобразование с отображениями и for-yield
При программировании в императивном стиле вы видоизменяете суще
ствующие структуры данных до тех пор, пока не достигнете цели алгоритма.
В функциональном стиле для достижения цели вы преобразуете неизменя
емые структуры данных в новые.
Важным методом, упрощающим функциональные преобразования неиз
меняемых коллекций, является map
. Как и foreach
, map принимает функцию в качестве параметра. Но в отличие от foreach
, который использует пере
данную функцию для выполнения побочного эффекта для каждого элемента, map использует переданную функцию для преобразования каждого элемента в новое значение. Результатом работы map является новая коллекция, содер
жащая эти новые значения. Например, учитывая этот список строк: val adjectives = List("One", "Two", "Red", "Blue")
88 Глава 3 • Дальнейшие шаги в Scala вы можете преобразовать его в новый список из новых строк, например:
val nouns = adjectives.map(adj => adj + " Fish")
// List(One Fish, Two Fish, Red Fish, Blue Fish)
Другой способ выполнить преобразование — использовать выражение for
, в котором вы вводите тело функции с ключевым словом yield вместо do
:
val nouns =
for adj <- adjectives yield adj + " Fish"
// List(One Fish, Two Fish, Red Fish, Blue Fish)
For-yield дает точно такой же результат, что и map
, потому что компилятор преобразует выражение for-yield в вызов map
1
. Поскольку список, воз
вращаемый map
, содержит значения, созданные переданной функцией, тип элементов возвращаемого списка будет такой же, как и результат функции.
В предыдущем примере переданная функция возвращает строку, поэтому map возвращает
List[String]
. Если функция, переданная map
, приводит к друго
му типу, то список, возвращаемый map
, будет содержать этот тип в качестве типа элемента. Например, ниже функция map преобразует строку в целое число, равное длине каждого элемента строки. Следовательно, результатом map является новый
List[Int]
, содержащий эти длины:
val lengths = nouns.map(noun => noun.length)
// List(8, 8, 8, 9)
Как и раньше, вы также можете использовать выражение for-yield для до
стижения того же преобразования:
val lengths =
for noun <- nouns yield noun.length
// List(8, 8, 8, 9)
Метод map присутствует во многих типах, не только в
List
. Это позволяет использовать выражения со многими типами. Одним из примеров является
Vector
— неизменяемая последовательность, обеспечивающая «фактически фиксированное время» для всех своих операций. Поскольку
Vector пред
лагает метод map с соответствующей сигнатурой, вы можете выполнять те же виды функциональных преобразований в
Vectors
, что и в
Lists
, либо напрямую вызывая map
, либо используя for-yield
. Например:
1
Подробности того, как компилятор переписывает выражения, будут даны в раз
деле 7.3.
Шаг 12 . Преобразование с отображениями и for-yield 89
val ques = Vector("Who", "What", "When", "Where", "Why")
val usingMap = ques.map(q => q.toLowerCase + "?")
// Vector(who?, what?, when?, where?, why?)
val usingForYield =
for q <- ques yield q.toLowerCase + "?"
// Vector(who?, what?, when?, where?, why?)
Обратите внимание, что при сопоставлении
List вы получаете новый
List
Когда вы сопоставляете
Vector
, вы получаете обратно новый
Vector
. В даль
нейшем вы поймете, что этот шаблон верен для большинства типов, которые определяют метод map
В качестве последнего примера рассмотрим тип
Option в Scala. Scala ис
пользует
Option для представления необязательного значения, избегая традиционной техники Java, использующей для этой цели null
1
. Параметр
Option
— это либо
Some
, что указывает на то, что значение существует, либо
None
, которое указывает, что значение не существует.
В качестве примера, показывающего
Option в действии, рассмотрим метод find
. Все типы коллекций Scala, включая
List и
Vector
, предлагают find
, который ищет элемент, соответствующий заданному предикату, — функцию, которая принимает аргумент типа элемента и возвращает булево значение.
Тип результата find
—
Option[E]
, где
E
— тип элемента коллекции. Метод find выполняет итерации по элементам коллекции, передавая каждый из них предикату. Если функция возвращает true
, find прекращает итерацию и возвращает этот элемент, заключенный в
Some
. Если find доходит до конца элементов без передачи предикату, он возвращает
None
. Вот несколько при
меров, в которых тип результата поиска всегда
Option[String]
:
val startsW = ques.find(q => q.startsWith("W")) // Some(Who)
val hasLen4 = ques.find(q => q.length == 4) // Some(What)
val hasLen5 = ques.find(q => q.length == 5) // Some(Where)
val startsH = ques.find(q => q.startsWith("H")) // None
Хотя
Option не является коллекцией, он предлагает map
метод
2
. Если
Opti- on является
Some
, который называется «определенным» параметром, map
1
В Java 8 к стандартной библиотеке был добавлен тип
Optional
, но многие суще
ствующие библиотеки Java попрежнему используют null для обозначения от
сутствующего необязательного значения.
2
Однако
Option можно представить как набор, который содержит либо ноль (случай
None
) элементов, либо один (случай
Some
).
В данном случае вторая строка кода, jetSet
+=
"Lear"
, фактически является сокращенной формой записи следующего кода:
jetSet = jetSet + "Lear"
Следовательно, во второй строке кода листинга 3.5 var
переменной jetSet присваивается новое множество, содержащее "Boeing"
,
"Airbus"
и "Lear"
Наконец, последняя строка листинга 3.5 определяет, содержит ли множество строку "Cessna"
(как и следовало ожидать, результат — false
).
Если нужно изменяемое множество, то следует, как показано в листинге 3.6, воспользоваться инструкцией
import
Листинг 3.6. Создание, инициализация и использование изменяемого множества import scala.collection.mutable val movieSet = mutable.Set("Spotlight", "Moonlight")
movieSet += "Parasite"
// movieSet теперь содержит: "Spotlight", "Moonlight", "Parasite"
В первой строке данного листинга выполняется импорт scala.collecti- on.mu table
. Инструкция import позволяет использовать простое имя, например
Set
, вместо длинного полного имени. В результате при указа
нии mutable.Set во второй строке компилятор знает, что имеется в виду sca la.col lection.mutable.Set
. В этой строке movieSet инициализиру
ется новым изменяемым множеством, содержащим строки "Spotlight"
и "Moonlight"
. В следующей строке к изменяемому множеству добавляется "Parasite"
, для чего в отношении множества вызывается метод
+=
с переда
чей ему строки "Parasite"
. Как уже упоминалось,
+=
— метод, определенный для изменяемых множеств. При желании можете вместо кода movieSet
+=
"Parasite"
воспользоваться кодом movieSet.+=("Shrek")
1
Рассмотренной до сих пор исходной реализации множеств, которые вы
полняются изменяемыми и неизменяемыми фабричными методами
Set
, скорее всего, будет достаточно для большинства ситуаций. Однако време
нами может потребоваться специальный вид множества. К счастью, при
1
Множество в листинге 3.6 изменяемое, поэтому повторно присваивать значение movieSet не нужно, и данная переменная может относиться к val
переменным.
В отличие от этого, использование метода
+=
с неизменяемым множеством в ли
стинге 3.5 требует повторного присваивания значения переменной jetSet
, поэтому она должна быть var
переменной.
82 Глава 3 • Дальнейшие шаги в Scala этом используется аналогичный синтаксис. Следует просто импортировать нужный класс и применить фабричный метод в отношении его объекта
компаньона. Например, если требуется неизменяемый
HashSet
, то можно сделать следующее:
import scala.collection.immutable.HashSet val hashSet = HashSet("Tomatoes", "Chilies")
val ingredients = hashSet + "Coriander"
// ingredients содержит "Tomatoes", "Chilies", "Coriander"
Еще одним полезным трейтом коллекций в Scala является отображение —
Map
. Как и для множеств, Scala предоставляет изменяемые и неизменяемые версии
Map с применением иерархии классов. Как показано на рис. 3.3, иерар
хия классов для отображений во многом похожа на иерархию для множеств.
В пакете scala.collection есть основной трейт
Map и два трейтанаследника отображения
Map
: изменяемый вариант в scala.collection.mutable и неиз
меняемый в scala.collection.immutable
Реализации
Map
, например
HashMap
реализации в иерархии классов, пока
занной на рис. 3.3, расширяются либо в изменяемый, либо в неизменяемый трейт. Отображения можно создавать и инициализировать, используя фа
бричные методы, подобные тем, что применялись для массивов, списков и множеств.
Рис. 3.3. Иерархия классов для отображений Scala
Шаг 10 . Используем множества и отображения 83
Листинг 3.7. Создание, инициализация и использование изменяемого отображения import scala.collection.mutable val treasureMap = mutable.Map.empty[Int, String]
treasureMap += (1 –> "Go to island.")
treasureMap += (2 –> "Find big X on ground.")
treasureMap += (3 –> "Dig.")
val step2 = treasureMap(2) // " Find big X on ground."
Например, в листинге 3.7 показана работа с изменяемым отображением: в первой строке оно импортируется, затем определяется val
переменная treasureMap
, которая инициализируется пустым изменяемым отображе
нием, имеющим целочисленные ключи и строковые значения. Оно пустое, поскольку вызывается фабричный метод с именем empty и указывается
Int в качестве типа ключа и
String в качестве типа значения
1
. В следующих трех строках к отображению добавляются пары «ключ — значение», для чего ис
пользуются методы
–>
и
+=
. Как уже было показано, компилятор Scala преоб
разует выражения бинарных операций вида
1
–>
"Go to island."
в код
(1).–>
("Go to island.")
. Следовательно, когда указывается
1
–>
"Go to island."
, фактически в отношении объекта
1
вызывается метод по имени
–>
, которому передается строка со значением "Go to island."
. Метод
–>
, который можно вызвать в отношении любого объекта в программе Scala, возвращает двух
элементный кортеж, содержащий ключ и значение
2
. Затем этот кортеж пере
дается методу
+=
объекта отображения, на который ссылается treasureMap
И наконец, в последней строке ищется значение, соответствующее клю
чу
2
в treasureMap
. После выполнения этого кода переменная step2
будет ссылаться на "Find big
X
on ground"
Если отдать предпочтение неизменяемому отображению, то ничего импор
тировать не нужно, поскольку это отображение используется по умолчанию.
Пример показан в листинге 3.8.
1
Явная параметризация типа "[Int,
String]"
требуется в листинге 3.7 изза того, что без какоголибо значения, переданного фабричному методу, компилятор не в состоянии выполнить логический вывод типов параметров отображения. В от
личие от этого компилятор может выполнить вывод типов параметров из значений, переданных фабричному методу map, показанному в листинге 3.8, поэтому явного указания типов параметров там не требуется.
2
Механизм Scala, позволяющий вызывать такие методы, как
–>
для объектов, которые не объявляют их напрямую, называется методом расширения. Он будет рассмотрен в главе 22.
84 Глава 3 • Дальнейшие шаги в Scala
Листинг 3.8. Создание, инициализация и использование неизменяемого отображения val romanNumeral = Map(
1 –> "I", 2 –> "II", 3 –> "III", 4 –> "IV", 5 –> "V"
)
val four = romanNumeral(4) // "IV"
Учитывая отсутствие импортирования, при указании
Map в первой строке данного листинга вы получаете используемый по умолчанию экземпляр класса scala.collection.immutable.Map
. Фабричному методу отображения передаются пять кортежей «ключ — значение», а он возвращает неизменяе
мое
Map
отображение, содержащее эти переданные пары. Если запустить код, показанный в листинге 3.8, то переменная
4
будет ссылаться на
IV
Шаг 11 . Учимся распознавать функциональный стиль
Как упоминалось в главе 1, Scala позволяет программировать в императив
ном стиле, но побуждает вас переходить преимущественно к функциональ
ному. Если к Scala вы пришли, имея опыт работы в императивном стиле, к примеру, вам приходилось программировать на Java, то одной из основных возможных сложностей станет программирование в функциональном стиле.
Мы понимаем, что поначалу этот стиль может быть неизвестен, и в данной книге стараемся перевести вас из одного состояния в другое. От вас также потребуются некоторые усилия, которые мы настоятельно рекомендуем при
ложить. Мы уверены, что при наличии опыта работы в императивном стиле изучение программирования в функциональном позволит вам не только стать более квалифицированным программистом на Scala, но и расширит ваш кругозор, сделав вас более ценным программистом в общем смысле.
Сначала нужно усвоить разницу между двумя стилями, отражающуюся в коде. Один верный признак заключается в том, что если код содержит var
переменные, то он, вероятнее всего, написан в императивном стиле.
Если он вообще не содержит var
переменных, то есть включает только val
переменные, то, вероятнее всего, он написан в функциональном стиле.
Следовательно, один из способов приблизиться к последнему — попытаться обойтись в программах без var
переменных.
Обладая багажом императивности, то есть опытом работы с такими языка
ми, как Java, C++ или C#, var
переменные можно рассматривать в качестве обычных, а val
переменные — в качестве переменных особого вида. В то же
Шаг 11 . Учимся распознавать функциональный стиль 85
время, если у вас имеется опыт работы в функциональном стиле на таких языках, как Haskell, OCaml или Erlang, val
переменные можно представ
лять как обычные, а var
переменные — как некое кощунственное обращение с кодом. Но с точки зрения Scala val
и var
переменные — всего лишь два разных инструмента в вашем арсенале средств и оба одинаково полезны и не отвергаемы. Scala побуждает вас к использованию val
переменных, но, по сути, дает возможность применять тот инструмент, который лучше подходит для решаемой задачи. И тем не менее, даже будучи согласными с подобной философией, вы поначалу можете испытывать трудности, связанные с из
бавлением от var
переменных в коде.
Рассмотрим позаимствованный из главы 2 пример цикла while
, в котором используется var
переменная, означающая, что он выполнен в императивном стиле:
def printArgs(args: List[String]): Unit =
var i = 0
while i < args.length do println(args(i))
i += 1
Вы можете преобразовать этот код — придать ему более функциональный стиль, отказавшись от использования var
переменной, например, так:
def printArgs(args: List[String]): Unit =
for arg <- args do println(arg)
или вот так:
def printArgs(args: List[String]): Unit =
args.foreach(println)
В этом примере демонстрируется одно из преимуществ программирования с меньшим количеством var
переменных. Код после рефакторинга (более функциональный) выглядит понятнее, он более лаконичен, и в нем труднее допустить какиелибо ошибки, чем в исходном (более императивном) коде.
Причина навязывания в Scala функционального стиля заключается в том, что он помогает создавать более понятный код, при написании которого труднее ошибиться.
Но вы можете пойти еще дальше. Метод после рефакторинга printArgs нельзя отнести к чисто функциональным, поскольку у него имеются побоч
ные эффекты. В данном случае такой эффект — вывод в поток стандартного устройства вывода. Признаком функции, имеющей побочные эффекты,
86 Глава 3 • Дальнейшие шаги в Scala выступает то, что результирующим типом у нее является
Unit
. Если функция не возвращает никакое интересное значение, о чем, собственно, и свиде
тельствует результирующий тип
Unit
, то единственный способ внести этой функцией какоелибо изменение в окружающий мир — проявить некий побочный эффект. Более функциональным подходом будет определение метода, который форматирует передаваемые аргументы в целях их после
дующего вывода и, как показано в листинге 3.9, просто возвращает отфор
матированную строку.
Листинг 3.9. Функция без побочных эффектов или var-переменных def formatArgs(args: List[String]) = args.mkString("\n")
Теперь вы действительно перешли на функциональный стиль: нет ни побоч
ных эффектов, ни var
переменных. Метод mkString
, который можно вызвать в отношении любой коллекции, допускающей последовательный перебор элементов (включая массивы, списки, множества и отображения), возвра
щает строку, состоящую из результата вызова метода toString в отношении каждого элемента, с разделителями из переданной строки. Таким образом, если args содержит три элемента,
"zero"
,
"one"
и "two"
, то метод formatArgs возвращает "zero\none\ntwo"
. Разумеется, эта функция, в отличие от мето
дов printArgs
, ничего не выводит, но в целях выполнения данной работы ее результаты можно легко передать функции println
:
println(formatArgs(args))
Каждая полезная программа, вероятнее всего, будет иметь какиелибо по
бочные эффекты. Отдавая предпочтение методам без побочных эффектов, вы будете стремиться к разработке программ, в которых такие эффекты све
дены к минимуму. Одним из преимуществ такого подхода станет упрощение тестирования ваших программ.
Например, чтобы протестировать любой из трех показанных ранее в этом разделе методов printArgs
, вам придется переопределить метод println
, перехватить передаваемый ему вывод и убедиться в том, что он соответствует вашим ожиданиям. В отличие от этого функцию formatArgs можно проте
стировать, просто проверяя ее результат:
val res = formatArgs(List("zero", "one", "two"))
assert(res == "zero\none\ntwo")
Имеющийся в Scala метод assert проверяет переданное ему буле
во выражение и, если последнее вычисляется в false
, выдает ошибку
AssertionError
. Если же переданное булево выражение вычисляется
Шаг 12 . Преобразование с отображениями и for-yield 87
в true
, то метод просто молча возвращает управление вызвавшему его коду.
Более подробно о тестах, проводимых с помощью assert
, и тестировании речь пойдет в главе 25.
И всетаки нужно иметь в виду: ни var
переменные, ни побочные эффекты не следует рассматривать как нечто абсолютно неприемлемое. Scala не явля
ется чисто функциональным языком, заставляющим вас программировать в функциональном стиле. Scala — гибрид императивного и функционального языков. Может оказаться, что в некоторых ситуациях для решения текущей задачи больше подойдет императивный стиль, и тогда вы должны прибег
нуть к нему без всяких колебаний. Но чтобы помочь вам разобраться в про
граммировании без использования var
переменных, в главе 7 мы покажем множество конкретных примеров кода с использованием var
переменных и рассмотрим способы их преобразования в val
переменные.
Сбалансированный подход Scala-программистов
Старайтесь отдавать предпочтение val
переменным, неизменяе
мым объектам и методам без побочных эффектов. Используйте var
переменные, изменяемые объекты и методы с побочными эффектами тогда, когда у вас есть конкретная необходимость и обоснование для их использования.
Шаг 12 . Преобразование с отображениями и for-yield
При программировании в императивном стиле вы видоизменяете суще
ствующие структуры данных до тех пор, пока не достигнете цели алгоритма.
В функциональном стиле для достижения цели вы преобразуете неизменя
емые структуры данных в новые.
Важным методом, упрощающим функциональные преобразования неиз
меняемых коллекций, является map
. Как и foreach
, map принимает функцию в качестве параметра. Но в отличие от foreach
, который использует пере
данную функцию для выполнения побочного эффекта для каждого элемента, map использует переданную функцию для преобразования каждого элемента в новое значение. Результатом работы map является новая коллекция, содер
жащая эти новые значения. Например, учитывая этот список строк: val adjectives = List("One", "Two", "Red", "Blue")
88 Глава 3 • Дальнейшие шаги в Scala вы можете преобразовать его в новый список из новых строк, например:
val nouns = adjectives.map(adj => adj + " Fish")
// List(One Fish, Two Fish, Red Fish, Blue Fish)
Другой способ выполнить преобразование — использовать выражение for
, в котором вы вводите тело функции с ключевым словом yield вместо do
:
val nouns =
for adj <- adjectives yield adj + " Fish"
// List(One Fish, Two Fish, Red Fish, Blue Fish)
For-yield дает точно такой же результат, что и map
, потому что компилятор преобразует выражение for-yield в вызов map
1
. Поскольку список, воз
вращаемый map
, содержит значения, созданные переданной функцией, тип элементов возвращаемого списка будет такой же, как и результат функции.
В предыдущем примере переданная функция возвращает строку, поэтому map возвращает
List[String]
. Если функция, переданная map
, приводит к друго
му типу, то список, возвращаемый map
, будет содержать этот тип в качестве типа элемента. Например, ниже функция map преобразует строку в целое число, равное длине каждого элемента строки. Следовательно, результатом map является новый
List[Int]
, содержащий эти длины:
val lengths = nouns.map(noun => noun.length)
// List(8, 8, 8, 9)
Как и раньше, вы также можете использовать выражение for-yield для до
стижения того же преобразования:
val lengths =
for noun <- nouns yield noun.length
// List(8, 8, 8, 9)
Метод map присутствует во многих типах, не только в
List
. Это позволяет использовать выражения со многими типами. Одним из примеров является
Vector
— неизменяемая последовательность, обеспечивающая «фактически фиксированное время» для всех своих операций. Поскольку
Vector пред
лагает метод map с соответствующей сигнатурой, вы можете выполнять те же виды функциональных преобразований в
Vectors
, что и в
Lists
, либо напрямую вызывая map
, либо используя for-yield
. Например:
1
Подробности того, как компилятор переписывает выражения, будут даны в раз
деле 7.3.
Шаг 12 . Преобразование с отображениями и for-yield 89
val ques = Vector("Who", "What", "When", "Where", "Why")
val usingMap = ques.map(q => q.toLowerCase + "?")
// Vector(who?, what?, when?, where?, why?)
val usingForYield =
for q <- ques yield q.toLowerCase + "?"
// Vector(who?, what?, when?, where?, why?)
Обратите внимание, что при сопоставлении
List вы получаете новый
List
Когда вы сопоставляете
Vector
, вы получаете обратно новый
Vector
. В даль
нейшем вы поймете, что этот шаблон верен для большинства типов, которые определяют метод map
В качестве последнего примера рассмотрим тип
Option в Scala. Scala ис
пользует
Option для представления необязательного значения, избегая традиционной техники Java, использующей для этой цели null
1
. Параметр
Option
— это либо
Some
, что указывает на то, что значение существует, либо
None
, которое указывает, что значение не существует.
В качестве примера, показывающего
Option в действии, рассмотрим метод find
. Все типы коллекций Scala, включая
List и
Vector
, предлагают find
, который ищет элемент, соответствующий заданному предикату, — функцию, которая принимает аргумент типа элемента и возвращает булево значение.
Тип результата find
—
Option[E]
, где
E
— тип элемента коллекции. Метод find выполняет итерации по элементам коллекции, передавая каждый из них предикату. Если функция возвращает true
, find прекращает итерацию и возвращает этот элемент, заключенный в
Some
. Если find доходит до конца элементов без передачи предикату, он возвращает
None
. Вот несколько при
меров, в которых тип результата поиска всегда
Option[String]
:
val startsW = ques.find(q => q.startsWith("W")) // Some(Who)
val hasLen4 = ques.find(q => q.length == 4) // Some(What)
val hasLen5 = ques.find(q => q.length == 5) // Some(Where)
val startsH = ques.find(q => q.startsWith("H")) // None
Хотя
Option не является коллекцией, он предлагает map
метод
2
. Если
Opti- on является
Some
, который называется «определенным» параметром, map
1
В Java 8 к стандартной библиотеке был добавлен тип
Optional
, но многие суще
ствующие библиотеки Java попрежнему используют null для обозначения от
сутствующего необязательного значения.
2
Однако
Option можно представить как набор, который содержит либо ноль (случай
None
) элементов, либо один (случай
Some
).
1 ... 6 7 8 9 10 11 12 13 ... 64