Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
36 Введение Исходный код и дополнительные материалы к книге вы найдете по адресу https://booksites .artima .com/programming_in_scala_5ed Исходный код Исходный код, рассматриваемый в данной книге, выпущенный под откры той лицензией в виде ZIPфайла, можно найти на сайте книги: https://booksi- tes .artima .com/programming_in_scala_5ed От издательства Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter .com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! На вебсайте издательства www .piter .com вы найдете подробную информацию о наших книгах. 1 Масштабируемый язык Scala означает «масштабируемый язык» (от англ. scalable language). Это на звание он получил, поскольку был спроектирован так, чтобы расти вместе с запросами своих пользователей. Язык Scala может решать широкий круг задач программирования: от написания небольших скриптов до создания больших систем 1 Scala легко освоить. Он работает на стандартных платформах Java и JavaScript и без проблем взаимодействует с библиотеками обеих платформ. Это довольно хороший язык для написания скриптов, объединяющих существующие би блиотеки. Но он может еще больше проявить себя при построении больших систем и фреймворков из компонентов многократного использования. С технической точки зрения Scala — смесь объектноориентированной и функциональной концепций программирования в статически типизиро ванном языке. Подобный сплав проявляется во многих аспектах Scala — он, вероятно, может считаться более всеобъемлющим, чем другие широко ис пользуемые языки. Когда дело доходит до масштабируемости, два стиля про граммирования дополняют друг друга. Используемые в Scala конструкции функционального программирования упрощают быстрое создание инте ресных компонентов из простых частей. Объектноориентированные кон струкции же облегчают структурирование больших систем и их адаптацию к новым требованиям. Сочетание двух стилей в Scala позволяет создавать новые виды шаблонов программирования и абстракций компонентов. Оно также способствует выработке понятного и лаконичного стиля программиро вания. И благодаря такой гибкости языка программирование на Scala может принести массу удовольствия. 1 Scala произносится как «скала». 38 Глава 1 • Масштабируемый язык В этой вступительной главе мы отвечаем на вопрос «Почему именно Scala?». Мы даем общий обзор структуры Scala и ее обоснование. Прочитав главу, вы получите базовое представление о том, что такое Scala и с какого рода задачами он поможет справиться. Книга представляет собой руководство по языку Scala, однако данную главу нельзя считать частью этого руководства. И если вам не терпится приступить к написанию кода на Scala, то можете сразу перейти к изу чению главы 2. 1 .1 . Язык, который растет вместе с вами Программы различных размеров требуют, как правило, использования разных программных конструкций. Рассмотрим, к примеру, следующую небольшую программу на Scala: var capital = Map("US" –> "Washington", "France" –> "Paris") capital += ("Japan" –> "Tokyo") println(capital("France")) Эта программа устанавливает отображение стран на их столицы, модифи цирует отображение, добавляя новую конструкцию ( "Japan" –> "Tokyo" ), и выводит название столицы, связанное со страной France 1 . В этом примере используется настолько высокоуровневая система записи, что она не загро мождена ненужными точками с запятыми и сигнатурами типов. И действи тельно возникает ощущение использования современного языка скриптов наподобие Perl, Python или Ruby. Одна из общих характеристик этих языков, применимая к данному примеру, — поддержка всеми ими в синтаксисе языка конструкции ассоциативного отображения. Ассоциативные отображения очень полезны, поскольку помогают поддер живать понятность и краткость программ, но порой вам может не подойти их философия «на все случаи жизни», поскольку вам в своей программе нужно управлять свойствами отображений более тонко. При необходимости Scala обеспечивает точное управление, поскольку отображения в нем не являются синтаксисом языка. Это библиотечные абстракции, которые можно расши рять и приспосабливать под свои нужды. В показанной ранее программе вы получите исходную реализацию ото бражения Map , но ее можно будет без особого труда изменить. К примеру, можно указать конкретную реализацию, такую как HashMap или TreeMap , 1 Пожалуйста, не сердитесь на нас, если не сможете разобраться во всех тонкостях этой программы. Объяснения будут даны в двух следующих главах. 1 .1 . Язык, который растет вместе с вами 39 или с помощью модуля параллельных коллекций Scala вызвать метод par для получения отображения ParMap , операции в котором выполняются па раллельно. Можно указать для отображения значение по умолчанию или переопределить любой другой метод созданного вами отображения. Во всех случаях для отображений вполне пригоден такой же простой синтаксис до ступа, как и в приведенном примере. В нем показано, что Scala может обеспечить вам как удобство, так и гибкость. Язык содержит набор удобных конструкций, которые помогают быстро на чать работу и позволяют программировать в приятном лаконичном стиле. В то же время вы всегда сможете перекроить программу под свои требования, поскольку все в ней основано на библиотечных модулях, которые можно выбрать и приспособить под свои нужды. Растут новые типы Эрик Рэймонд (Eric Raymond) в качестве двух метафор разработки про граммных продуктов ввел собор и базар [Ray99]. Под собором понимается почти идеальная разработка, создание которой требует много времени. После сборки она долго остается неизменной. Разработчики же базара, напротив, чтото адаптируют и дополняют каждый день. В книге Рэймонда базар — метафора, описывающая разработку ПО с открытым кодом. Гай Стил (Guy Steele) отметил в докладе о «растущем языке», что аналогичное различие можно применить к структуре языка программирования [Ste99]. Scala больше похож на базар, чем на собор, в том смысле, что спроектирован с расчетом на расширение и адаптацию его теми, кто на нем программирует. Вместо того чтобы предоставлять все конструкции, которые только могут пригодиться в одном всеобъемлющем языке, Scala дает вам инструменты для создания таких конструкций. Рассмотрим пример. Многие приложения нуждаются в целочисленном типе, который при выполнении арифметических операций может становиться произвольно большим без переполнения или циклического перехода в на чало. В Scala такой тип определяется в библиотеке класса scala.math.BigInt Определение использующего этот тип метода, который вычисляет факториал переданного ему целочисленного значения, имеет следующий вид 1 : def factorial(x: BigInt): BigInt = if x == 0 then 1 else x * factorial(x - 1) 1 factorial(x) , или x! в математической записи — результат вычисления 1 * 2 * ∙∙∙ * * x , где для 0! определено значение 1 40 Глава 1 • Масштабируемый язык Теперь, вызвав factorial(30) , вы получите: 265252859812191058636308480000000 Тип BigInt похож на встроенный, поскольку со значениями этого типа можно использовать целочисленные литералы и операторы наподобие * и – Тем не менее это просто класс, определение которого задано в стандартной библиотеке Scala 1 . Если бы класса не было, то любой программист на Scala мог бы запросто написать его реализацию, например создав оболочку для имеющегося в языке Java класса java.math.BigInteger (фактически именно так и реализован класс BigInt в Scala). Конечно, класс Java можно использовать напрямую. Но результат будет не столь приятным: хоть Java и позволяет вам создавать новые типы, они не производят впечатление получающих естественную поддержку языка: import java.math.BigInteger def factorial(x: BigInteger): BigInteger = if x == BigInteger.ZERO then BigInteger.ONE else x.multiply(factorial(x.subtract(BigInteger.ONE))) Тип BigInt — один из многих других числовых типов: больших десятичных чисел, комплексных и рациональных чисел, доверительных интервалов, полиномов, и данный список можно продолжить. В некоторых языках программирования часть этих типов реализуется естественным образом. Например, в Lisp, Haskell и Python есть большие целые числа, в Fortran и Python — комплексные. Но любой язык, в котором пытаются одновре менно реализовать все эти абстракции, разрастается до таких размеров, что становится неуправляемым. Более того, даже существуй подобный язык, нашлись бы приложения, требующие других числовых типов, которые все равно не были бы представлены. Следовательно, подход, при котором пред принимается попытка реализовать все в одном языке, не позволяет получить хорошую масштабируемость. Язык Scala, напротив, дает пользователям воз можность наращивать и адаптировать его в нужных направлениях. Он делает это с помощью определения простых в использовании библиотек, которые производят впечатление средств, естественно реализованных в языке. 1 Scala поставляется со стандартной библиотекой, часть которой будет рассмотре на в кни ге. За дополнительной информацией можно обратиться к имеющейся в библиотеке документации Scaladoc, доступной в дистрибутиве и в интернете по адресу www.scalalang.org. 1 .2 . Почему язык Scala масштабируемый? 41 Растут новые управляющие конструкции Такая расширяемость иллюстрируется стилем AnyFunSuite ScalaTest, попу лярной библиотеки тестирования для Scala. В качестве примера приведем простой тестовый класс, содержащий два теста: class SetSpec extends AnyFunSuite: test("An empty Set should have size 0") { assert(Set.empty.size == 0) } test("Invoking head on an empty Set should fail") { assertThrows[NoSuchElementException] { Set.empty.head } } Мы не ожидаем, что вы сейчас полностью поймете пример AnyFunSuite . Ско рее, что важно в этом примере для темы масштабируемости, так это то, что ни тестовая конструкция, ни синтаксис assertThrows не являются встроенными операциями в Scala. Хотя обе они могут выглядеть и действовать очень по хоже на встроенные управляющие конструкции, на самом деле они являются методами, определенными в библиотеке ScalaTest. Обе эти конструкции полностью независимы от языка программирования Scala. Этот пример показывает, что вы можете «развивать» язык Scala в новых на правлениях, даже таких специализированных, как тестирование программ ного обеспечения. Конечно, для этого нужны опытные архитекторы и про граммисты. Но важно то, что это осуществимо — вы можете разрабатывать и реализовывать абстракции в Scala, которые адресованы радикально новым доменам приложений, но при этом ощущать поддержку родного языка при использовании. 1 .2 . Почему язык Scala масштабируемый? На возможность масштабирования влияет множество факторов, от осо бенностей синтаксиса до структуры абстрактных компонентов. Но если бы потребовалось назвать всего один аспект Scala, который способствует масштабируемости, то мы бы выбрали присущее этому языку сочета ние объектноориентированного и функционального программирования (мы немного слукавили, на самом деле это два аспекта, но они взаимо связаны). 42 Глава 1 • Масштабируемый язык Scala в объединении объектноориентированного и функционального про граммирования в однородную структуру языка пошел дальше всех осталь ных широко известных языков. Например, там, где в других языках объекты и функции — два разных понятия, в Scala функция по смыслу является объектом. Функциональные типы — это классы, которые могут наследо ваться подклассами. Эти особенности могут показаться не более чем тео ретическими, но имеют весьма серьезные последствия для возможностей масштабирования. Фактически ранее упомянутое понятие актора не может быть реализовано без этой унификации функций и объектов. Здесь мы рас смотрим возможные в Scala способы смешивания объектноориентирован ной и функциональной концепций. Scala — объектно-ориентированный язык Развитие объектноориентированного программирования шло весьма успеш но. Появившись в языке Simula в середине 1960х годов и Smalltalk в 1970х, оно теперь доступно в подавляющем большинстве языков. В некоторых областях все полностью захвачено объектами. Точного определения «объ ектной ориентированности» нет, однако объекты явно чемто привлекают программистов. В принципе, мотивация для применения объектноориентированного про граммирования очень проста: все, за исключением самых простых программ, нуждается в определенной структуре. Наиболее понятный путь достижения желаемого результата заключается в помещении данных и операций в свое образные контейнеры. Основной замысел объектноориентированного программирования состоит в придании этим контейнерам полной универ сальности, чтобы в них могли содержаться не только операции, но и данные и чтобы сами они также были элементами, которые могли бы храниться в других контейнерах или передаваться операциям в качестве параметров. Подобные контейнеры называются объектами. Алан Кей (Alan Kay), изобре татель языка Smalltalk, заметил, что таким образом простейший объект имеет принцип построения, аналогичный полноценному компьютеру: под форма лизованным интерфейсом данные в нем сочетаются с операциями [Kay96]. То есть объекты имеют непосредственное отношение к масштабируемости языка: одни и те же технологии применяются к построению как малых, так и больших программ. Хотя долгое время объектноориентированное программирование преобла дало, немногие языки стали последователями Smalltalk по части внедрения этого принципа построения в свое логическое решение. Например, множе 1 .2 . Почему язык Scala масштабируемый? 43 ство языков допускает использование элементов, не являющихся объекта ми, — можно вспомнить имеющиеся в языке Java значения примитивных типов. Или же в них допускается применение статических полей и методов, не входящих в какойлибо объект. Эти отклонения от чистой идеи объектно ориентированного программирования на первый взгляд выглядят вполне безобидными, но имеют досадную тенденцию к усложнению и ограничению масштабирования. В отличие от этого Scala — объектноориентированный язык в чистом виде: каждое значение является объектом и каждая операция — вызовом метода. Например, когда в Scala речь заходит о вычислении 1 + 2 , фактически вызы вается метод по имени + , который определен в классе Int . Можно определять методы с именами, похожими на операторы, а клиенты вашего API смогут с помощью этих методов записать операторы. Когда речь заходит о составлении объектов, Scala проявляется как более со вершенный язык по сравнению с большинством других. В качестве примера приведем имеющиеся в Scala трейты. Они подобны интерфейсам в Java, но могут содержать также реализации методов и даже поля 1 . Объекты создаются путем композиции примесей, при котором к членам класса добавляются чле ны нескольких трейтов. Таким образом, различные аспекты классов могут быть инкапсулированы в разных трейтах. Это выглядит как множественное наследование, но есть разница в конкретных деталях. В отличие от класса трейт может добавить в суперкласс новые функциональные возможности. Это придает трейтам более высокую степень подключаемости по сравнению с классами. В частности, благодаря этому удается избежать возникновения присущих множественному наследованию классических проблем «ром бовидного» наследования, которые возникают, когда один и тот же класс наследуется по нескольким различным путям. Scala — функциональный язык Наряду с тем, что Scala является чистым объектноориентированным язы ком, его можно назвать и полноценным функциональным языком. Идеи функционального программирования старше электронных вычислительных систем. Их основы были заложены в лямбдаисчислении Алонзо Черча (Alonzo Church), разработанном в 1930е годы. Первым языком функцио нального программирования был Lisp, появление которого датируется 1 Начиная с Java 8, у интерфейсов могут быть реализации методов по умолчанию, но они не предлагают всех тех возможностей, которые есть у трейтов языка Scala. 44 Глава 1 • Масштабируемый язык концом 1950х. К другим популярным функциональным языкам относятся Scheme, SML, Erlang, Haskell, OCaml и F#. Долгое время функциональное программирование играло второстепенные роли — будучи популярным в научных кругах, оно не столь широко использовалось в промышленности. Но в последние годы интерес к его языкам и технологиям растет. Функциональное программирование базируется на двух основных идеях. Первая заключается в том, что функции являются значениями первого класса. В функциональных языках функция есть значение, имеющее такой же статус, как целое число или строка. Функции можно передавать в каче стве аргументов другим функциям, возвращать их в качестве результатов из других функций или сохранять в переменных. Вдобавок функцию мож но определять внутри другой функции точно так же, как это делается при определении внутри функции целочисленного значения. И функции можно определять, не присваивая им имен, добавляя в код функциональные лите ралы с такой же легкостью, как и целочисленные, наподобие 42 Функции как значения первого класса — удобное средство абстрагирования, касающееся операций и создания новых управляющих конструкций. Эта универсальность функций обеспечивает более высокую степень выразитель ности, что зачастую приводит к созданию весьма разборчивых и кратких программ. Она также играет важную роль в обеспечении масштабируемости. В качестве примера библиотека тестирования ScalaTest предлагает кон струкцию eventually , получающую функцию в качестве аргумента. Данная конструкция используется следующим образом: val xs = 1 to 3 val it = xs.iterator eventually { it.next() shouldBe 3 } Код внутри eventually , являющийся утверждением, it.next() shouldBe 3 , включает в себя функцию, передаваемую невыполненной в метод eventually Через настраиваемый период времени eventually станет неоднократно выполнять функцию до тех пор, пока утверждение не будет успешно под тверждено. Вторая основная идея функционального программирования заключается в том, что операции программы должны преобразовать входные значения в выходные, а не изменять данные на месте. Чтобы понять разницу, рас смотрим реализацию строк в Ruby и Java. В Ruby строка является масси вом символов. Символы в строке могут быть изменены по отдельности. Например, внутри одного и того же строкового объекта символ точки с запятой в строке можно заменить точкой. А в Java и Scala строка — по следовательность символов в математическом смысле. Замена символа |