Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
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 ). |