Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
45 в строке с использованием выражения вида s.replace(';', '.') приводит к возникновению нового строкового объекта, отличающегося от s . То же самое можно сказать подругому: в Java строки неизменяемые, а в Ruby — изменяемые. То есть, рассматривая только строки, можно прийти к выводу, что Java — функциональный язык, а Ruby — нет. Неизменяемая структура данных — один из краеугольных камней функционального программирова ния. В библиотеках Scala в качестве надстроек над соответствующими API Java определяется также множество других неизменяемых типов данных. Например, в Scala имеются неизменяемые списки, кортежи, отображения и множества. Еще один способ утверждения второй идеи функционального программи рования заключается в том, что у методов не должно быть никаких побоч- ных эффектов. Они должны обмениваться данными со своим окружением только путем получения аргументов и возвращения результатов. Например, под это описание подпадает метод replace , принадлежащий Javaклассу String . Он получает строку и два символа и выдает новую строку, где все появления одного символа заменены появлениями второго. Других эффектов от вызова replace нет. Методы, подобные replace , называются ссылочно прозрачными. Это значит, что для любого заданного ввода вызов функции можно заменить его результатом, при этом семантика программы остается неизменной. Функциональные языки заставляют применять неизменяемые структуры данных и ссылочно прозрачные методы. В некоторых функциональных языках это выражено в виде категоричных требований. Scala же дает воз можность выбрать. При желании можно писать программы в императивном стиле — так называется программирование с изменяемыми данными и по бочными эффектами. Но при необходимости в большинстве случаев Scala позволяет с легкостью избежать использования императивных конструкций благодаря существованию хороших функциональных альтернатив. 1 .3 . Почему именно Scala Подойдет ли вам язык Scala? Разбираться и принимать решение придется самостоятельно. Мы считаем, что, помимо хорошей масштабируемости, существует еще множество причин, по которым вам может понравиться про граммирование на Scala. В этом разделе будут рассмотрены четыре наиболее важных аспекта: совместимость, лаконичность, абстракции высокого уровня и расширенная статическая типизация. 46 Глава 1 • Масштабируемый язык Scala — совместимый язык Scala не требует резко отходить от платформы Java, чтобы опередить на шаг этот язык. Scala позволяет повышать ценность уже существующего кода, то есть опираться на то, что у вас уже есть, поскольку он был разработан для достижения беспрепятственной совместимости с Java 1 . Программы на Scala компилируются в байткоды виртуальной машины Java (JVM). Произво дительность при выполнении этих кодов находится на одном уровне с про изводительностью программ на Java. Код Scala может вызывать методы Java, обращаться к полям этого языка, поддерживать наследование от его классов и реализовывать его интерфейсы. Для всего перечисленного не требуются ни специальный синтаксис, ни явные описания интерфейса, ни какойлибо связующий код. По сути, весь код Scala интенсивно использует библиотеки Java, зачастую даже без ведома программистов. Еще один показатель полной совместимости — интенсивное заимствование в Scala типов данных Java. Данные типа Int в Scala представлены в виде имеющегося в Java примитивного целочисленного типа int , соответственно Float представлен как float , Boolean — как boolean и т. д. Массивы Scala отображаются на массивы Java. В Scala из Java позаимствованы и многие стандартные библиотечные типы. Например, тип строкового литерала "abc" в Scala фактически представлен классом java.lang.String , а исключение должно быть подклассом java.lang.Throwable Javaтипы в Scala не только заимствованы, но и «принаряжены» для прида ния им привлекательности. Например, строки в Scala поддерживают такие методы, как toInt или toFloat , которые преобразуют строки в целое число или число с плавающей точкой. То есть вместо Integer.parseInt(str) вы можете написать str.toInt . Как такое возможно без нарушения совмести мости? Класс String в Java определенно не имеет метода toInt ! Фактически у Scala есть очень общее решение для устранения этого противоречия между передовой разработкой и функциональной совместимостью 2 . Scala позволяет определять многофункцио нальные расширения, которые всегда применя ются при выборе несуществующих элементов. В рассматриваемом случае при поиске метода toInt для работы со строковым значением компилятор 1 Изначально существовала реализация Scala, запускаемая на платформе .NET, но она больше не используется. В последнее время все большую популярность на бирает реализация Scala под названием Scala.js, запускаемая на JavaScript. 2 В версии 3.0.0 стандартные расширения реализованы посредством неявных пре образований. В последующих версиях Scala они будут заменены методами расши рения. 1 .3 . Почему именно Scala 47 Scala не найдет такого элемента в классе String . Однако он найдет неявное преобразование, превращающее Javaкласс String в экземпляр Scalaкласса StringOps , в котором такой элемент определен. Затем преобразование будет автоматически применено, прежде чем будет выполнена операция toInt Код Scala также может быть вызван из кода Java. Иногда при этом следует учитывать некоторые нюансы. Scala — более утонченный язык, чем Java, по этому некоторые расширенные функции Scala должны быть закодированы, прежде чем они смогут быть отображены на Java. Scala — лаконичный язык Программы на Scala, как правило, отличаются краткостью. Программисты, работающие с данным языком, отмечают сокращение количества строк почти на порядок по сравнению с Java. Но это можно считать крайним случаем. Бо лее консервативные оценки свидетельствуют о том, что обычная программа на Scala должна умещаться в половину тех строк, которые используются для аналогичной программы на Java. Меньшее количество строк означает не только сокращение объема набираемого текста, но и экономию сил при чтении и осмыслении программ, а также уменьшение количества возможных недочетов. Свой вклад в сокращение количества строк кода вносят сразу несколько факторов. В синтаксисе Scala не используются некоторые шаблонные элементы, отя гощающие программы на Java. Например, в Scala не обязательно применять точки с запятыми. Есть и несколько других областей, где синтаксис Scala менее зашумлен. В качестве примера можно сравнить, как записывается код классов и конструкторов в Java и Scala. В Java класс с конструктором зачастую выглядит следующим образом: class MyClass { // Java private int index; private String name; public MyClass(int index, String name) { this.index = index; this.name = name; } } А в Scala, скорее всего, будет использована такая запись: class MyClass(index: Int, name: String) 48 Глава 1 • Масштабируемый язык Получив указанный код, компилятор Scala создаст класс с двумя приват ными переменными экземпляра (типа Int по имени index и типа String по имени name ) и конструктор, который получает исходные значения для этих переменных в виде параметров. Код данного конструктора проинициализи рует две переменные экземпляра значениями, переданными в качестве па раметров. Короче говоря, в итоге вы получите ту же функциональность, что и у более многословной версии кода на Java 1 . Класс в Scala быстрее пишется и проще читается, а еще — и это наиболее важно — допустить ошибку при его создании значительно труднее, чем при создании класса в Java. Еще один фактор, способствующий лаконичности, — используемый в Scala вывод типов. Повторяющуюся информацию о типе можно отбросить, и тогда программы избавятся от лишнего и их легче будет читать. Но, вероятно, наиболее важный аспект сокращения объема кода — наличие кода, не требующего внесения в программу, поскольку это уже сделано в би блиотеке. Scala предоставляет вам множество инструментальных средств для определения эффективных библиотек, позволяющих выявить и вынести за скобки общее поведение. Например, различные аспекты библиотечных классов можно выделить в трейты, которые затем можно перемешивать про извольным образом. Или же библиотечные методы могут быть параметризо ваны с помощью операций, позволяя вам определять конструкции, которые, по сути, являются вашими собственными управляющими конструкциями. Собранные вместе, эти конструкции позволяют определять библиотеки, со четающие в себе высокоуровневый характер и гибкость. Scala — высокоуровневый язык Программисты постоянно борются со сложностью. Для продуктивного про граммирования нужно понимать код, над которым вы работаете. Чрезмерно сложный код был причиной краха многих программных проектов. К сожа лению, важные программные продукты обычно бывают весьма сложными. Избежать сложности невозможно, но ею можно управлять. Scala помогает управлять сложностью, позволяя повышать уровень абстрак ции в разрабатываемых и используемых интерфейсах. Представим, к при меру, что есть переменная name , имеющая тип String , и нужно определить, 1 Единственное отличие заключается в том, что переменные экземпляра, полученные в случае применения Scala, будут финальными (final). Как сделать их не финаль ными, рассказывается в разделе 10.6. 1 .3 . Почему именно Scala 49 наличествует ли в этой строковой переменной символ в верхнем регистре. До выхода Java 8 приходилось создавать следующий цикл: boolean nameHasUpperCase = false; // Java for (int i = 0; i < name.length(); ++i) { if (Character.isUpperCase(name.charAt(i))) { nameHasUpperCase = true; break; } } А в Scala можно написать такой код: val nameHasUpperCase = name.exists(_.isUpper) Код Java считает строки низкоуровневыми элементами, требующими по символьного перебора в цикле. Код Scala рассматривает те же самые строки как высокоуровневые последовательности символов, в отношении которых можно применять запросы с предикатами. Несомненно, код Scala намного короче и — для натренированного глаза — более понятен, чем код Java. Следовательно, код Scala значительно меньше влияет на общую сложность приложения. Кроме того, уменьшается вероятность допустить ошибку. Предикат _.isUpper — пример используемого в Scala функционального ли терала 1 . В нем дается описание функции, которая получает аргумент в виде символа (представленного знаком подчеркивания) и проверяет, не является ли этот символ буквой в верхнем регистре 2 В Java 8 появилась поддержка лямбда-выражений и потоков (streams), по зволяющая выполнять подобные операции на Java. Вот как это могло бы выглядеть: boolean nameHasUpperCase = // Java 8 или выше name.chars().anyMatch( (int ch) –> Character.isUpperCase((char) ch) ); Несмотря на существенное улучшение по сравнению с более ранними версия ми Java, код Java 8 все же более многословен, чем его эквивалент на языке Scala. Излишняя тяжеловесность кода Java, а также давняя традиция исполь зования в этом языке циклов может натолкнуть многих Javaпрограммистов 1 Функциональный литерал может называться предикатом, если результирующим типом будет Boolean. 2 Такое использование символа подчеркивания в качестве заместителя для аргумен тов рассматривается в разделе 8.5. 50 Глава 1 • Масштабируемый язык на мысль о необходимости новых методов, подобных exists , позволяющих просто переписать циклы и смириться с растущей сложностью кода. В то же время функциональные литералы в Scala действительно восприни маются довольно легко и задействуются очень часто. По мере углубления знакомства со Scala перед вами будет открываться все больше и больше воз можностей для определения и использования собственных управля ющих абстракций. Вы поймете, что это поможет избежать повторений в коде, со храняя лаконичность и чистоту программ. Scala — статически типизированный язык Системы со статической типизацией классифицируют переменные и выра жения в соответствии с видом хранящихся и вычисляемых значений. Scala выделяется как язык своей совершенной системой статической типизации. Обладая системой вложенных типов классов, во многом похожей на име ющуюся в Java, этот язык позволяет вам проводить параметризацию типов с помощью средств обобщенного программирования, комбинировать типы с использованием пересечений и скрывать особенности типов, применяя абстрактные типы 1 . Так формируется прочный фундамент для создания собственных типов, который дает возможность разрабатывать безопасные и в то же время гибкие в использовании интерфейсы. Если вам нравятся динамические языки, такие как Perl, Python, Ruby или Groovy, то вы можете посчитать немного странным факт, что система ста тических типов в Scala упоминается как одна из его сильных сторон. Ведь отсутствие такой системы часто называют основным преимуществом дина мических языков. Наиболее часто, говоря о ее недостатках, приводят такие аргументы, как присущая программам многословность, воспрепятствование свободному самовыражению программистов и невозможность применения конкретных шаблонов динамических изменений программных систем. Но зачастую эти аргументы направлены не против идеи статических типов в целом, а против конкретных систем типов, воспринимаемых как слишком многословные или недостаточно гибкие. Например, Алан Кей, автор языка Smalltalk, однажды заметил: «Я не против типов, но не знаю ни одной бес проблемной системы типов. Так что мне все еще нравится динамическая типизация» 2 1 Обобщенные типы рассматриваются в главе 18, пересечения (например, A с B с C) — в разделе 17.5, а абстрактные типы — в главе 20. 2 Kay A. C. Электронное письмо о значении объектноориентированного програм мирования [Kay03]. 1 .3 . Почему именно Scala 51 В этой книге мы надеемся убедить вас в том, что система типов в Scala далека от проблемной. На самом деле она вполне изящно справляется с двумя обыч ными опасениями, связываемыми со статической типизацией: многословия удается избежать за счет логического вывода типов, а гибкость достигается благодаря сопоставлению с образцом и ряду новых способов записи и со ставления типов. По мере устранения этих препятствий к классическим преимуществам систем статических типов начинают относиться намного более благосклонно. Среди наиболее важных преимуществ можно назвать верифицируемые свойства программных абстракций, безопасный рефакто ринг и более качественное документирование. Верифицируемые свойства. Системы статических типов способны под тверждать отсутствие конкретных ошибок, выявляемых в ходе выполнения программы. Это могут быть следующие правила: булевы значения никогда не складываются с целыми числами; приватные переменные недоступны за пределами своего класса; функции применяются к надлежащему количеству аргументов; в множество строк можно добавлять только строки. Существующие в настоящее время системы статических типов не выявляют ошибки других видов. Например, обычно они не обнаруживают бесконечные функции, нарушение границ массивов или деление на ноль. Вдобавок эти системы не смогут определить несоответствие вашей программы ее специ фикации (при наличии таковой!). Поэтому некоторые отказываются от них, считая не слишком полезными. Аргументация такова: если эти системы могут выявлять только простые ошибки, а модульные тесты обеспечивают более широкий охват, то зачем вообще связываться со статическими типами? Мы считаем, что в этих аргументах упущено главное. Система статических типов, конечно же, не может заменить собой модульное тестирование, од нако может сократить количество необходимых модульных тестов, выявляя некие свойства, которые в противном случае нужно было бы протестировать. А модульное тестирование не способно заменить статическую типизацию. Ведь Эдсгер Дейкстра (Edsger Dijkstra) сказал, что тестирование позволяет убедиться лишь в наличии ошибок, но не в их отсутствии [Dij70]. Гарантии, которые обеспечиваются статической типизацией, могут быть простыми, но это реальные гарантии, не способные обеспечить никакие объемы тести рования. Безопасный рефакторинг. Системы статических типов дают гарантии, позволяющие вам вносить изменения в основной код, будучи совершенно уверенными в благополучном исходе этого действия. Рассмотрим, к при меру, рефакторинг, при котором к методу нужно добавить еще один пара метр. В статически типизированном языке вы можете внести изменения, 52 Глава 1 • Масштабируемый язык перекомпилировать систему и просто исправить те строки, которые вызовут ошибку типа. Сделав это, вы будете пребывать в уверенности, что были найдены все места, требовавшие изменений. То же самое справедливо для другого простого рефакторинга, например изменения имени метода или перемещения метода из одного класса в другой. Во всех случаях проверка статического типа позволяет быть вполне уверенными в том, что работо способность новой системы осталась на уровне работоспособности старой. Документирование. Статические типы — документация программы, про веряемой компилятором на корректность. В отличие от обычного коммен тария, аннотация типа никогда не станет устаревшей (по крайней мере, если содержащий ее исходный файл недавно успешно прошел компиляцию). Более того, компиляторы и интегрированные среды разработки (integrated development environments, IDE) могут использовать аннотации для выдачи более качественной контекстной справки. Например, IDE может вывести на экран все элементы, доступные для выбора, путем определения статическо го типа выражения, которое выбрано, и дать возможность просмотреть все элементы этого типа. Хотя статические типы в целом полезны для документирования програм мы, иногда они могут вызывать раздражение тем, что засоряют ее. Обычно полезным считается документирование тех сведений, которые читателям программы самостоятельно извлечь довольно трудно. Полезно знать, что в методе, определенном так: def f(x: String) = ... аргументы метода f должны принадлежать типу String . В то же время может вызвать раздражение по крайней мере одна из двух аннотаций в следующем примере: val x: HashMap[Int, String] = new HashMap[Int, String]() Понятно, что было бы достаточно показать отношение x к типу HashMap с Int типами в качестве ключей и String типами в качестве значений только один раз, дважды повторять одно и то же нет смысла. В Scala имеется весьма сложная система логического вывода типов, позво ляющая опускать почти всю информацию о типах, которая обычно вызывает раздражение. В предыдущем примере вполне работоспособны и две менее раздражающие альтернативы: val x = new HashMap[Int, String]() val x: Map[Int, String] = new HashMap() |