Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
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). 200 Глава 9 • Управляющие абстракции В качестве конкретного примера представим, будто нужно реализовать кон струкцию утверждения под названием myAssert 1 . Функция myAssert будет получать в качестве ввода функциональное значение и обращаться к флагу, чтобы решить, что делать. Если флаг установлен, то myAssert вызовет пере данную функцию и проверит, что она возвращает true . Если сброшен, то myAssert будет молча бездействовать. Не прибегая к использованию параметров, передаваемых по имени, кон струкцию myAssert можно создать следующим образом: var assertionsEnabled = true def myAssert(predicate: () => Boolean) = if assertionsEnabled && !predicate() then throw new AssertionError С определением все в порядке, но пользоваться им неудобно: myAssert(() => 5 > 3) Конечно, лучше было бы обойтись в функциональном литерале без пустого списка параметров и обозначения => и создать следующий код: myAssert(5 > 3) // Не будет работать из-за отсутствия () => Именно для воплощения задуманного и существуют параметры, передавае мые по имени. Чтобы создать такой параметр, задавать тип параметра нужно с обозначения => , а не с () => . Например, можно заменить в myAssert пара метр predicate параметром, передаваемым по имени, изменив его тип () => Boolean на => Boolean . Как это должно выглядеть, показано в листинге 9.5. Листинг 9.5. Использование параметра, передаваемого по имени def byNameAssert(predicate: => Boolean) = if assertionsEnabled && !predicate then throw new AssertionError Теперь в свойстве, по поводу которого нужно высказать утверждение, мож но избавиться от пустого параметра. В результате этого использование byNameAssert выглядит абсолютно так же, как встроенная управляющая конструкция: byNameAssert(5 > 3) 1 Здесь используется название myAssert , а не assert , поскольку имя assert предостав ляется самим языком Scala. Соответствующее описание будет дано в разделе 25.1. 9 .5 . Передача параметров по имени 201 Тип «по имени» (byname), в котором отбрасывается пустой список параме тров () , допустимо использовать только в отношении параметров. Никаких bynameпеременных или bynameполей не существует. Можно, конечно, удивиться, почему нельзя просто написать функцию myAs- sert , воспользовавшись для ее параметров старым добрым типом Boolean и создав следующий код: def boolAssert(predicate: Boolean) = if assertionsEnabled && !predicate then throw new AssertionError Разумеется, такая формулировка тоже будет работать и код, использующий эту версию boolAssert , будет выглядеть точно так же, как и прежде: boolAs sert(5 > 3) И все же эти два подхода различаются весьма значительным образом. Для параметра boolAssert используется тип Boolean , и потому выраже ние внутри круглых скобок в boolAssert(5 > 3) вычисляется до вызова boolAssert . Выражение 5 > 3 выдает значение true , которое передается в boolAssert . В отличие от этого, поскольку типом параметра predicate функции byNameAssert является => Boolean , выражение внутри круглых скобок в byNameAssert(5 > 3) до вызова byNameAssert не вычисляется. Вме сто этого будет создано функциональное значение, чей метод apply станет вычислять 5 > 3 , и это функциональное значение будет передано функции byNameAssert Таким образом, разница между двумя подходами состоит в том, что при отключении утверждений вам будут видны любые побочные эффекты, которые могут быть в выражении внутри круглых скобок в boolAssert , но byNameAssert это не касается. Например, если утверждения отключены, то попытки утверждать, что x / 0 == 0 , в случае использования boolAssert при ведут к генерации исключения: val x = 5 assertionsEnabled = false boolAssert(x / 0 == 0) java.lang.ArithmeticException: / by zero ... 27 elided Но попытки утверждать это на основе того же самого кода в случае исполь зования byNameAssert не приведут к генерации исключения: byNameAssert(x / 0 == 0) // Возвращается нормально 202 Глава 9 • Управляющие абстракции Резюме В этой главе мы показали, как с помощью богатого инструментария функций Scala строить управляющие абстракции. Функции внутри вашего кода мож но применять для избавления от распространенных шаблонов управления, а чтобы повторно задействовать шаблоны управления, часто встречающиеся в вашем программном коде, можно прибегнуть к функциям высшего порядка из библиотеки Scala. Кроме того, мы обсудили приемы использования кар ринга и параметров, передаваемых по имени, которые позволяют применять в весьма лаконичном синтаксисе собственные функции высшего порядка. В двух последних главах мы рассмотрели довольно много всего, что относит ся к функциям. В следующих нескольких главах вернемся к рассмотрению дополнительных объектноориентированных средств языка. 10 Композиция и наследование В главе 6 мы представили часть основных объектноориентированных аспектов Scala. В текущей продолжим рассматривать эту тему с того места, где остановились в главе 6, и дадим углубленное и гораздо более полное представление об имеющейся в Scala поддержке объектноориентированного программирования. Нам предстоит сравнить два основных вида взаимоотношений между класса ми: композицию и наследование. Композиция означает, что один класс содер жит ссылку на другой и использует класс, на который ссылается, в качестве вспомогательного средства для выполнения своей миссии. Наследование — это отношения «суперкласс/подкласс» («родительский/дочерний класс»). Помимо этих тем, мы рассмотрим абстрактные классы, методы без параметров, расширение классов, переопределение методов и полей, параметрические поля, вызов конструкторов суперкласса, полиморфизм и динамическое свя зывание, финальные члены и классы, а также фабричные объекты и методы. 10 .1 . Библиотека двумерной разметки В качестве рабочего примера в этой главе мы создадим библиотеку для по строения и вывода на экран двумерных элементов разметки. Каждый элемент будет представлен прямоугольником, заполненным текстом. Для удобства библиотека будет предоставлять фабричные методы по имени elem , которые создают новые элементы из переданных данных. Например, вы сможете соз дать элемент разметки, содержащий строку, используя фабричный метод со следующей сигнатурой: elem(s: String): Element |