Книга «Scala. Профессиональное программирование. 5-е изд.»

image Привет, Хаброжители! Мы издали главную книгу по Scala, популярному языку для платформы Java, в котором сочетаются концепции объектно-ориентированного и функционального программирования, благодаря чему он превращается в уникальное и мощное средство разработки.

Этот авторитетный труд, написанный создателями Scala, поможет вам пошагово изучить язык и идеи, лежащие в его основе.

Пятое издание значительно обновлено, чтобы охватить многочисленные изменения, появившиеся в Scala 3.

Об авторах

Мартин Одерски, создатель языка Scala, — профессор в Федеральной политехнической школе Лозанны, Швейцария (EPFL), и основатель Lightbend, Inc. Работает над языками программирования и системами, в частности над темой совмещения объектно-ориентированного и функционального подходов. С 2001 года сосредоточен на проектировании, реализации и улучшении Scala. Внес вклад в разработку Java как соавтор обобщенных типов и создатель текущего эталонного компилятора javac. Мартину было присвоено звание действительного члена ACM.
Лекс Спун — разработчик программного обеспечения в компании Square Inc.1, создающей простое в использовании программное обеспечение для бизнеса и мобильных платежей. Занимался Scala на протяжении двух лет в ходе постдокторантуры в EPFL. Помимо Scala, участвовал в разработке самых разнообразных языков, включая динамический язык Smalltalk, научный язык X10 и логический язык CodeQL.
Билл Веннерс — президент Artima, Inc., занимающейся консалтингом, курсами, книгами и инструментами для работы со Scala. Автор книги Inside the Java Virtual Machine про архитектуру и внутреннее устройство платформы Java. Билл представляет сообщество в Scala Center и является ведущим разработчиком и проектировщиком фреймворка тестирования ScalaTest и библиотеки Scalactic, предназначенной для функционального и объектно-ориентированного программирования.
Фрэнк Соммерс — основатель и президент компании Autospaces Inc., предоставляющей решения для автоматизации рабочих процессов в сфере финансовых услуг. Фрэнк ежедневно работает с языком Scala уже свыше двенадцати лет.

Для кого написана эта книга и почему

Книга в основном рассчитана на программистов, желающих научиться программировать на Scala. Если у вас есть желание создать свой следующий проект на этом языке, то наша книга вам подходит. Кроме того, она должна заинтересовать программистов, которые хотят расширить кругозор, изучив новые концепции. Если вы, к примеру, программируете на Java, то эта книга раскроет для вас множество концепций функционального программирования, а также передовых идей из сферы объектно-ориентированного программирования. Мы уверены: изучение Scala и заложенных в этот язык идей поможет вам повысить свой профессиональный уровень как программиста. Предполагается, что вы уже владеете общими знаниями в области программирования. Scala вполне подходит на роль первого изучаемого языка, однако это не та книга, которая может использоваться для обучения программированию. В то же время вам не нужно быть каким-то особенным знатоком языков программирования. Большинство людей использует Scala на платформе Java, однако наша книга не предполагает, что вы тесно знакомы с языком Java. Но все же мы ожидаем, что Java известен многим читателям, и поэтому иногда сравниваем оба языка, чтобы помочь таким читателям понять разницу.

Пришло время написать какой-нибудь код на Scala. Прежде чем углубиться в руководство по этому языку, мы приведем две обзорные главы по нему и, что наиболее важно, заставим вас приступить к написанию кода. Рекомендуем по мере освоения материала на практике проверить работу всех примеров кода, представленных в этой и последующей главах. Лучше всего приступить к изучению Scala, программируя на данном языке.

Запуск представленных далее примеров возможен с помощью стандартной установки Scala. Чтобы ее осуществить, перейдите по адресу www.scala-lang. org/downloads и следуйте инструкциям для вашей платформы. На этой странице описано несколько способов установки Scala. Будем считать, что вы уже установили двоичные файлы Scala и добавили их в переменную окружения path1, что необходимо для выполнения шагов из этой главы.

Если вы опытный программист, но новичок в Scala, то внимательно прочитайте следующие две главы: в них приводится достаточный объем информации, позволяющий приступить к написанию полезных программ на этом языке. Если же опыт программирования у вас невелик, то часть материалов может показаться чем-то загадочным. Однако не стоит переживать. Чтобы ускорить процесс изучения, нам пришлось обойтись без некоторых подробностей. Более обстоятельные пояснения мы представим в последующих главах. Кроме того, в следующих двух главах дадим ряд сносок с указанием разделов книги, в которых можно найти более подробные объяснения.

Шаг 1. Осваиваем Scala REPL


Самый простой способ начать работу со Scala — использовать Scala REPL, интерактивную оболочку для написания выражений и программ Scala. REPL, который называется scala, оценивает введенные вами выражения и выводит полученное значение. Чтобы его использовать, нужно набрать scala в командной строке:

$ scala
Starting Scala REPL...
scala>


После того как вы наберете выражение, например 1 + 2, и нажмете клавишу Enter:

scala> 1 + 2
REPL выведет на экран:
val res0: Int = 3


Эта строка включает:

  • ключевое слово val, объявляющее переменную;
  • автоматически сгенерированное или определенное пользователем имя для ссылки на вычисленное значение (res0, означающее результат 0);
  • двоеточие (:), за которым следует тип выражения (Int);
  • знак равенства (=);
  • значение, полученное в результате вычисления выражения (3).


Тип Int означает класс Int в пакете scala. Пакеты в Scala аналогичны пакетам в Java — они разбивают глобальное пространство имен на части и предоставляют механизм для сокрытия данных3. Значения класса Int соответствуют int-значениям в Java. Если говорить в общем, то все примитивные типы Java имеют соответствующие классы в пакете scala. Например, scala.Boolean соответствует Java-типу boolean. А scala.Float соответствует Java-типу float. И при компиляции вашего кода Scala в байт-код Java компилятор Scala будет по возможности использовать примитивные типы Java, чтобы обеспечить вам преимущество в производительности при работе с примитивными типами.

Идентификатор resX может использоваться в последующих строках. Например, поскольку ранее для res0 было установлено значение 3, то результат выражения res0×3 будет равен 9:

scala> res0 * 3
val res1: Int = 9


Чтобы вывести на экран необходимое, но недостаточно информативное приветствие Hello, world!, наберите следующую команду:

scala> println("Hello, world!")
Hello, world!


Функция println выводит на стандартное устройство вывода переданную ей строку, подобно тому как это делает System.out.println в Java.

Шаг 2. Объявляем переменные


В Scala имеются две разновидности переменных: val-переменные и var-переменные. Первые аналогичны финальным переменным в Java. После инициализации val-переменная уже никогда не может быть присвоена повторно. В отличие от нее var-переменная аналогична нефинальной переменной в Java и может быть присвоена повторно в течение своего жизненного цикла. Определение val-переменной выглядит так:

scala> val msg = "Hello, world!"
val msg: String = Hello, world!


Эта инструкция вводит в употребление переменную msg в качестве имени для строки «Hello, world!». Типом msg является java.lang.String, поскольку строки в JVM Scala реализуются Java-классом String.

Если вы привыкли объявлять переменные в Java, то в этом примере кода можете заметить одно существенное отличие: в val-определении нигде не фигурируют ни java.lang.String, ни String. Пример демонстрирует логический вывод типов, то есть возможность Scala определять неуказанные типы. В данном случае, поскольку вы инициализировали msg строковым литералом, Scala придет к выводу, что типом msg должен быть String. Когда REPL (или компилятор) Scala хочет выполнить вывод типов, зачастую лучше всего будет позволить ему сделать это, не засоряя код ненужными явными аннотациями типов. Но при желании можете указать тип явно, и, вероятно, иногда это придется делать. Явная аннотация типа может не только гарантировать, что компилятор Scala выведет желаемый тип, но и послужить полезной документацией для тех, кто впоследствии станет читать ваш код. В отличие от Java, где тип переменной указывается перед ее именем, в Scala вы указываете тип переменной после ее имени, отделяя его двоеточием, например:

scala> val msg2: java.lang.String = "Hello again, world!"
val msg2: String = Hello again, world!


Или же, поскольку типы java.lang вполне опознаваемы в программах на Scala по их простым именам, запись можно упростить:

scala> val msg3: String = "Hello yet again, world!"
msg3: String = Hello yet again, world!


Возвратимся к исходной переменной msg. Поскольку она определена, то ею можно воспользоваться в соответствии с вашими ожиданиями, например:

scala> println(msg)
Hello, world!


Учитывая, что msg является val-, а не var-переменной, вы не сможете повторно присвоить ей другое значение. Посмотрите, к примеру, как REPL выражает свое недовольство при попытке сделать следующее:

cala> msg = "Goodbye cruel world!"
1 |msg = "Goodbye cruel world!"
|ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
|Reassignment to val msg


Если необходимо выполнить повторное присваивание, следует воспользоваться var-переменной:

scala> var greeting = "Hello, world!"
var greeting: String = Hello, world!


Как только приветствие станет var-, а не val-переменной, ему можно будет присвоить другое значение. Если, к примеру, чуть позже вы станете более раздражительными, то можете поменять приветствие на просьбу оставить вас в покое:

scala> greeting = "Leave me alone, world!"
greeting: String = Leave me alone, world!


Чтобы ввести в REPL код, который не помещается в одну строку, просто продолжайте набирать код после заполнения первой строки. Если набор кода еще не завершен, то REPL отреагирует установкой на следующей строке вертикальной черты:

scala> val multiLine =
          | "This is the next line."
multiLine: String = This is the next line.


Если вы понимаете, что набрали что-то не так, но REPL все еще ожидает ввода дополнительных данных, вы можете использовать клавиши со стрелками для перемещения вверх, вниз, влево или вправо, чтобы исправить ошибки. Если вы хотите полностью отменить ввод, вы можете выйти, дважды нажав Enter:

scala> val oops =
          |
          |
You typed two blank lines. Starting a new command.
scala>


Далее по тексту мы чаще всего будем опускать подсказку scala, верикальные линии и вывод REPL при успешном вводе, чтобы упростить чтение кода (и облегчить копирование и вставку из электронной книги PDF в REPL).

Шаг 3. Определяем функции


После работы с переменными в Scala вам, вероятно, захотелось написать какие-нибудь функции. Это делается так:

def max(x: Int, y: Int): Int =
if x > y then x
else y


Определение функции начинается с ключевого слова def. После имени функции, в данном случае max, стоит заключенный в круглые скобки перечень параметров, разделенных запятыми. За каждым параметром функции должна следовать аннотация типа, перед которой ставится двоеточие, поскольку компилятор Scala (и REPL, но с этого момента будет упоминаться только компилятор) не выводит типы параметров функции. В данном примере функция по имени max получает два параметра, x и y, и оба они относятся к типу Int. После закрывающей круглой скобки перечня параметров функции max обнаруживается аннотация типа: Int. Она определяет результирующий тип самой функции max. За типом результата функции следует знак равенства и тело функции, которое отделено отступами. В этом случае тело содержит одно выражение if, которое в качестве результата функции max выбирает либо x, либо y, в зависимости от того, что больше. Как показано здесь, выражение if в Scala может приводить к значению, аналогичному тернарному оператору Java. Например, в Scala выражение if x > y then x else y вычисляется точно так же, как выражение (x > y)? x: y в Java. Знак равенства, предшествующий телу функции, дает понять, что с точки зрения функционального мира функция определяет выражение, результатом вычисления которого становится значение. Основная структура функции показана на рис. 2.1.

2436a2vn-g6lik7-txzkbdqphc0.png


Иногда компилятор Scala может потребовать от вас указать результирующий тип функции. Если, к примеру, функция является рекурсивной2, то вы должны указать ее результирующий тип явно. Но в случае с функцией max вы можете не указывать результирующий тип функции — компилятор выведет его самостоятельно3. Кроме того, если функция состоит всего лишь из одного оператора, вы сможете целиком написать ее в одну строку. Таким образом, у вас появляется альтернативный вариант реализации функции max:

def max(x: Int, y: Int) = if x > y then x else y


После того как вы определили функцию, вы можете вызвать ее по имени, например:

val bigger = max(3, 5) // 5


А вот определение функции, которая не принимает никаких параметров и не возвращает какого-либо интересного результата:

scala> def greet() = println("Hello, world!")
def greet(): Unit


Когда определяется функция приветствия greet (), REPL откликается следующим приветствием: def greet (): Unit. Разумеется, слово greet — это имя функции. Пустота в скобках показывает, что функция не получает параметров. А Unit — результирующий тип функции greet. Он показывает, что функция не возвращает никакого интересного значения. Тип Unit в Scala подобен типу void в Java. Фактически каждый метод, возвращающий void в Java, отображается на метод, возвращающий Unit в Scala. Таким образом, методы с результирующим типом Unit выполняются только для того, чтобы проявились их побочные эффекты. В случае с greet () побочным эффектом будет дружеское приветствие, выведенное на стандартное устройство вывода.

При выполнении следующего шага код Scala будет помещен в файл и запущен в качестве скрипта. Если нужно выйти из REPL, то это можно сделать с помощью команды : quit:

scala> :quit
$


Шаг 4. Пишем Scala-скрипты


Несмотря на то что язык Scala разработан, чтобы помочь программистам создавать очень большие масштабируемые системы, он вполне может подойти и для решения менее масштабных задач наподобие написания скриптов. Скрипт — это просто исходный файл Scala, который содержит функцию верхнего уровня, определяемую как main. Поместите в файл по имени hello.scala следующий код:

@main def m() =
println("Hello, world, from a script!")


Главная загрузочная запись MBR была придумана для компьютеров IBM еще в прошлом тысячелетии, в начале 1980-х, в захватывающую эпоху десятимегабайтных жестких дисков. MBR занимает первые 512 байт первого сектора диска, предшествующего первому разделу, и содержит загрузчик и таблицу разделов. Загрузчик занимает 446 байт, таблица разделов — 64 байта, а оставшиеся 2 байта хранят сигнатуру.а затем запустите файл на выполнение:

$ scala hello.scala


И вы получите еще одно приветствие:

Hello, world, from a script!


В этом примере функция, отмеченная main, называется m (от слова main), но это имя не имеет значения для выполнения скрипта. Чтобы скрипт сработал, вам необходимо запустить Scala и указать имя файла, содержащего функцию main, а не имя этой функции.

Вы можете получить доступ к аргументам командной строки, переданным вашему скрипту, приняв их в качестве параметров вашей основной функции. Например, вы можете принять строковые аргументы, взяв параметр со специальной аннотацией типа String*, что означает от нуля до многих повторяющихся параметров типа String. Внутри основной функции параметр будет иметь тип Seq[String], то есть последовательность строк. В Scala последовательности начинаются с нуля, и чтобы получить доступ к элементу, необходимо указать его индекс в круглых скобках. Таким образом, первым элементом в последовательности Scala с именем steps будет steps (0). Чтобы попробовать это, введите в новый файл с именем helloarg.scala следующее:

@main def m(args: String*) =
   // Поприветствуйте содержимое первого аргумента
   println("Hello, " + args(0) + "!")


а затем запустите его на выполнение:

$ scala helloarg.scala planet


В данной команде planet передается в качестве аргумента командной строки, доступного в скрипте при использовании выражения args (0). Поэтому вы должны увидеть на экране следующий текст:

Hello, planet!


Обратите внимание на наличие комментария в скрипте. Компилятор Scala проигнорирует символы между парой символов // и концом строки, а также все символы между сочетаниями символов /* и */. Вдобавок в этом примере показана конкатенация String-значений, выполненная с помощью оператора +. Весь код работает вполне предсказуемо. Выражение «Hello,» + «world!» будет вычислено в строку «Hello, world!».

Шаг 5. Организуем цикл с while и принимаем решение с if


Чтобы попробовать в работе конструкцию while, наберите следующий код и сохраните его в файле printargs.scala:

@main def m(args: String*) =
var i = 0
while i < args.length do
println(args(i))
i += 1


ПРИМЕЧАНИЕ
Хотя примеры в данном разделе помогают объяснить суть циклов while, они не демонстрируют наилучший стиль программирования на Scala. В следующем разделе будут показаны более рациональные подходы, позволяющие избежать повторения последовательностей с помощью индексов.


Этот скрипт начинается с определения переменой, var i = 0. Вывод типов относит переменную i к типу Int, поскольку это тип ее начального значения 0. Конструкция while на следующей строке заставляет блок (две строки кода снизу) повторно выполняться, пока булево выражение i

$ scala printargs.scala Scala is fun


И вы увидите:

Scala
is
fun


Далее наберите в новом файле по имени echoargs.scala следующий код:

@main def m(args: String*) =
var i = 0
while i < args.length do
if i != 0 then
print(" ")
print(args(i))
i += 1
println()


В целях вывода всех аргументов в одной и той же строке в этой версии вместо вызова println используется вызов print. Чтобы эту строку можно было прочитать, перед каждым аргументом, за исключением первого, благодаря использованию конструкции if i!= 0 then вставляется пробел. При первом проходе цикла while выражение i!= 0 станет вычисляться в false, поэтому перед начальным элементом пробел выводиться не будет. В самом конце добавлена еще одна инструкция println, чтобы после вывода аргументов произошел переход на новую строку. Тогда у вас получится очень красивая картинка. Если запустить этот скрипт с помощью команды:

$ scala echoargs.scala Scala is even more fun


то вы увидите на экране такой текст:

Scala is even more fun


Обратите внимание, что в Scala, в отличие от Java, вам не нужно помещать логическое выражение while или if в круглые скобки. Еще одно отличие от Java состоит в том, что вы можете опустить фигурные скобки в блоке, даже если он содержит более одного оператора, при условии, что вы сделаете соответствующий отступ для каждой строки. И хотя вы не видели ни одной точки с запятой, Scala использует их для разделения операторов, как и Java, за исключением того, что в Scala эти знаки очень часто являются необязательными, что дает некоторое облегчение вашему правому мизинцу. Если бы вы были более многословны, вы могли бы написать скрипт echoargs.scala в стиле Java следующим образом:

@main def m(args: String*) = {
var i = 0;
while (i < args.length) {
if (i != 0) {
print(" ");
}
print(args(i));
i += 1;
}
println();
}


Начиная со Scala 3, вместо фигурных скобок рекомендуется использовать стиль на основе отступов, называемый «тихим синтаксисом». В Scala 3 также были добавлены маркеры окончания кода, помогающие понять, где заканчиваются более крупные области с отступом. Маркеры окончания кода состоят из ключевого слова end и следующего за ним токена спецификатора, который является либо идентификатором, либо ключевым словом. Пример показан в листинге 10.9.

Шаг 6. Перебираем элементы с foreach и for-do


Возможно, при написании циклов while на предыдущем шаге вы даже не осознавали того, что программирование велось в императивном стиле. Обычно он применяется с такими языками, как Java, C++ и Python. При работе в этом стиле императивные команды в случае последовательного перебора элементов в цикле выдаются поочередно и зачастую изменяемое состояние совместно используется различными функциями. Scala позволяет программировать в императивном стиле, но, узнав этот язык получше, вы, скорее всего, перейдете преимущественно на функциональный стиль. По сути, одна из основных целей этой книги — помочь освоить работу в функциональном стиле, чтобы она стала такой же комфортной, как и работа в императивном.

Одна из основных характеристик функционального языка — то, что его функции относятся к конструкциям первого класса, и это абсолютно справедливо для языка Scala. Например, еще один, гораздо более лаконичный вариант вывода каждого аргумента командной строки выглядит так:

@main def m(args: String*) =
args.foreach(arg => println(arg))


В этом коде в отношении массива args вызывается метод foreach, в который передается функция. В данном случае передается функциональный литерал с одним параметром arg. Тело функции — вызов println (arg). Если набрать показанный ранее код в новом файле по имени pa.scala и запустить этот файл на выполнение с помощью команды:

$ scala pa.scala Concise is nice


то на экране появятся строки:

Concise
is
nice


В предыдущем примере компилятор Scala вывел тип arg, причислив эту переменную к String, поскольку String — тип элемента последовательности, в отношении которого вызван метод foreach. Если вы предпочитаете конкретизировать, то можете упомянуть название типа. Но, пойдя по этому пути, придется часть кода, в которой указывается переменная аргумента, заключать в круглые скобки (это и есть обычный синтаксис):

@main def m(args: String*) =
args.foreach((arg: String) => println(arg))


При запуске этот скрипт ведет себя точно так же, как и предыдущий.

Если же вы склонны не к конкретизации, а к более лаконичному изложению кода, то можете воспользоваться специальными сокращениями, принятыми в Scala. Если функциональный литерал функции состоит из одной инструкции, принимающей один аргумент, то обозначать данный аргумент явным образом по имени не нужно. Поэтому работать будет и следующий код:

@main def m(args: String*) =
args.foreach(println)


Резюмируем усвоенное: синтаксис для функционального литерала представляет собой список поименованных параметров, заключенный в круглые скобки, а также правую стрелку, за которой следует тело функции. Этот синтаксис показан на рис. 2.2.

vt7oxagd5ue6tuijmc0sdsigdsa.png


Теперь вы можете поинтересоваться: что же случилось с теми проверенными циклами for, которые вы привыкли использовать в таких императивных языках, как Java или Python? Придерживаться функционального направления в Scala возможно с помощью только одного функционального родственника императивной конструкции for, который называется выражением for. Поскольку вы не сможете понять всю его эффективность и выразительность, пока не доберетесь до раздела 7.3 (или не заглянете в него), здесь о нем будет дано лишь общее представление. Наберите в новом файле по имени forargs.scala следующий код:

@main def m(args: String*) =
for arg <- args do
println(arg)


Между for и do находится arg

Если скрипт forargs.scala запустить с помощью команды:

$ scala forargs.scala for arg in args
то вы увидите:
for
arg
in
args


Диапазон применения выражения for значительно шире, но для начала этого примера достаточно. Мы расскажем вам больше о for в шаге 12 главы 3 и в разделе 7.3.

Резюме


В данной главе мы привели основную информацию о Scala. Надеемся, вы воспользовались возможностью создать код на этом языке. В следующей главе мы продолжим вводный обзор и рассмотрим более сложные темы.

Более подробно с книгой можно ознакомиться на сайте издательства:

» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Покупка электронной книги вне РФ доступна на Google Play.

Для Хаброжителей скидка 35% по купону — Scala

© Habrahabr.ru