Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 776
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
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()
1 .4 . Истоки Scala
1 2 3 4 5 6 7 8 9 ... 64
53
Вывод типа в Scala может заходить довольно далеко. Фактически пользова
тельский код нередко вообще обходится без явного задания типов. Поэтому программы на Scala часто выглядят похожими на программы, написанные на динамически типизированных языках скриптов. Это, в частности, спра
ведливо для прикладного клиентского кода, который склеивается из заранее написанных библиотечных компонентов. Но для них самих это менее харак
терно, поскольку в них зачастую применяются довольно сложные типы, не допускающие гибкого использования таких схем. И это вполне естественно.
Ведь сигнатуры типов элементов, составляющих интерфейс повторно исполь
зуемых компонентов, должны задаваться в явном виде, поскольку составляют существенную часть соглашения между компонентом и его клиентами.
1 .4 . Истоки Scala
На идею создания Scala повлияли многие языки программирования и идеи, выработанные на основе исследований таких языков. Фактически обновле
ния в Scala незначительны — большинство характерных особенностей языка уже применялось в том или ином виде в других языках программирования.
Инновации в Scala появляются в основном из того, как его конструкции сводятся воедино. В этом разделе будут перечислены основные факторы, оказавшие влияние на структуру языка Scala. Перечень не может быть ис
черпывающим, поскольку в дизайне языков программирования так много толковых идей, что перечислить здесь их все просто невозможно.
На внешнем уровне Scala позаимствовал существенную часть синтаксиса у Java и C#, которые, в свою очередь, взяли большинство своих синтаксиче
ских соглашений у C и C++. Выражения, инструкции и блоки — в основном из Java, как, собственно, и синтаксис классов, создание пакетов и импорт
1 1
Главное отличие от Java касается синтаксиса для объявления типов: вместо «Тип переменная», как в Java, задействуется форма «переменная: Тип». Используемый в Scala постфиксный синтаксис типа похож на синтаксис, применяемый в Pascal,
Modula2 или Eiffel. Основная причина такого отклонения имеет отношение к ло
гическому выводу типов, зачастую позволяющему опускать тип переменной или тип возвращаемого методом значения. Легче использовать синтаксис «переменная:
Тип», поскольку двоеточие и тип можно просто не указывать. Но в стиле языка C, применяющем форму «Тип переменная», просто так не указывать тип нельзя, поскольку при этом исчезнет сам признак начала определения. Неуказанный тип в качестве заполнителя требует какоенибудь ключевое слово (C# 3.0, в котором имеется логический вывод типов, для этой цели задействует ключевое слово var).
Такое альтернативное ключевое слово представляется несколько более надуман
ным и менее привычным, чем подход, который используется в Scala.
54 Глава 1 • Масштабируемый язык
Кроме синтаксиса, Scala позаимствовал и другие элементы Java, такие как его основные типы, библиотеки классов и модель выполнения.
Scala многим обязан и другим языкам. Его однородная модель объектов впервые появилась в Smalltalk и впоследствии была принята языком Ruby.
Его идея универсальной вложенности (почти каждую конструкцию в Scala можно вложить в любую другую) реализована также в Algol, Simula, а в по
следнее время в Beta и gbeta. Его принцип единообразного доступа к вызову методов и выбору полей пришел из Eiffel. Его подход к функциональному программированию очень близок по духу к применяемому в семействе языков ML, видными представителями которого являются SML, OCaml и F#. Многие функции высшего порядка в стандартной библиотеке Scala присутствуют также в ML или Haskell. Толчком для появления в Scala не
явных параметров стали классы типов языка Haskell — в более классическом объектноориентированном окружении они дают аналогичные результаты.
Используемая в Scala основная библиотека многопоточного вычисления на основе акторов — Akka — создавалась под сильным влиянием особенностей языка Erlang.
Scala не первый язык, делающий упор на масштабируемость и расширяе
мость. Такое понятие, как расширяемые языки, которые могут охватывать различные области применения, впервые встречается в статье Питера Лэнди
на (Peter Landin) 1966 года (язык, описанный в этой статье, — Iswim — стоит рядом с Lisp как один из первых функциональных языков) [Lan66]. Конкрет
ная идея рассматривать инфиксный оператор как функцию восходит к Iswim и Smalltalk. Другая важная идея — разрешить функциональный литерал
(или блок) в качестве параметра, который позволяет библиотекам опреде
лять управляющие структуры. Опять же это восходит к Iswim и Smalltalk.
И Smalltalk, и Lisp обладают гибким синтаксисом, который широко приме
нялся для создания внутренних специфичных для конкретной предметной области языков. C++ — еще один масштабируемый язык, который можно адаптировать и расширить с помощью перегрузки операторов и его систе
мы шаблонов; по сравнению со Scala он построен на более низкоуровневом, более системно — ориентированном ядре.
Кроме того, Scala не первый язык, объединяющий в себе функциональное и объектноориентированное программирование, хотя, вероятно, в этом направлении продвинулся гораздо дальше прочих. К числу других языков, объединивших некоторые элементы функционального программирования с объектноориентированным, относятся Ruby, Smalltalk и Python. Расши
рения Javaподобного ядра некоторыми функциональными идеями были предприняты на Javaплатформе в Pizza, Nice, MultiJava и самом Java 8.