автордың кітабын онлайн тегін оқу Scala. Профессиональное программирование
Перевод С. Черников
Мартин Одерски, Лекс Спун, Билл Веннерс, Фрэнк Соммерс
Scala. Профессиональное программирование. 5-е изд.. — СПб.: Питер, 2022.
ISBN 978-5-4461-1914-1
© ООО Издательство "Питер", 2022
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Отзывы на предыдущие издания книги
Это, вероятно, одна из лучших книг по программированию, которые я когда-либо читал. Мне нравятся стиль изложения, краткость и подробные объяснения. Книга, кажется, отвечает на каждый вопрос, который приходит мне в голову, — она всегда на шаг впереди меня. Авторы не просто дают вам код — они объясняют самую суть, чтобы вы действительно понимали, о чем идет речь. Мне это очень нравится.
Кен Эджервари (Ken Egervari), ведущий разработчик программного обеспечения
Эта книга написана четко, основательно и понятно. В ней есть отличные примеры и полезные советы. Она позволила нашей организации быстро и эффективно освоить язык Scala. Эта книга отлично подходит для любого программиста, который хочет разобраться в гибкости и элегантности этого языка.
Ларри Моррони (Larry Morroni), владелец Morroni Technologies, Inc.
Это отличный учебник по языку Scala. В нем хорошо проработана каждая глава, основанная на концепциях и примерах, описанных в предыдущих главах. Подробно объясняются конструкции языка, часто приводятся примеры того, чем язык отличается от Java. Помимо основного языка, также рассматриваются такие библиотеки, как контейнеры и акторы.
С этим пособием действительно легко работать, и, вероятно, это одна из лучших технических книг, которые я читал за последнее время. Я настоятельно рекомендую эту книгу любому программисту, желающему узнать больше о языке Scala.
Мэтью Тодд (Matthew Todd)
Я поистине впечатлен работой, проделанной авторами этой книги. Она бесценное руководство по языку Scala — средству для эффективного написания кода, постоянный источник вдохновения для разработки и реализации масштабируемого ПО. Если бы только у меня была Scala в ее нынешнем зрелом состоянии и эта книга на моем столе еще в 2003 году, когда я участвовал в разработке и внедрении части инфраструктуры портала Олимпийских игр 2004 года в Афинах!
Всем читателям: независимо от вашего опыта, я думаю, программирование на Scala покажется вам очень гибким, и эта книга станет вашим верным другом в этом путешествии.
Христос К.К. Ловердос (Christos KK Loverdos), консультант по программному обеспечению, исследователь
«Scala. Профессиональное программирование» — это превосходное углубленное введение в Scala, а также отличный справочник. Я бы сказал, что эта книга занимает видное место на моей полке, если не считать, что я почти везде ношу ее с собой.
Брайан Клэппер (Brian Clapper), президент ArdenTex, Inc.
Отличная книга, хорошо написанная, с вдумчивыми примерами. Рекомендую как опытным программистам, так и новичкам.
Говард Ловатт (Howard Lovatt)
Эта книга рассказывает не только о том, как разрабатывать программы на языке Scala, но и, что более важно, о том, зачем это делать. Прагматичный подход книги к представлению возможностей сочетания объектно-ориентированного и функционального программирования не оставляет читателю никаких сомнений в том, что такое Scala на самом деле.
Доктор Эрвин Варга (Ervin Varga), генеральный директор и основатель EXPRO I.T. Consulting
Это отличное введение в функциональное программирование для объектно-ориентированных программистов. Моей основной целью было изучение функционального программирования, и на этом пути меня поджидали некоторые приятные сюрпризы Scala, такие как case-классы и сопоставление с образцом. Scala — интригующий язык, и эта книга хорошо его раскрывает.
В подобного рода книгах всегда есть риск сказать слишком много или слишком мало. Я считаю, что в «Scala. Профессиональное программирование» как раз достигнут идеальный баланс.
Джефф Хон (Jeff Heon), программист-аналитик
Понятность и техническая полнота — отличительные черты любой хорошо написанной книги, и я поздравляю Мартина Одерски, Лекса Спуна и Билла Веннерса с действительно очень хорошо выполненной работой! Книга «Scala. Профессиональное программирование» начинается с описания базовых концепций и поднимает пользователя до среднего уровня и выше. Эту книгу, безусловно, необходимо купить всем, кто хочет изучить Scala.
Джаган Намби (Jagan Nambi), архитектор корпоративных бизнес-решений, GMAC Financial Services
Читать эту книгу — одно удовольствие. Это одна из тех хорошо написанных технических книг, которые обеспечивают глубокое и всестороннее освещение предмета исключительно кратким и элегантным образом. Книга построена очень естественно и логично. Она одинаково хорошо подходит как для инженера-любителя, который просто хочет быть в курсе текущих тенденций, так и для профессионала, стремящегося к глубокому пониманию основных функций языка и его дизайна. Настоятельно рекомендую эту книгу всем, кто интересуется функциональным программированием в целом. Разработчикам Scala эту книгу, безусловно, необходимо прочитать.
Игорь Хлыстов, архитектор программного обеспечения/ведущий программист, Greystone Inc.
По мере изучения книги «Scala. Профессиональное программирование» начинаешь понимать, какое огромное количество труда в нее вложено. Я никогда раньше не читал настолько всеобъемлющего пособия для новичков. Большинство авторов, стараясь объяснять материал на доступном уровне и не запутать читателя, опускают некоторые сложные аспекты. Это оставляет довольно неприятный осадок, так как возникает навязчивое ощущение, что материал до конца не усвоен. Всегда есть остаточная «магия», которую не объяснили и о которой читатель вообще не может судить. В этой книге такого никогда не происходит, она никогда не принимает ничего как должное: подробно описана каждая деталь и, если этого недостаточно, дается ссылка на более глубокое объяснение. В самом деле, в тексте много перекрестных ссылок, поэтому составить полную картину сложной темы относительно легко.
Джеральд Леффлер (Gerald Loeffler), Java-архитектор корпоративных бизнес-решений
Во времена, когда хорошие книги по программированию редки, «Scala. Профессиональное программирование» Мартина Одерски, Лекса Спуна и Билла Веннерса действительно выделяется — это отличное введение для программистов среднего уровня. Здесь вы найдете все необходимое, чтобы выучить этот многообещающий язык.
Кристиан Нойкирхен (Christian Neukirchen)
Предисловие
Забавно наблюдать за рождением нового языка программирования. Большинство тех, кто пользуется языками программирования — будь то новичок или профессионал, — не задумываются об их происхождении. Подобно молотку или топору, язык программирования — это инструмент, который позволяет нам выполнять свою работу. Мы редко размышляем о том, как появился этот инструмент, каков был процесс его разработки. У нас может быть свое мнение о его синтаксисе, но обычно просто принимаем его как есть и движемся вперед.
Однако создание языка программирования открывает совершенно иную перспективу. Появляются новые возможности того, что могло показаться безграничным. Но в то же время язык программирования должен удовлетворять бесконечному списку ограничений. Странное противоречие.
Новые языки программирования создаются по многим причинам: из-за личного желания сделать что-то свое, глубокой академической проницательности, технического долга или анализа других архитектур компиляторов и даже политики. Scala 3 — это комбинация некоторых из них.
Какая бы ни была комбинация, все началось с того, что однажды Мартин Одерски (Martin Odersky) исчез, появившись несколько дней спустя, чтобы объявить на собрании исследовательской группы, что он начал эксперименты по воплощению DOT-исчисления в жизнь, написав новый компилятор с нуля1.
Мы были группой аспирантов и кандидатов, которые до недавнего времени играли важную роль в разработке и сопровождении Scala 2. В то время Scala достигал, казалось, непостижимых высот успеха, особенно для эзотерического и академического языка программирования из школы с забавным названием в Швейцарии. Scala недавно стал популярным среди стартапов в Области залива Сан-Франциско, и для поддержки, сопровождения и управления выпусками Scala 2 недавно была создана компания Typesafe, позже названная Lightbend. Так почему же вдруг появился новый компилятор и, возможно, новый и другой язык программирования? Большинство были настроены скептически. Мартина это не испугало.
Прошли месяцы. Как по будильнику, в 12 часов дня вся лаборатория стягивалась в коридор, соединяющий все наши офисы. После того как нас собиралось изрядное количество, мы вместе с Мартином отправлялись в один из многочисленных буфетов ФПШЛ (Федеральная политехническая школа Лозанны), чтобы пообедать, а затем выпить кофе. Каждый день во время этого ритуала идеи для нового компилятора были постоянной темой для обсуждения. Обсуждения были жаркими, мы прыгали с одной темы на другую: от чего-то, что «на 150 %» совместимо со Scala 2 (чтобы избежать фиаско, как при переходе с Python 2 на Python 3), до создания нового языка с полным спектром зависимых типов.
Один за другим, все скептики в исследовательской группе в итоге начинали увлекаться каким-либо из аспектов Scala 3, будь то оптимизация реализации проверки типов, новая архитектура компилятора или мощные дополнения к системе типов. Со временем большая часть сообщества также пришла к мысли, что Scala 3 значительно улучшен по сравнению со Scala 2. У разных людей были разные причины для этого. Для некоторых это было улучшение читаемости за счет решения сделать необязательными фигурные и круглые скобки вокруг условий в условных операторах. Для других это были улучшения в системе типов, например сопоставление типов для улучшенного программирования на уровне типов. Список был бесконечным.
Вместо того чтобы слепо продвигаться вперед в разработке Scala 3, основываясь лишь на догадках, я могу с уверенностью сказать, что Scala 3 — это результат большого изучения решений прошлого и многолетнего взаимодействия с исследовательской группой ФПШЛ и сообществом Scala. И не было другого выхода, кроме как начать с чистого листа и строить на чистом фундаменте. Благодаря такому подходу с нуля возник, по сути, новый язык программирования, и имя ему — Scala 3. Конечно, он может быть совместим со Scala 2 и считаться третьим крупным релизом уже существующего языка программирования, но не дайте себя обмануть: Scala 3 представляет собой существенную оптимизацию многих экспериментальных идей, впервые появившихся в Scala 2.
Возможно, самое уникальное в Scala 3 — это то, что случилось с имплицитами. Scala с момента своего создания использовался умными программистами для достижения функциональности, которую мало кто считал возможной даже с учетом набора функций Scala, не говоря уже о том, для чего этот язык был разработан. Функция, ранее известная как имплициты, — это, пожалуй, самая известная функция Scala, которая применялась для использования Scala 2 самым неожиданным образом.
Примеры использования имплицитов включают в себя добавление метода к классу задним числом без расширения и повторной компиляции этого класса. Или, учитывая сигнатуру типа, используемую в некотором контексте, автоматический выбор правильной реализации для этого контекста. Это лишь верхушка айсберга — мы даже написали исследовательскую работу, в которой пытались каталогизировать множество способов, которыми разработчики использовали имплициты [Kri19].
Это все равно что дать пользователю кнопки и рычаги и предоставить ему возможность построить отлаженный механизм, как механический калькулятор (рис. 1). Но часто вместо этого получается что-то наподобие кинетической скульптуры Тео Янсена (Theo Jansen) (рис. 2), а не что-то имеющее очевидное применение2. Проще говоря, вы даете сообществу программистов нечто столь же простое, как кнопка и рычаг, и бесстрашные люди будут искать творческие способы их использования. Такова природа человека. Но, возможно, ошибкой Scala 2 была идея предоставить в первую очередь что-то столь же универсальное, как кнопки и рычаги.
|
|
|
| Рис. 1. Что мы задумали… |
Рис. 2. …И что получили |
Дело здесь в том, что в Scala 2 был бесконечный набор возможностей для использования имплицитов, что потребовало исследовательской работы, и сообщество в целом не могло прийти к согласию относительно того, как разумно их использовать. Никакая языковая функция не должна иметь столь туманного предназначения. И тем не менее они были. Имплициты рассматривались одними как уникальная и мощная особенность Scala, которой, по сути, не было ни в одном другом языке, а другими — как загадочный и часто разочаровывающий механизм, который агрессивно переписывал ваш код, чтобы он стал чем-то другим.
Возможно, вы слышали часто повторяемую мантру о том, что Scala 3 во многих отношениях представляет собой упрощение Scala/Scala 2. История имплицитов — отличный тому пример. Осознавая все кульбиты, которые программисты делали с имплицитами в попытке реализовать более широкие шаблоны программирования, такие как вывод классов, Мартин, не без помощи, пришел к выводу, что нам не следует сосредотачиваться на имплицитах как на механизме, который люди могут использовать в самых общих случаях. Скорее, мы должны сосредоточиться на том, что программисты хотят делать с имплицитами, и сделать это проще и эффективнее. Отсюда и мантра: «Scala 3 фокусируется на намерении, а не на механизме».
В Scala 3 вместо того, чтобы сосредоточиться на универсальности имплицитов как механизма, было принято решение сосредоточиться на конкретных сценариях их использования, которые разработчики имели в виду при выборе имплицитов в первую очередь, и сделать эти шаблоны более доступными для использования по прямому назначению. Примеры включают в себя передачу контекстной или конфигурационной информации неявными методами, без необходимости передавать повторяющиеся аргументы программисту, добавление методов в классы задним числом и преобразование между типами, такими как Ints и Doubles, во время вычислений. Теперь Scala 3 делает эти варианты использования доступными для программистов без необходимости применять некоторую «глубокую» интуицию в отношении того, как компилятор Scala решит использовать имплициты. Вместо этого вы можете просто сосредоточиться на таких задачах, как «добавить метод foo в класс Bar без необходимости его перекомпиляции». Докторская степень не требуется. Просто замените предыдущее понятие «неявный» другими, более прямыми ключевыми словами, которые соответствуют конкретным вариантам использования, например такими, как given и using. Подробнее об этом читайте в главах 21 и 22.
Эта история о том, что «приоритет отдается намерению над механизмом», не останавливается на пересмотре имплицитов. Скорее, философия затрагивает почти все аспекты языка. Примерами могут служить дополнения и оптимизация многих аспектов системы типов Scala от типов объединения и перечислений до сопоставления типов или даже чистки синтаксиса Scala: необязательные фигурные скобки для улучшения читаемости и более читаемый «тихий» синтаксис для конструкций if, else и while, в результате чего условные выражения больше напоминают английский язык, чем машинный код.
Не верьте мне на слово. Независимо от того, являетесь ли вы новичком в Scala или опытным разработчиком, я надеюсь, что вы найдете новшества, вошедшие в Scala 3, такими же свежими и простыми, какими их нахожу я!
Хизер Миллер (Heather Miller),Лозанна, Швейцария,1 июня 2021 г.
DOT-исчисления, или зависимые типы объектов, — это попытка теоретически обосновать систему типов языка Scala.
Динамичные изображения кинетических скульптур Тео Янсена, получивших название Strandbeest, см. в видео: https://www.youtube.com/watch?v=LewVEF2B_pM.
Это все равно что дать пользователю кнопки и рычаги и предоставить ему возможность построить отлаженный механизм, как механический калькулятор (рис. 1). Но часто вместо этого получается что-то наподобие кинетической скульптуры Тео Янсена (Theo Jansen) (рис. 2), а не что-то имеющее очевидное применение2. Проще говоря, вы даете сообществу программистов нечто столь же простое, как кнопка и рычаг, и бесстрашные люди будут искать творческие способы их использования. Такова природа человека. Но, возможно, ошибкой Scala 2 была идея предоставить в первую очередь что-то столь же универсальное, как кнопки и рычаги.
Какая бы ни была комбинация, все началось с того, что однажды Мартин Одерски (Martin Odersky) исчез, появившись несколько дней спустя, чтобы объявить на собрании исследовательской группы, что он начал эксперименты по воплощению DOT-исчисления в жизнь, написав новый компилятор с нуля1.
Благодарности
Мы благодарны за вклад в эту книгу многим людям.
Сам язык Scala — плод усилий множества специалистов. Свой вклад в проектирование и реализацию версии 1.0 внесли Филипп Альтер (Philippe Altherr), Винсент Кремет (Vincent Cremet), Жиль Дюбоше (Gilles Dubochet), Бурак Эмир (Burak Emir), Стефан Мишель (Stеphane Micheloud), Николай Михайлов (Nikolay Mihaylov), Мишель Шинц (Michel Schinz), Эрик Стенман (Erik Stenman) и Матиас Зенгер (Matthias Zenger). К разработке второй и текущей версий языка, а также инструментальных средств подключились Фил Багвелл (Phil Bagwell), Антонио Куней (Antonio Cunei), Юлиан Драгос (Iulian Dragos), Жиль Дюбоше (Gilles Dubochet), Мигель Гарсиа (Miguel Garcia), Филипп Халлер (Philipp Haller), Шон Макдирмид (Sean McDirmid), Инго Майер (Ingo Maier), Донна Малайери (Donna Malayeri), Адриан Мурс (Adriaan Moors), Хуберт Плоциничак (Hubert Plociniczak), Пол Филлипс (Paul Phillips), Александр Прокопец (Aleksandar Prokopec), Тиарк Ромпф (Tiark Rompf), Лукас Рыц (Lukas Rytz) и Джеффри Уошберн (Geoffrey Washburn).
Следует также упомянуть тех, кто участвовал в работе над структурой языка. Эти люди любезно делились с нами своими идеями в оживленных и вдохновляющих дискуссиях, вносили важные фрагменты кода в открытую разработку и делали весьма ценные замечания по поводу предыдущих версий. Это Гилад Браха (Gilad Bracha), Натан Бронсон (Nathan Bronson), Коаюан (Caoyuan), Эймон Кэннон (Aemon Cannon), Крейг Чамберс (Craig Chambers), Крис Конрад (Chris Conrad), Эрик Эрнст (Erik Ernst), Матиас Феллизен (Matthias Felleisen), Марк Харра (Mark Harrah), Шрирам Кришнамурти (Shriram Krishnamurti), Гэри Ливенс (Gary Leavens), Дэвид Макивер (David MacIver), Себастьян Манит (Sebastian Maneth), Рикард Нильссон (Rickard Nilsson), Эрик Мейер (Erik Meijer), Лалит Пант (Lalit Pant), Дэвид Поллак (David Pollak), Джон Претти (Jon Pretty), Клаус Остерман (Klaus Ostermann), Хорхе Ортис (Jorge Ortiz), Дидье Реми (Didier Rеmy), Майлз Сабин (Miles Sabin), Виджей Сарасват (Vijay Saraswat), Даниэль Спивак (Daniel Spiewak), Джеймс Страчан (James Strachan), Дон Симе (Don Syme), Эрик Торреборре (Erik Torreborre), Мэдс Торгерсен (Mads Torgersen), Филип Уодлер (Philip Wadler), Джейми Уэбб (Jamie Webb), Джон Уильямс (John Williams), Кевин Райт (Kevin Wright) и Джейсон Зауг (Jason Zaugg). Очень полезные отзывы, которые помогли нам улучшить язык и его инструментальные средства, были получены от людей, подписанных на наши рассылки по Scala.
Джордж Бергер (George Berger) усердно работал над тем, чтобы процесс создания и размещения книги в интернете протекал гладко. Как результат, в данном проекте не было никаких технических сбоев.
Ценные отзывы о начальных вариантах текста книги были получены нами от многих людей. Наших благодарностей заслуживают Эрик Армстронг (Eric Armstrong), Джордж Бергер (George Berger), Алекс Блевитт (Alex Blewitt), Гилад Браха (Gilad Bracha), Уильям Кук (William Cook), Брюс Экель (Bruce Eckel), Стефан Мишель (Stеphane Micheloud), Тод Мильштейн (Todd Millstein), Дэвид Поллак (David Pollak), Филип Уодлер (Philip Wadler) и Матиас Зенгер (Matthias Zenger). Спасибо также представителям Silicon Valley Patterns group за их весьма полезный обзор. Это Дейв Астелс (Dave Astels), Трейси Бялик (Tracy Bialik), Джон Брюер (John Brewer), Эндрю Чейз (Andrew Chase), Брэдфорд Кросс (Bradford Cross), Рауль Дюк (Raoul Duke), Джон П. Эйрих (John P. Eurich), Стивен Ганц (Steven Ganz), Фил Гудвин (Phil Goodwin), Ральф Йочем (Ralph Jocham), Ян-Фа Ли (Yan-Fa Li), Тао Ма (Tao Ma), Джеффри Миллер (Jeffery Miller), Суреш Пай (Suresh Pai), Русс Руфер (Russ Rufer), Дэйв У. Смит (Dave W. Smith), Скотт Торнквест (Scott Turnquest), Вальтер Ваннини (Walter Vannini), Дарлин Уоллах (Darlene Wallach) и Джонатан Эндрю Уолтер (Jonathan Andrew Wolter). Кроме того, хочется поблагодарить Дуэйна Джонсона (Dewayne Johnson) и Кима Лиди (Kim Leedy) за помощь в художественном оформлении обложки, а также Фрэнка Соммерса (Frank Sommers) — за работу над алфавитным указателем.
Хотелось бы выразить особую благодарность и всем нашим читателям, приславшим комментарии. Они нам очень пригодились для повышения качества книги. Мы не в состоянии опубликовать имена всех приславших комментарии, но объявим имена тех читателей, кто прислал не менее пяти комментариев на стадии eBook PrePrint™. Отсортируем их имена по убыванию количества комментариев. Наших благодарностей заслуживают Дэвид Бизак (David Biesack), Дон Стефан (Donn Stephan), Матс Хенриксон (Mats Henricson), Роб Диккенс (Rob Dickens), Блэр Захак (Blair Zajac), Тони Слоан (Tony Sloane), Найджел Харрисон (Nigel Harrison), Хавьер Диас Сото (Javier Diaz Soto), Уильям Хелан (William Heelan), Джастин Фурдер (Justin Forder), Грегор Перди (Gregor Purdy), Колин Перкинс (Colin Perkins), Бьярте С. Карлсен (Bjarte S. Karlsen), Эрвин Варга (Ervin Varga), Эрик Уиллигерс (Eric Willigers), Марк Хейс (Mark Hayes), Мартин Элвин (Martin Elwin), Калум Маклин (Calum MacLean), Джонатан Уолтер (Jonathan Wolter), Лес Прушински (Les Pruszynski), Сет Тисуе (Seth Tisue), Андрей Формига (Andrei Formiga), Дмитрий Григорьев (Dmitry Grigoriev), Джордж Бергер (George Berger), Говард Ловетт (Howard Lovatt), Джон П. Эйрих (John P. Eurich), Мариус Скуртеску (Marius Scurtescu), Джефф Эрвин (Jeff Ervin), Джейми Уэбб (Jamie Webb), Курт Зольман (Kurt Zoglmann), Дин Уэмплер (Dean Wampler), Николай Линдберг (Nikolaj Lindberg), Питер Маклейн (Peter McLain), Аркадиуш Стрыйски (Arkadiusz Stryjski), Шанки Сурана (Shanky Surana), Крейг Борделон (Craig Bordelon), Александр Пэтри (Alexandre Patry), Филип Моэнс (Filip Moens), Фред Янон (Fred Janon), Джефф Хеон (Jeff Heon), Борис Лорбир (Boris Lorbeer), Джим Менард (Jim Menard), Тим Аццопарди (Tim Azzopardi), Томас Юнг (Thomas Jung), Уолтер Чанг (Walter Chang), Йерун Дийкмейер (Jeroen Dijkmeijer), Кейси Боумен (Casey Bowman), Мартин Смит (Martin Smith), Ричард Даллауэй (Richard Dallaway), Энтони Стаббс (Antony Stubbs), Ларс Вестергрен (Lars Westergren), Маартен Хэйзвинкель (Maarten Hazewinkel), Мэтт Рассел (Matt Russell), Ремигиус Михаловски (Remigiusz Michalowski), Андрей Толопко (Andrew Tolopko), Кертис Стэнфорд (Curtis Stanford), Джошуа Каф (Joshua Cough), Земен Денг (Zemian Deng), Кристофер Родригес Масиас (Christopher Rodrigues Macias), Хуан Мигель Гарсия Лопес (Juan Miguel Garcia Lopez), Мишель Шинц (Michel Schinz), Питер Мур (Peter Moore), Рэндольф Кал (Randolph Kahle), Владимир Кельман (Vladimir Kelman), Даниэль Гронау (Daniel Gronau), Дирк Детеринг (Dirk Detering), Хироаки Накамура (Hiroaki Nakamura), Оле Хугаард (Ole Hougaard), Бхаскар Маддала (Bhaskar Maddala), Дэвид Бернар (David Bernard), Дерек Махар (Derek Mahar), Джордж Коллиас (George Kollias), Кристиан Нордал (Kristian Nordal), Нормен Мюллер (Normen Mueller), Рафаэль Феррейра (Rafael Ferreira), Бинил Томас (Binil Thomas), Джон Нильсон (John Nilsson), Хорхе Ортис (Jorge Ortiz), Маркус Шульте (Marcus Schulte), Вадим Герасимов (Vadim Gerasimov), Кэмерон Таггарт (Cameron Taggart), Джон-Андерс Тейген (Jon-Anders Teigen), Сильвестр Забала (Silvestre Zabala), Уилл Маккуин (Will McQueen) и Сэм Оуэн (Sam Owen).
Хочется также сказать спасибо тем, кто отправил сообщения о замеченных неточностях после публикации первых двух изданий. Это Феликс Зигрист (Felix Siegrist), Лотар Мейер-Лербс (Lothar Meyer-Lerbs), Диетард Михаэлис (Diethard Michaelis), Рошан Даврани (Roshan Dawrani), Донн Стефан (Donn Stephan), Уильям Утер (William Uther), Франсиско Ревербель (Francisco Reverbel), Джим Балтер (Jim Balter), Фрик де Брюйн (Freek de Bruijn), Амброз Лэнг (Ambrose Laing), Сехар Прабхала (Sekhar Prabhala), Левон Салдамли (Levon Saldamli), Эндрю Бурсавич (Andrew Bursavich), Хьялмар Петерс (Hjalmar Peters), Томас Фер (Thomas Fehr), Ален О’Ди (Alain O’Dea), Роб Диккенс (Rob Dickens), Тим Тейлор (Tim Taylor), Кристиан Штернагель (Christian Sternagel), Мишель Паризьен (Michel Parisien), Джоэл Нили (Joel Neely), Брайан Маккеон (Brian McKeon), Томас Фер (Thomas Fehr), Джозеф Эллиотт (Joseph Elliott), Габриэль да Силва Рибейро (Gabriel da Silva Ribeiro), Томас Фер (Thomas Fehr), Пабло Рипольес (Pablo Ripolles), Дуглас Гейлор (Douglas Gaylor), Кевин Сквайр (Kevin Squire), Гарри-Антон Талвик (Harry-Anton Talvik), Кристофер Симпкинс (Christopher Simpkins), Мартин Витман-Функ (Martin Witmann-Funk), Джим Балтер (Jim Balter), Питер Фостер (Peter Foster), Крейг Бордолон (Craig Bordelon), Хайнц-Питер Гум (Heinz-Peter Gumm), Питер Чапин (Peter Chapin), Кевин Райт (Kevin Wright), Анантан Сринивасан (Ananthan Srinivasan), Омар Килани (Omar Kilani), Дон Стефан (Donn Stephan), Гюнтер Ваффлер (Guenther Waffler).
Лекс хотел бы поблагодарить специалистов, среди которых Аарон Абрамс (Aaron Abrams), Джейсон Адамс (Jason Adams), Генри и Эмили Крутчер (Henry and Emily Crutcher), Джои Гибсон (Joey Gibson), Гунар Хиллерт (Gunnar Hillert), Мэтью Линк (Matthew Link), Тоби Рейлтс (Toby Reyelts), Джейсон Снейп (Jason Snape), Джон и Мелинда Уэзерс (John and Melinda Weathers), и всех представителей Atlanta Scala Enthusiasts за множество полезных обсуждений структуры языка, его математических основ и способов представления языка Scala специалистам-практикам.
Особую благодарность хочется выразить Дэйву Брикчетти (Dave Briccetti) и Адриану Мурсу (Adriaan Moors) за рецензирование третьего издания, а также Маркони Ланна (Marconi Lanna) не только за рецензирование, но и за мотивацию выпустить третье издание, которая возникла после разговора о новинках, появившихся со времени выхода предыдущего издания.
Билл хотел бы поблагодарить нескольких специалистов за предоставленную информацию и советы по изданию книги. Его благодарность заслужили Гэри Корнелл (Gary Cornell), Грег Доенч (Greg Doench), Энди Хант (Andy Hunt), Майк Леонард (Mike Leonard), Тайлер Ортман (Tyler Ortman), Билл Поллок (Bill Pollock), Дейв Томас (Dave Thomas) и Адам Райт (Adam Wright). Билл также хотел бы поблагодарить Дика Уолла (Dick Wall) за сотрудничество над разработкой нашего курса Stairway to Scala, который большей частью основывался на материалах, вошедших в эту книгу. Наш многолетний опыт преподавания курса Stairway to Scala помог повысить его качество. И наконец, Билл хотел бы выразить благодарность Дарлин Грюндль (Darlene Gruendl) и Саманте Вулф (Samantha Woolf) за помощь в завершении третьего издания.
Наконец, мы хотели бы поблагодарить Жюльена Ричарда-Фоя (Julien Richard-Foy) за работу над обновлением четвертого издания этой книги до версии Scala 2.13, в частности за перепроектирование библиотеки коллекций.
Введение
Эта книга — руководство по Scala, созданное людьми, непосредственно занимающимися разработкой данного языка программирования. Нашей целью было научить вас всему, что необходимо для превращения в продуктивного программиста на языке Scala. Все примеры в книге компилируются с помощью Scala версии 3.0.0
Целевая аудитория
Книга в основном рассчитана на программистов, желающих научиться программировать на Scala. Если у вас есть желание создать свой следующий проект на этом языке, то наша книга вам подходит. Кроме того, она должна заинтересовать программистов, которые хотят расширить кругозор, изучив новые концепции. Если вы, к примеру, программируете на Java, то эта книга раскроет для вас множество концепций функционального программирования, а также передовых идей из сферы объектно-ориентированного программирования. Мы уверены: изучение Scala и заложенных в этот язык идей поможет вам повысить свой профессиональный уровень как программиста. Предполагается, что вы уже владеете общими знаниями в области программирования. Scala вполне подходит на роль первого изучаемого языка, однако это не та книга, которая может использоваться для обучения программированию. В то же время вам не нужно быть каким-то особенным знатоком языков программирования. Большинство людей использует Scala на платформе Java, однако наша книга не предполагает, что вы тесно знакомы с языком Java. Но все же мы ожидаем, что Java известен многим читателям, и поэтому иногда сравниваем оба языка, чтобы помочь таким читателям понять разницу.
Как пользоваться книгой
Книгу рекомендуется читать в порядке следования глав, от начала до конца. Мы очень старались в каждой главе вводить читателя в курс только одной темы и объяснять новый материал лишь в понятиях из ранее рассмотренных тем. Поэтому если перескочить вперед, чтобы поскорее в чем-то разобраться, то можно встретить объяснения, в которых используются еще непонятные концепции. Мы считаем, что получать знания в области программирования на Scala лучше постепенно, читая главы в порядке их следования.
Встретив незнакомое понятие, можно обратиться к глоссарию. Многие читатели бегло просматривают части книги, и это вполне нормально. Но при встрече с непонятными терминами можно выяснить, что просмотр был слишком поверхностным, и вернуться к справочным материалам.
Прочитав книгу, вы можете в дальнейшем использовать ее в качестве справочника по Scala. Конечно, существует официальная спецификация языка, но в ней прослеживается стремление к точности в ущерб удобству чтения. В нашем издании не охвачены абсолютно все подробности Scala, но его особенности изложены в нем вполне обстоятельно. Так что по мере освоения программирования на этом языке издание вполне может стать доступным справочником.
Как изучать Scala
Весьма обширные познания о Scala можно получить, просто прочитав книгу от начала до конца. Но быстрее и основательнее освоить язык можно с помощью ряда дополнительных действий.
Прежде всего можно воспользоваться множеством примеров программ, включенных в эту книгу. Самостоятельно набирая их, вам придется вдумываться в каждую строку кода. Попытки его разнообразить позволят вам глубже заинтересоваться изучаемой темой и убедиться в том, что вы действительно поняли, как работает этот код.
Затем можно поучаствовать в работе множества онлайн-форумов. Это позволит вам и многим другим приверженцам Scala помочь друг другу в его освоении. Есть множество рассылок, дискуссионных форумов, чатов, вики-источников и несколько информационных каналов, которые посвящены этому языку и содержат соответствующие публикации. Уделите время поиску источников информации, более всего отвечающих вашим запросам. Вы будете гораздо быстрее решать мелкие проблемы, что позволит уделять больше времени более серьезным и глубоким вопросам.
И наконец, получив при чтении книги достаточный объем знаний, приступайте к разработке собственного проекта. Поработайте с нуля над созданием какой-нибудь небольшой программы или разработайте дополнение к более объемной. Вы не добьетесь быстрых результатов одним только чтением.
Условные обозначения
При первом упоминании какого-либо понятия или термина его название дается курсивом. Для небольших встроенных в текст примеров кода, таких как x+1, используется моноширинный шрифт. Большие примеры кода представлены в виде отдельных блоков, для которых тоже используется моноширинный шрифт:
def hello() =
println("Hello, world!")
Когда показывается работа с интерактивной оболочкой, ответы последней выделяются шрифтом на сером фоне:
scala> 3 + 4
val res0: Int = 7
Структура книги
• Глава 1 «Масштабируемый язык» представляет обзор структуры языка Scala, а также ее логическое обоснование и историю.
• Глава 2 «Первые шаги в Scala» показывает, как в языке выполняется ряд основных задач программирования, не вдаваясь в подробности, касающиеся особенностей работы механизмов языка. Цель этой главы — заставить ваши пальцы набирать и запускать код на Scala.
• Глава 3 «Дальнейшие шаги в Scala» показывает ряд основных задач программирования, помогающих ускорить освоение этого языка. Изучив данную главу, вы сможете использовать Scala для автоматизации простых задач.
• Глава 4 «Классы и объекты» закладывает начало углубленного рассмотрения языка Scala, приводит описание его основных объектно-ориентированных строительных блоков и указания по выполнению компиляции и запуску приложений Scala.
• Глава 5 «Основные типы и операции» охватывает основные типы Scala, их литералы, операции, которые могут над ними проводиться, вопросы работы уровней приоритета и ассоциативности и дает представление об обогащающих оболочках.
• Глава 6 «Функциональные объекты» углубляет представление об объектно-ориентированных свойствах Scala, используя в качестве примера функциональные (то есть неизменяемые) рациональные числа.
• Глава 7 «Встроенные управляющие конструкции» показывает способы использования таких конструкций Scala, как if, while, for, try и match.
• Глава 8 «Функции и замыкания» углубленно рассматривает функции как основные строительные блоки функциональных языков.
• Глава 9 «Управляющие абстракции» показывает, как усовершенствовать основные управляющие конструкции Scala с помощью определения собственных управляющих абстракций.
• Глава 10 «Композиция и наследование» рассматривает имеющуюся в Scala дополнительную поддержку объектно-ориентированного программирования. Затрагиваемые темы не столь фундаментальны, как те, что излагались в главе 4, но вопросы, которые в них рассматриваются, часто встречаются на практике.
• Глава 11 «Трейты» охватывает существующий в Scala механизм создания композиции примесей. Показана работа трейтов, описываются примеры их наиболее частого использования, и объясняется, как с помощью трейтов совершенствуется традиционное множественное наследование.
• Глава 12 «Пакеты, импорты и экспорты» рассматривает вопросы программирования в целом, включая высокоуровневые пакеты, инструкции импортирования и модификаторы управления доступом, такие как protected и private.
• Глава 13 «Сопоставление с образцом» описывает двойные конструкции, которые помогут вам при написании обычных, неинкапсулированных структур данных. Классы регистра и сопоставление с образцом особенно полезны для древовидных рекурсивных данных.
• Глава 14 «Работа со списками» подробно рассматривает списки, которые, вероятно, можно отнести к самым востребованным структурам данных в программах на Scala.
• Глава 15 «Работа с другими коллекциями» показывает способы использования основных коллекций Scala, таких как списки, массивы, кортежи, множества и отображения.
• Глава 16 «Изменяемые объекты» объясняет суть изменяемых объектов и синтаксиса для выражения этих объектов, обеспечиваемого Scala. Глава завершается практическим примером моделирования дискретного события, в котором показан ряд изменяемых объектов в действии.
• Глава 17 «Иерархия Scala» объясняет иерархию наследования языка и рассматривает универсальные методы и низшие типы.
• Глава 18 «Параметризация типов» объясняет некоторые методы сокрытия информации, представленные в главе 13, на конкретном примере: конструкции класса для чисто функциональных очередей. Глава строится на описании вариации параметров типа и того, как она взаимодействует с сокрытием информации.
• Глава 19 «Перечисления» вводит двойные конструкции, которые помогут вам при написании обычных, неинкапсулированных структур данных.
• Глава 20 «Абстрактные члены» дает описание всех видов абстрактных членов, поддерживаемых Scala, — не только методов, но и полей и типов, которые можно объявлять абстрактными.
• Глава 21 «Гивены» описывает функцию Scala, которая помогает вам работать с контекстными параметрами функций. Передача всей контекстной информации проста, но может потребовать большого количества шаблонов. Гивены позволят вам упростить эту задачу.
• Глава 22 «Методы расширения» описывает механизм Scala, позволяющий создать впечатление, что функция определена как метод в классе, хотя на самом деле она определена вне класса.
• Глава 23 «Классы типов» (которую еще предстоит написать). В этой главе будет проиллюстрировано несколько примеров классов типов.
• Глава 24 «Углубленное изучение коллекций» предлагает углубленный обзор библиотеки коллекций.
• Глава 25 «Утверждения и тесты» демонстрирует механизм утверждения Scala и дает обзор нескольких инструментов, доступных для написания тестов на Scala, уделяя особое внимание ScalaTest.
Ресурсы
На https://www.scala-lang.org — официальном сайте Scala — вы найдете последнюю версию Scala и ссылки на документацию и ресурсы сообщества. Исходный код и дополнительные материалы к книге вы найдете по адресу https://booksites.artima.com/programming_in_scala_5ed.
Исходный код
Исходный код, рассматриваемый в данной книге, выпущенный под открытой лицензией в виде ZIP-файла, можно найти на сайте книги: https://booksites.artima.com/programming_in_scala_5ed.
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
1. Масштабируемый язык
Scala означает «масштабируемый язык» (от англ. scalable language). Это название он получил, поскольку был спроектирован так, чтобы расти вместе с запросами своих пользователей. Язык Scala может решать широкий круг задач программирования: от написания небольших скриптов до создания больших систем3.
Scala легко освоить. Он работает на стандартных платформах Java и JavaScript и без проблем взаимодействует с библиотеками обеих платформ. Это довольно хороший язык для написания скриптов, объединяющих существующие библиотеки. Но он может еще больше проявить себя при построении больших систем и фреймворков из компонентов многократного использования.
С технической точки зрения Scala — смесь объектно-ориентированной и функциональной концепций программирования в статически типизированном языке. Подобный сплав проявляется во многих аспектах Scala — он, вероятно, может считаться более всеобъемлющим, чем другие широко используемые языки. Когда дело доходит до масштабируемости, два стиля программирования дополняют друг друга. Используемые в Scala конструкции функционального программирования упрощают быстрое создание интересных компонентов из простых частей. Объектно-ориентированные конструкции же облегчают структурирование больших систем и их адаптацию к новым требованиям. Сочетание двух стилей в Scala позволяет создавать новые виды шаблонов программирования и абстракций компонентов. Оно также способствует выработке понятного и лаконичного стиля программирования. И благодаря такой гибкости языка программирование на Scala может принести массу удовольствия.
В этой вступительной главе мы отвечаем на вопрос «Почему именно Scala?». Мы даем общий обзор структуры Scala и ее обоснование. Прочитав главу, вы получите базовое представление о том, что такое Scala и с какого рода задачами он поможет справиться. Книга представляет собой руководство по языку Scala, однако данную главу нельзя считать частью этого руководства. И если вам не терпится приступить к написанию кода на Scala, то можете сразу перейти к изучению главы 2.
1.1. Язык, который растет вместе с вами
Программы различных размеров требуют, как правило, использования разных программных конструкций. Рассмотрим, к примеру, следующую небольшую программу на Scala:
var capital = Map("US" –> "Washington", "France" –> "Paris")
capital += ("Japan" –> "Tokyo")
println(capital("France"))
Эта программа устанавливает отображение стран на их столицы, модифицирует отображение, добавляя новую конструкцию ("Japan"–>"Tokyo"), и выводит название столицы, связанное со страной France4. В этом примере используется настолько высокоуровневая система записи, что она не загромождена ненужными точками с запятыми и сигнатурами типов. И действительно возникает ощущение использования современного языка скриптов наподобие Perl, Python или Ruby. Одна из общих характеристик этих языков, применимая к данному примеру, — поддержка всеми ими в синтаксисе языка конструкции ассоциативного отображения.
Ассоциативные отображения очень полезны, поскольку помогают поддерживать понятность и краткость программ, но порой вам может не подойти их философия «на все случаи жизни», поскольку вам в своей программе нужно управлять свойствами отображений более тонко. При необходимости Scala обеспечивает точное управление, поскольку отображения в нем не являются синтаксисом языка. Это библиотечные абстракции, которые можно расширять и приспосабливать под свои нужды.
В показанной ранее программе вы получите исходную реализацию отображения Map, но ее можно будет без особого труда изменить. К примеру, можно указать конкретную реализацию, такую как HashMap или TreeMap, или с помощью модуля параллельных коллекций Scala вызвать метод par для получения отображения ParMap, операции в котором выполняются параллельно. Можно указать для отображения значение по умолчанию или переопределить любой другой метод созданного вами отображения. Во всех случаях для отображений вполне пригоден такой же простой синтаксис доступа, как и в приведенном примере.
В нем показано, что Scala может обеспечить вам как удобство, так и гибкость. Язык содержит набор удобных конструкций, которые помогают быстро начать работу и позволяют программировать в приятном лаконичном стиле. В то же время вы всегда сможете перекроить программу под свои требования, поскольку все в ней основано на библиотечных модулях, которые можно выбрать и приспособить под свои нужды.
Растут новые типы
Эрик Рэймонд (Eric Raymond) в качестве двух метафор разработки программных продуктов ввел собор и базар [Ray99]. Под собором понимается почти идеальная разработка, создание которой требует много времени. После сборки она долго остается неизменной. Разработчики же базара, напротив, что-то адаптируют и дополняют каждый день. В книге Рэймонда базар — метафора, описывающая разработку ПО с открытым кодом. Гай Стил (Guy Steele) отметил в докладе о «растущем языке», что аналогичное различие можно применить к структуре языка программирования [Ste99]. Scala больше похож на базар, чем на собор, в том смысле, что спроектирован с расчетом на расширение и адаптацию его теми, кто на нем программирует. Вместо того чтобы предоставлять все конструкции, которые только могут пригодиться в одном всеобъемлющем языке, Scala дает вам инструменты для создания таких конструкций.
Рассмотрим пример. Многие приложения нуждаются в целочисленном типе, который при выполнении арифметических операций может становиться произвольно большим без переполнения или циклического перехода в начало. В Scala такой тип определяется в библиотеке класса scala.math.BigInt. Определение использующего этот тип метода, который вычисляет факториал переданного ему целочисленного значения, имеет следующий вид5:
def factorial(x: BigInt): BigInt =
if x == 0 then 1 else x * factorial(x - 1)
Теперь, вызвав factorial(30), вы получите:
265252859812191058636308480000000
Тип BigInt похож на встроенный, поскольку со значениями этого типа можно использовать целочисленные литералы и операторы наподобие * и –. Тем не менее это просто класс, определение которого задано в стандартной библиотеке Scala6. Если бы класса не было, то любой программист на 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, напротив, дает пользователям возможность наращивать и адаптировать его в нужных направлениях. Он делает это с помощью определения простых в использовании библиотек, которые производят впечатление средств, естественно реализованных в языке.
Растут новые управляющие конструкции
Такая расширяемость иллюстрируется стилем 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, который способствует масштабируемости, то мы бы выбрали присущее этому языку сочетание объектно-ориентированного и функционального программирования (мы немного слукавили, на самом деле это два аспекта, но они взаимосвязаны).
Scala в объединении объектно-ориентированного и функционального программирования в однородную структуру языка пошел дальше всех остальных широко известных языков. Например, там, где в других языках объекты и функции — два разных понятия, в Scala функция по смыслу является объектом. Функциональные типы — это классы, которые могут наследоваться подклассами. Эти особенности могут показаться не более чем теоретическими, но имеют весьма серьезные последствия для возможностей масштабирования. Фактически ранее упомянутое понятие актора не может быть реализовано без этой унификации функций и объектов. Здесь мы рассмотрим возможные в Scala способы смешивания объектно-ориентированной и функциональной концепций.
Scala — объектно-ориентированный язык
Развитие объектно-ориентированного программирования шло весьма успешно. Появившись в языке Simula в середине 1960-х годов и Smalltalk в 1970-х, оно теперь доступно в подавляющем большинстве языков. В некоторых областях все полностью захвачено объектами. Точного определения «объектной ориентированности» нет, однако объекты явно чем-то привлекают программистов.
В принципе, мотивация для применения объектно-ориентированного программирования очень проста: все, за исключением самых простых программ, нуждается в определенной структуре. Наиболее понятный путь достижения желаемого результата заключается в помещении данных и операций в своеобразные контейнеры. Основной замысел объектно-ориентированного программирования состоит в придании этим контейнерам полной универсальности, чтобы в них могли содержаться не только операции, но и данные и чтобы сами они также были элементами, которые могли бы храниться в других контейнерах или передаваться операциям в качестве параметров. Подобные контейнеры называются объектами. Алан Кей (Alan Kay), изобретатель языка Smalltalk, заметил, что таким образом простейший объект имеет принцип построения, аналогичный полноценному компьютеру: под формализованным интерфейсом данные в нем сочетаются с операциями [Kay96]. То есть объекты имеют непосредственное отношение к масштабируемости языка: одни и те же технологии применяются к построению как малых, так и больших программ.
Хотя долгое время объектно-ориентированное программирование преобладало, немногие языки стали последователями Smalltalk по части внедрения этого принципа построения в свое логическое решение. Например, множество языков допускает использование элементов, не являющихся объектами, — можно вспомнить имеющиеся в языке Java значения примитивных типов. Или же в них допускается применение статических полей и методов, не входящих в какой-либо объект. Эти отклонения от чистой идеи объектно-ориентированного программирования на первый взгляд выглядят вполне безобидными, но имеют досадную тенденцию к усложнению и ограничению масштабирования.
В отличие от этого Scala — объектно-ориентированный язык в чистом виде: каждое значение является объектом и каждая операция — вызовом метода. Например, когда в Scala речь заходит о вычислении 1+2, фактически вызывается метод по имени +, который определен в классе Int. Можно определять методы с именами, похожими на операторы, а клиенты вашего API смогут с помощью этих методов записать операторы.
Когда речь заходит о составлении объектов, Scala проявляется как более совершенный язык по сравнению с большинством других. В качестве примера приведем имеющиеся в Scala трейты. Они подобны интерфейсам в Java, но могут содержать также реализации методов и даже поля7. Объекты создаются путем композиции примесей, при котором к членам класса добавляются члены нескольких трейтов. Таким образом, различные аспекты классов могут быть инкапсулированы в разных трейтах. Это выглядит как множественное наследование, но есть разница в конкретных деталях. В отличие от класса трейт может добавить в суперкласс новые функциональные возможности. Это придает трейтам более высокую степень подключаемости по сравнению с классами. В частности, благодаря этому удается избежать возникновения присущих множественному наследованию классических проблем «ромбовидного» наследования, которые возникают, когда один и тот же класс наследуется по нескольким различным путям.
Scala — функциональный язык
Наряду с тем, что Scala является чистым объектно-ориентированным языком, его можно назвать и полноценным функциональным языком. Идеи функционального программирования старше электронных вычислительных систем. Их основы были заложены в лямбда-исчислении Алонзо Черча (Alonzo Church), разработанном в 1930-е годы. Первым языком функционального программирования был Lisp, появление которого датируется концом 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()shouldBe3, включает в себя функцию, передаваемую невыполненной в метод eventually. Через настраиваемый период времени eventually станет неоднократно выполнять функцию до тех пор, пока утверждение не будет успешно подтверждено.
Вторая основная идея функционального программирования заключается в том, что операции программы должны преобразовать входные значения в выходные, а не изменять данные на месте. Чтобы понять разницу, рассмотрим реализацию строк в Ruby и Java. В Ruby строка является массивом символов. Символы в строке могут быть изменены по отдельности. Например, внутри одного и того же строкового объекта символ точки с запятой в строке можно заменить точкой. А в Java и Scala строка — последовательность символов в математическом смысле. Замена символа в строке с использованием выражения вида s.replace(';','.') приводит к возникновению нового строкового объекта, отличающегося от s. То же самое можно сказать по-другому: в Java строки неизменяемые, а в Ruby — изменяемые. То есть, рассматривая только строки, можно прийти к выводу, что Java — функциональный язык, а Ruby — нет. Неизменяемая структура данных — один из краеугольных камней функционального программирования. В библиотеках Scala в качестве надстроек над соответствующими API Java определяется также множество других неизменяемых типов данных. Например, в Scala имеются неизменяемые списки, кортежи, отображения и множества.
Еще один способ утверждения второй идеи функционального программирования заключается в том, что у методов не должно быть никаких побочных эффектов. Они должны обмениваться данными со своим окружением только путем получения аргументов и возвращения результатов. Например, под это описание подпадает метод replace, принадлежащий Java-классу String. Он получает строку и два символа и выдает новую строку, где все появления одного символа заменены появлениями второго. Других эффектов от вызова replace нет. Методы, подобные replace, называются ссылочно прозрачными. Это значит, что для любого заданного ввода вызов функции можно заменить его результатом, при этом семантика программы остается неизменной.
Функциональные языки заставляют применять неизменяемые структуры данных и ссылочно прозрачные методы. В некоторых функциональных языках это выражено в виде категоричных требований. Scala же дает возможность выбрать. При желании можно писать программы в императивном стиле — так называется программирование с изменяемыми данными и побочными эффектами. Но при необходимости в большинстве случаев Scala позволяет с легкостью избежать использования императивных конструкций благодаря существованию хороших функциональных альтернатив.
1.3. Почему именно Scala
Подойдет ли вам язык Scala? Разбираться и принимать решение придется самостоятельно. Мы считаем, что, помимо хорошей масштабируемости, существует еще множество причин, по которым вам может понравиться программирование на Scala. В этом разделе будут рассмотрены четыре наиболее важных аспекта: совместимость, лаконичность, абстракции высокого уровня и расширенная статическая типизация.
Scala — совместимый язык
Scala не требует резко отходить от платформы Java, чтобы опередить на шаг этот язык. Scala позволяет повышать ценность уже существующего кода, то есть опираться на то, что у вас уже есть, поскольку он был разработан для достижения беспрепятственной совместимости с Java8. Программы на 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 есть очень общее решение для устранения этого противоречия между передовой разработкой и функциональной совместимостью9. Scala позволяет определять многофункциональные расширения, которые всегда применяются при выборе несуществующих элементов. В рассматриваемом случае при поиске метода toInt для работы со строковым значением компилятор 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)
Получив указанный код, компилятор Scala создаст класс с двумя приватными переменными экземпляра (типа Int по имени index и типа String по имени name) и конструктор, который получает исходные значения для этих переменных в виде параметров. Код данного конструктора проинициализирует две переменные экземпляра значениями, переданными в качестве параметров. Короче говоря, в итоге вы получите ту же функциональность, что и у более многословной версии кода на Java10. Класс в Scala быстрее пишется и проще читается, а еще — и это наиболее важно — допустить ошибку при его создании значительно труднее, чем при создании класса в Java.
Еще один фактор, способствующий лаконичности, — используемый в Scala вывод типов. Повторяющуюся информацию о типе можно отбросить, и тогда программы избавятся от лишнего и их легче будет читать.
Но, вероятно, наиболее важный аспект сокращения объема кода — наличие кода, не требующего внесения в программу, поскольку это уже сделано в библиотеке. Scala предоставляет вам множество инструментальных средств для определения эффективных библиотек, позволяющих выявить и вынести за скобки общее поведение. Например, различные аспекты библиотечных классов можно выделить в трейты, которые затем можно перемешивать произвольным образом. Или же библиотечные методы могут быть параметризованы с помощью операций, позволяя вам определять конструкции, которые, по сути, являются вашими собственными управляющими конструкциями. Собранные вместе, эти конструкции позволяют определять библиотеки, сочетающие в себе высокоуровневый характер и гибкость.
Scala — высокоуровневый язык
Программисты постоянно борются со сложностью. Для продуктивного программирования нужно понимать код, над которым вы работаете. Чрезмерно сложный код был причиной краха многих программных проектов. К сожалению, важные программные продукты обычно бывают весьма сложными. Избежать сложности невозможно, но ею можно управлять.
Scala помогает управлять сложностью, позволяя повышать уровень абстракции в разрабатываемых и используемых интерфейсах. Представим, к примеру, что есть переменная name, имеющая тип String, и нужно определить, наличествует ли в этой строковой переменной символ в верхнем регистре. До выхода 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 функционального литерала11. В нем дается описание функции, которая получает аргумент в виде символа (представленного знаком подчеркивания) и проверяет, не является ли этот символ буквой в верхнем регистре12.
В Java 8 появилась поддержка лямбда-выражений и потоков (streams), позволяющая выполнять подобные операции на Java. Вот как это могло бы выглядеть:
boolean nameHasUpperCase = // Java 8 или выше
name.chars().anyMatch(
(int ch) –> Character.isUpperCase((char) ch)
);
Несмотря на существенное улучшение по сравнению с более ранними версиями Java, код Java 8 все же более многословен, чем его эквивалент на языке Scala. Излишняя тяжеловесность кода Java, а также давняя традиция использования в этом языке циклов может натолкнуть многих Java-программистов на мысль о необходимости новых методов, подобных exists, позволяющих просто переписать циклы и смириться с растущей сложностью кода.
В то же время функциональные литералы в Scala действительно воспринимаются довольно легко и задействуются очень часто. По мере углубления знакомства со Scala перед вами будет открываться все больше и больше возможностей для определения и использования собственных управляющих абстракций. Вы поймете, что это поможет избежать повторений в коде, сохраняя лаконичность и чистоту программ.
Scala — статически типизированный язык
Системы со статической типизацией классифицируют переменные и выражения в соответствии с видом хранящихся и вычисляемых значений. Scala выделяется как язык своей совершенной системой статической типизации. Обладая системой вложенных типов классов, во многом похожей на имеющуюся в Java, этот язык позволяет вам проводить параметризацию типов с помощью средств обобщенного программирования, комбинировать типы с использованием пересечений и скрывать особенности типов, применяя абстрактные типы13. Так формируется прочный фундамент для создания собственных типов, который дает возможность разрабатывать безопасные и в то же время гибкие в использовании интерфейсы.
Если вам нравятся динамические языки, такие как Perl, Python, Ruby или Groovy, то вы можете посчитать немного странным факт, что система статических типов в Scala упоминается как одна из его сильных сторон. Ведь отсутствие такой системы часто называют основным преимуществом динамических языков. Наиболее часто, говоря о ее недостатках, приводят такие аргументы, как присущая программам многословность, воспрепятствование свободному самовыражению программистов и невозможность применения конкретных шаблонов динамических изменений программных систем. Но зачастую эти аргументы направлены не против идеи статических типов в целом, а против конкретных систем типов, воспринимаемых как слишком многословные или недостаточно гибкие. Например, Алан Кей, автор языка Smalltalk, однажды заметил: «Я не против типов, но не знаю ни одной беспроблемной системы типов. Так что мне все еще нравится динамическая типизация»14.
В этой книге мы надеемся убедить вас в том, что система типов в Scala далека от проблемной. На самом деле она вполне изящно справляется с двумя обычными опасениями, связываемыми со статической типизацией: многословия удается избежать за счет логического вывода типов, а гибкость достигается благодаря сопоставлению с образцом и ряду новых способов записи и составления типов. По мере устранения этих препятствий к классическим преимуществам систем статических типов начинают относиться намного более благосклонно. Среди наиболее важных преимуществ можно назвать верифицируемые свойства программных абстракций, безопасный рефакторинг и более качественное документирование.
Верифицируемые свойства. Системы статических типов способны подтверждать отсутствие конкретных ошибок, выявляемых в ходе выполнения программы. Это могут быть следующие правила: булевы значения никогда не складываются с целыми числами; приватные переменные недоступны за пределами своего класса; функции применяются к надлежащему количеству аргументов; в множество строк можно добавлять только строки.
Существующие в настоящее время системы статических типов не выявляют ошибки других видов. Например, обычно они не обнаруживают бесконечные функции, нарушение границ массивов или деление на ноль. Вдобавок эти системы не смогут определить несоответствие вашей программы ее спецификации (при наличии таковой!). Поэтому некоторые отказываются от них, считая не слишком полезными. Аргументация такова: если эти системы могут выявлять только простые ошибки, а модульные тесты обеспечивают более широкий охват, то зачем вообще связываться со статическими типами? Мы считаем, что в этих аргументах упущено главное. Система статических типов, конечно же, не может заменить собой модульное тестирование, однако может сократить количество необходимых модульных тестов, выявляя некие свойства, которые в противном случае нужно было бы протестировать. А модульное тестирование не способно заменить статическую типизацию. Ведь Эдсгер Дейкстра (Edsger Dijkstra) сказал, что тестирование позволяет убедиться лишь в наличии ошибок, но не в их отсутствии [Dij70]. Гарантии, которые обеспечиваются статической типизацией, могут быть простыми, но это реальные гарантии, не способные обеспечить никакие объемы тестирования.
Безопасный рефакторинг. Системы статических типов дают гарантии, позволяющие вам вносить изменения в основной код, будучи совершенно уверенными в благополучном исходе этого действия. Рассмотрим, к примеру, рефакторинг, при котором к методу нужно добавить еще один параметр. В статически типизированном языке вы можете внести изменения, перекомпилировать систему и просто исправить те строки, которые вызовут ошибку типа. Сделав это, вы будете пребывать в уверенности, что были найдены все места, требовавшие изменений. То же самое справедливо для другого простого рефакторинга, например изменения имени метода или перемещения метода из одного класса в другой. Во всех случаях проверка статического типа позволяет быть вполне уверенными в том, что работоспособность новой системы осталась на уровне работоспособности старой.
Документирование. Статические типы — документация программы, проверяемой компилятором на корректность. В отличие от обычного комментария, аннотация типа никогда не станет устаревшей (по крайней мере, если содержащий ее исходный файл недавно успешно прошел компиляцию). Более того, компиляторы и интегрированные среды разработки (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()
Вывод типа в Scala может заходить довольно далеко. Фактически пользовательский код нередко вообще обходится без явного задания типов. Поэтому программы на Scala часто выглядят похожими на программы, написанные на динамически типизированных языках скриптов. Это, в частности, справедливо для прикладного клиентского кода, который склеивается из заранее написанных библиотечных компонентов. Но для них самих это менее характерно, поскольку в них зачастую применяются довольно сложные типы, не допускающие гибкого использования таких схем. И это вполне естественно. Ведь сигнатуры типов элементов, составляющих интерфейс повторно используемых компонентов, должны задаваться в явном виде, поскольку составляют существенную часть соглашения между компонентом и его клиентами.
1.4. Истоки Scala
На идею создания Scala повлияли многие языки программирования и идеи, выработанные на основе исследований таких языков. Фактически обновления в Scala незначительны — большинство характерных особенностей языка уже применялось в том или ином виде в других языках программирования. Инновации в Scala появляются в основном из того, как его конструкции сводятся воедино. В этом разделе будут перечислены основные факторы, оказавшие влияние на структуру языка Scala. Перечень не может быть исчерпывающим, поскольку в дизайне языков программирования так много толковых идей, что перечислить здесь их все просто невозможно.
На внешнем уровне Scala позаимствовал существенную часть синтаксиса у Java и C#, которые, в свою очередь, взяли большинство своих синтаксических соглашений у C и C++. Выражения, инструкции и блоки — в основном из Java, как, собственно, и синтаксис классов, создание пакетов и импорт15. Кроме синтаксиса, 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, Multi-Java и самом Java 8. Существуют также изначально функциональные языки, которые приобрели систему объектов. В качестве примера можно привести OCaml, F# и PLT-Scheme.
В Scala применяются также некоторые нововведения в области языков программирования. Например, его абстрактные типы — более объектно-ориентированная альтернатива обобщенным типам, его трейты позволяют выполнять гибкую сборку компонентов, а экстракторы обеспечивают независимый от представления способ сопоставления с образцом. Эти нововведения были озвучены в статьях на конференциях по языкам программирования в последние годы16.
Резюме
Ознакомившись с текущей главой, вы получили некоторое представление о том, что такое Scala и как он может помочь программисту в работе. Разумеется, этот язык не решит все ваши проблемы и не увеличит волшебным образом вашу личную продуктивность. Следует заранее предупредить, что Scala нужно применять искусно, а для этого потребуется получить некоторые знания и практические навыки. Если вы перешли к Scala от языка Java, то одними из наиболее сложных аспектов его изучения для вас могут стать система типов Scala, которая существенно богаче, чем у Java, и его поддержка функционального стиля программирования. Цель данной книги — послужить руководством при поэтапном, от простого к сложному, изучении особенностей Scala. Полагаем, что вы приобретете весьма полезный интеллектуальный опыт, расширяющий ваш кругозор и изменяющий взгляд на проектирование программных средств. Надеемся, что вдобавок вы получите от программирования на Scala истинное удовольствие и познаете творческое вдохновение.
В следующей главе вы приступите к написанию кода Scala.
3 Scala произносится как «скала».
4 Пожалуйста, не сердитесь на нас, если не сможете разобраться во всех тонкостях этой программы. Объяснения будут даны в двух следующих главах.
5 factorial(x), или x! в математической записи — результат вычисления 1*2*∙∙∙*x, где для 0! определено значение 1.
6 Scala поставляется со стандартной библиотекой, часть которой будет рассмотрена в книге. За дополнительной информацией можно обратиться к имеющейся в библиотеке документации Scaladoc, доступной в дистрибутиве и в интернете по адресу www.scala-lang.org.
7 Начиная с Java 8, у интерфейсов могут быть реализации методов по умолчанию, но они не предлагают всех тех возможностей, которые есть у трейтов языка Scala.
8 Изначально существовала реализация Scala, запускаемая на платформе .NET, но она больше не используется. В последнее время все большую популярность набирает реализация Scala под названием Scala.js, запускаемая на JavaScript.
9 В версии 3.0.0 стандартные расширения реализованы посредством неявных преобразований. В последующих версиях Scala они будут заменены методами расширения.
10 Единственное отличие заключается в том, что переменные экземпляра, полученные в случае применения Scala, будут финальными (final). Как сделать их не финальными, рассказывается в разделе 10.6.
11 Функциональный литерал может называться предикатом, если результирующим типом будет Boolean.
12 Такое использование символа подчеркивания в качестве заместителя для аргументов рассматривается в разделе 8.5.
13 Обобщенные типы рассматриваются в главе 18, пересечения (например, A с B с C) — в разделе 17.5, а абстрактные типы — в главе 20.
14 Kay A.C. Электронное письмо о значении объектно-ориентированного программирования [Kay03].
15 Главное отличие от Java касается синтаксиса для объявления типов: вместо «Тип переменная», как в Java, задействуется форма «переменная: Тип». Используемый в Scala постфиксный синтаксис типа похож на синтаксис, применяемый в Pascal, Modula-2 или Eiffel. Основная причина такого отклонения имеет отношение к логическому выводу типов, зачастую позволяющему опускать тип переменной или тип возвращаемого методом значения. Легче использовать синтаксис «переменная: Тип», поскольку двоеточие и тип можно просто не указывать. Но в стиле языка C, применяющем форму «Тип переменная», просто так не указывать тип нельзя, поскольку при этом исчезнет сам признак начала определения. Неуказанный тип в качестве заполнителя требует какое-нибудь ключевое слово (C# 3.0, в котором имеется логический вывод типов, для этой цели задействует ключевое слово var). Такое альтернативное ключевое слово представляется несколько более надуманным и менее привычным, чем подход, который используется в Scala.
16 Для получения дополнительной информации см. [Ode03], [Ode05] и [Emi07] в библиографии.
Тип BigInt похож на встроенный, поскольку со значениями этого типа можно использовать целочисленные литералы и операторы наподобие * и –. Тем не менее это просто класс, определение которого задано в стандартной библиотеке Scala6. Если бы класса не было, то любой программист на Scala мог бы запросто написать его реализацию, например создав оболочку для имеющегося в языке Java класса java.math.BigInteger (фактически именно так и реализован класс BigInt в Scala).
Scala не требует резко отходить от платформы Java, чтобы опередить на шаг этот язык. Scala позволяет повышать ценность уже существующего кода, то есть опираться на то, что у вас уже есть, поскольку он был разработан для достижения беспрепятственной совместимости с Java8. Программы на Scala компилируются в байт-коды виртуальной машины Java (JVM). Производительность при выполнении этих кодов находится на одном уровне с производительностью программ на Java. Код Scala может вызывать методы Java, обращаться к полям этого языка, поддерживать наследование от его классов и реализовывать его интерфейсы. Для всего перечисленного не требуются ни специальный синтаксис, ни явные описания интерфейса, ни какой-либо связующий код. По сути, весь код Scala интенсивно использует библиотеки Java, зачастую даже без ведома программистов.
Scala означает «масштабируемый язык» (от англ. scalable language). Это название он получил, поскольку был спроектирован так, чтобы расти вместе с запросами своих пользователей. Язык Scala может решать широкий круг задач программирования: от написания небольших скриптов до создания больших систем3.
Рассмотрим пример. Многие приложения нуждаются в целочисленном типе, который при выполнении арифметических операций может становиться произвольно большим без переполнения или циклического перехода в начало. В Scala такой тип определяется в библиотеке класса scala.math.BigInt. Определение использующего этот тип метода, который вычисляет факториал переданного ему целочисленного значения, имеет следующий вид5:
Изначально существовала реализация Scala, запускаемая на платформе .NET, но она больше не используется. В последнее время все большую популярность набирает реализация Scala под названием Scala.js, запускаемая на JavaScript.
Начиная с Java 8, у интерфейсов могут быть реализации методов по умолчанию, но они не предлагают всех тех возможностей, которые есть у трейтов языка Scala.
В версии 3.0.0 стандартные расширения реализованы посредством неявных преобразований. В последующих версиях Scala они будут заменены методами расширения.
Пожалуйста, не сердитесь на нас, если не сможете разобраться во всех тонкостях этой программы. Объяснения будут даны в двух следующих главах.
Scala произносится как «скала».
Scala поставляется со стандартной библиотекой, часть которой будет рассмотрена в книге. За дополнительной информацией можно обратиться к имеющейся в библиотеке документации Scaladoc, доступной в дистрибутиве и в интернете по адресу www.scala-lang.org.
factorial(x), или x! в математической записи — результат вычисления 1*2*∙∙∙*x, где для 0! определено значение 1.
Java-типы в Scala не только заимствованы, но и «принаряжены» для придания им привлекательности. Например, строки в Scala поддерживают такие методы, как toInt или toFloat, которые преобразуют строки в целое число или число с плавающей точкой. То есть вместо Integer.parseInt(str) вы можете написать str.toInt. Как такое возможно без нарушения совместимости? Класс String в Java определенно не имеет метода toInt! Фактически у Scala есть очень общее решение для устранения этого противоречия между передовой разработкой и функциональной совместимостью9. Scala позволяет определять многофункциональные расширения, которые всегда применяются при выборе несуществующих элементов. В рассматриваемом случае при поиске метода toInt для работы со строковым значением компилятор Scala не найдет такого элемента в классе String. Однако он найдет неявное преобразование, превращающее Java-класс String в экземпляр Scala-класса StringOps, в котором такой элемент определен. Затем преобразование будет автоматически применено, прежде чем будет выполнена операция toInt.
Когда речь заходит о составлении объектов, Scala проявляется как более совершенный язык по сравнению с большинством других. В качестве примера приведем имеющиеся в Scala трейты. Они подобны интерфейсам в Java, но могут содержать также реализации методов и даже поля7. Объекты создаются путем композиции примесей, при котором к членам класса добавляются члены нескольких трейтов. Таким образом, различные аспекты классов могут быть инкапсулированы в разных трейтах. Это выглядит как множественное наследование, но есть разница в конкретных деталях. В отличие от класса трейт может добавить в суперкласс новые функциональные возможности. Это придает трейтам более высокую степень подключаемости по сравнению с классами. В частности, благодаря этому удается избежать возникновения присущих множественному наследованию классических проблем «ромбовидного» наследования, которые возникают, когда один и тот же класс наследуется по нескольким различным путям.
Для получения дополнительной информации см. [Ode03], [Ode05] и [Emi07] в библиографии.
Обобщенные типы рассматриваются в главе 18, пересечения (например, A с B с C) — в разделе 17.5, а абстрактные типы — в главе 20.
Такое использование символа подчеркивания в качестве заместителя для аргументов рассматривается в разделе 8.5.
Главное отличие от Java касается синтаксиса для объявления типов: вместо «Тип переменная», как в Java, задействуется форма «переменная: Тип». Используемый в Scala постфиксный синтаксис типа похож на синтаксис, применяемый в Pascal, Modula-2 или Eiffel. Основная причина такого отклонения имеет отношение к логическому выводу типов, зачастую позволяющему опускать тип переменной или тип возвращаемого методом значения. Легче использовать синтаксис «переменная: Тип», поскольку двоеточие и тип можно просто не указывать. Но в стиле языка C, применяющем форму «Тип переменная», просто так не указывать тип нельзя, поскольку при этом исчезнет сам признак начала определения. Неуказанный тип в качестве заполнителя требует какое-нибудь ключевое слово (C# 3.0, в котором имеется логический вывод типов, для этой цели задействует ключевое слово var). Такое альтернативное ключевое слово представляется несколько более надуманным и менее привычным, чем подход, который используется в Scala.
Kay A.C. Электронное письмо о значении объектно-ориентированного программирования [Kay03].
Эта программа устанавливает отображение стран на их столицы, модифицирует отображение, добавляя новую конструкцию ("Japan"–>"Tokyo"), и выводит название столицы, связанное со страной France4. В этом примере используется настолько высокоуровневая система записи, что она не загромождена ненужными точками с запятыми и сигнатурами типов. И действительно возникает ощущение использования современного языка скриптов наподобие Perl, Python или Ruby. Одна из общих характеристик этих языков, применимая к данному примеру, — поддержка всеми ими в синтаксисе языка конструкции ассоциативного отображения.
Функциональный литерал может называться предикатом, если результирующим типом будет Boolean.
Единственное отличие заключается в том, что переменные экземпляра, полученные в случае применения Scala, будут финальными (final). Как сделать их не финальными, рассказывается в разделе 10.6.
2. Первые шаги в Scala
Пришло время написать какой-нибудь код на Scala. Прежде чем углубиться в руководство по этому языку, мы приведем две обзорные главы по нему и, что наиболее важно, заставим вас приступить к написанию кода. Рекомендуем по мере освоения материала на практике проверить работу всех примеров кода, представленных в этой и последующей главах. Лучше всего приступить к изучению Scala, программируя на данном языке.
Запуск представленных далее примеров возможен с помощью стандартной установки Scala. Чтобы ее осуществить, перейдите по адресу www.scala-lang.org/downloads и следуйте инструкциям для вашей платформы. На этой странице описано несколько способов установки Scala. Будем считать, что вы уже установили двоичные файлы Scala и добавили их в переменную окружения path17, что необходимо для выполнения шагов из этой главы.
Если вы опытный программист, но новичок в Scala, то внимательно прочитайте следующие две главы: в них приводится достаточный объем информации, позволяющий приступить к написанию полезных программ на этом языке. Если же опыт программирования у вас невелик, то часть материалов может показаться чем-то загадочным. Однако не стоит переживать. Чтобы ускорить процесс изучения, нам пришлось обойтись без некоторых подробностей. Более обстоятельные пояснения мы представим в последующих главах. Кроме того, в следующих двух главах дадим ряд сносок с указанием разделов книги, в которых можно найти более подробные объяснения.
Шаг 1. Осваиваем Scala REPL
Самый простой способ начать работу со Scala — использовать Scala REPL18, интерактивную оболочку для написания выражений и программ Scala. REPL, который называется scala, оценивает введенные вами выражения и выводит полученное значение. Чтобы его использовать, нужно набрать scala в командной строке19:
$ 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 — они разбивают глобальное пространство имен на части и предоставляют механизм для сокрытия данных20. Значения класса 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 по их простым именам21, запись можно упростить:
scala> val msg3: String = "Hello yet again, world!"
msg3: String = Hello yet again, world!
Возвратимся к исходной переменной msg. Поскольку она определена, то ею можно воспользоваться в соответствии с вашими ожиданиями, например:
scala> println(msg)
Hello, world!
Учитывая, что msg является val-, а не var-переменной, вы не сможете повторно присвоить ей другое значение22. Посмотрите, к примеру, как REPL выражает свое недовольство при попытке сделать следующее:
scala> 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. Она определяет результирующийтип самой функции max23. За типом результата функции следует знак равенства и тело функции, которое отделено отступами. В этом случае тело содержит одно выражение if, которое в качестве результата функции max выбирает либо x, либо y, в зависимости от того, что больше. Как показано здесь, выражение if в Scala может приводить к значению, аналогичному тернарному оператору Java. Например, в Scala выражение ifx>ythenxelsey вычисляется точно так же, как выражение (x>y)?x:y в Java. Знак равенства, предшествующий телу функции, дает понять, что с точки зрения функционального мира функция определяет выражение, результатом вычисления которого становится значение. Основная структура функции показана на рис. 2.1.
Рис. 2.1. Основная форма определения функции в Scala
Иногда компилятор Scala может потребовать от вас указать результирующий тип функции. Если, к примеру, функция является рекурсивной24, то вы должны указать ее результирующий тип явно. Но в случае с функцией max вы можете не указывать результирующий тип функции — компилятор выведет его самостоятельно25. Кроме того, если функция состоит всего лишь из одного оператора, вы сможете целиком написать ее в одну строку. Таким образом, у вас появляется альтернативный вариант реализации функции 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 откликается следующим приветствием: defgreet():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!")
а затем запустите файл на выполнение:
$ scala hello.scala
И вы получите еще одно приветствие:
Hello, world, from a script!
В этом примере функция, отмеченная @main, называется m (от слова main), но это имя не имеет значения для выполнения скрипта. Чтобы скрипт сработал, вам необходимо запустить Scala и указать имя файла, содержащего функцию main, а не имя этой функции.
Вы можете получить доступ к аргументам командной строки, переданным вашему скрипту, приняв их в качестве параметров вашей основной функции. Например, вы можете принять строковые аргументы, взяв параметр со специальной аннотацией типа String*, что означает от нуля до многих повторяющихся параметров типа String26. Внутри основной функции параметр будет иметь тип 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. В следующем разделе будут показаны более рациональные подходы, позволяющие избежать повторения последовательностей с помощью индексов.
Этот скрипт начинается с определения переменой, vari=0. Вывод типов относит переменную i к типу Int, поскольку это тип ее начального значения 0. Конструкция while на следующей строке заставляет блок (две строки кода снизу) повторно выполняться, пока булево выражение i<args.length будет вычисляться в false. Метод args.length вычисляет длину последовательности args. Блок содержит две инструкции, каждая из которых набрана с отступом в два пробела, что является рекомендуемым стилем отступов для кода на Scala. Первая инструкция, println(args(i)), выводит на экран i-й аргумент командной строки. Вторая, i+=1, увеличивает значение переменной i на единицу. Обратите внимание: Java-код ++i и i++ в Scala не работает. Чтобы в Scala увеличить значение переменной на единицу, нужно использовать одно из двух выражений: либо i=i+1, либо i+=1. Запустите этот скрипт с помощью команды, показанной ниже:
$ 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. Чтобы эту строку можно было прочитать, перед каждым аргументом, за исключением первого, благодаря использованию конструкции ifi!=0then вставляется пробел. При первом проходе цикла 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. Если функциональный литерал функции состоит из одной инструкции, принимающей один аргумент, то обозначать данный аргумент явным образом по имени не нужно27. Поэтому работать будет и следующий код:
@main def m(args: String*) =
args.foreach(println)
Резюмируем усвоенное: синтаксис для функционального литерала представляет собой список поименованных параметров, заключенный в круглые скобки, а также правую стрелку, за которой следует тело функции. Этот синтаксис показан на рис. 2.2.
Рис. 2.2. Синтаксис функционального литерала в Scala
Теперь вы можете поинтересоваться: что же случилось с теми проверенными циклами for, которые вы привыкли использовать в таких императивных языках, как Java или Python? Придерживаться функционального направления в Scala возможно с помощью только одного функционального родственника императивной конструкции for, который называется выражением for. Поскольку вы не сможете понять всю его эффективность и выразительность, пока не доберетесь до раздела 7.3 (или не заглянете в него), здесь о нем будет дано лишь общее представление. Наберите в новом файле по имени forargs.scala следующий код:
@main def m(args: String*) =
for arg <- args do
println(arg)
Между for и do находится arg<-args28. Справа от символа <– расположена уже знакомая вам последовательность args. Слева от <– указана переменная arg, относящаяся к val-, а не к var-переменным (так как она всегда относится к val-переменным, записывается только arg, а не valarg). Может показаться, что arg относится к var-переменной, поскольку она будет получать новое значение при каждой итерации, однако в действительности она относится к val-переменной: arg не может получить новое значение внутри тела выражения. Вместо этого для каждого элемента массива args будет создана новая val-переменная по имени arg, которая будет инициализирована значением элемента, и тело for будет выполнено.
Если скрипт forargs.scala запустить с помощью команды:
$ scala forargs.scala for arg in args
то вы увидите:
for
arg
in
args
Диапазон применения выражения for значительно шире, но для начала этого примера достаточно. Мы расскажем вам больше о for в шаге 12 главы 3 и в разделе 7.3.
Резюме
В данной главе мы привели основную информацию о Scala. Надеемся, вы воспользовались возможностью создать код на этом языке. В следующей главе мы продолжим вводный обзор и рассмотрим более сложные темы.
17 Мы протестировали примеры из этой книги со Scala версии 3.0.0.
18 REPL означает read, evaluate, print, loop («чтение, оценка, печать, цикл»).
19 Если вы используете Windows, вам нужно будет ввести команду scala в оболочке командной строки.
20 Если вы не знакомы с пакетами Java, то их можно рассматривать как средство предоставления классам полных имен. Int входит в пакет scala. Int — простое имя класса, а scala.Int — полное. Подробнее о пакетах рассказывается в главе 12.
21 Простым именем java.lang.String является String.
22 Но в интерпретаторе новую val-переменную можно определить с именем, которое до этого уже использовалось. Этот механизм рассматривается в разделе 7.7.
23 В Java тип возвращаемого из метода значения является возвращаемым типом. В Scala то же самое понятие называется результирующим типом.
24 Функция называется рекурсивной, если вызывает саму себя.
25 Тем не менее зачастую есть смысл указывать результирующий тип явно, даже когда компилятор этого не требует. Такая аннотация типа может упростить чтение кода, поскольку читателю не придется изучать тело функции, чтобы определить, каким будет вывод результирующего типа.
26 Повторяющиеся параметры описаны в разделе 8.8.
27 Это сокращение, которое называется частично применяемой функцией, описано в разделе 8.6.
28 Вы можете интерпретировать символ <– как in. Следовательно, выражение forarg<–argsdo можно прочитать как forarginargsdo.
Если вы используете Windows, вам нужно будет ввести команду scala в оболочке командной строки.
REPL означает read, evaluate, print, loop («чтение, оценка, печать, цикл»).
Мы протестировали примеры из этой книги со Scala версии 3.0.0.
Простым именем java.lang.String является String.
Если вы не знакомы с пакетами Java, то их можно рассматривать как средство предоставления классам полных имен. Int входит в пакет scala. Int — простое имя класса, а scala.Int — полное. Подробнее о пакетах рассказывается в главе 12.
Это сокращение, которое называется частично применяемой функцией, описано в разделе 8.6.
Повторяющиеся параметры описаны в разделе 8.8.
Вы можете интерпретировать символ <– как in. Следовательно, выражение forarg<–argsdo можно прочитать как forarginargsdo.
В Java тип возвращаемого из метода значения является возвращаемым типом. В Scala то же самое понятие называется результирующим типом.
Но в интерпретаторе новую val-переменную можно определить с именем, которое до этого уже использовалось. Этот механизм рассматривается в разделе 7.7.
Тем не менее зачастую есть смысл указывать результирующий тип явно, даже когда компилятор этого не требует. Такая аннотация типа может упростить чтение кода, поскольку читателю не придется изучать тело функции, чтобы определить, каким будет вывод результирующего типа.
Функция называется рекурсивной, если вызывает саму себя.
3. Дальнейшие шаги в Scala
В этой главе продолжается введение в Scala, начатое в предыдущей главе. Здесь мы рассмотрим более сложные функциональные возможности. Когда вы усвоите материал главы, у вас будет достаточно знаний, чтобы начать создавать полезные скрипты на Scala. Мы вновь рекомендуем по мере чтения текста получать практические навыки с помощью приводимых примеров. Лучше всего осваивать Scala, начиная создавать код на данном языке.
Шаг 7. Параметризуем массивы типами
В Scala создавать объекты или экземпляры класса можно с помощью ключевого слова new. При создании объекта в Scala вы можете параметризовать его значениями и типами. Параметризация означает «конфигурирование» экземпляра при его создании. Параметризация экземпляра значениями производится путем передачи конструктору объектов в круглых скобках. Например, код Scala, который показан ниже, создает новый объект java.math.BigInteger, выполняя его параметризацию значением "12345":
val big = new java.math.BigInteger("12345")
Параметризация экземпляра типами выполняется с помощью указания одного или нескольких типов в квадратных скобках. Пример показан в листинге 3.1. Здесь greetStrings — значение типа Array[String] («массив строк»), инициализируемое длиной 3 путем его параметризации значением 3 в первой строке кода. Если запустить код в листинге 3.1 в качестве скрипта, то вы увидите еще одно приветствие Hello,world!. Учтите, что при параметризации экземпляра как типом, так и значением тип стоит первым и указывается в квадратных скобках, а за ним следует значение в круглых скобках.
Листинг 3.1. Параметризация массива типом
val greetStrings = new Array[String](3)
greetStrings(0) = "Hello"
greetStrings(1) = ", "
greetStrings(2) = "world!\n"
for i <- 0 to 2 do
print(greetStrings(i))
Примечание
Хотя код в листинге 3.1 содержит важные понятия, он не показывает рекомендуемый способ создания и инициализации массива в Scala. Более рациональный способ будет показан в листинге 3.2.
Если вы склонны делать более явные указания, то тип greetStrings можно обозначить так:
val greetStrings: Array[String] = new Array[String](3)
С учетом имеющегося в Scala вывода типов эта строка кода семантически эквивалентна первой строке листинга 3.1. Но в данной форме показано следующее: часть параметризации, которая относится к типу (название типа в квадратных скобках), формирует часть типа экземпляра, однако часть параметризации, относящаяся к значению (значения в круглых скобках), в формировании не участвует. Типом greetStrings является Array[String], а не Array[String](3).
В следующих трех строках кода в листинге 3.1 инициализируется каждый элемент массива greetStrings:
greetStrings(0) = "Hello"
greetStrings(1) = ", "
greetStrings(2) = "world!\n"
Как уже упоминалось, доступ к массивам в Scala осуществляется за счет помещения индекса элемента в круглые, а не в квадратные скобки, как в Java. Следовательно, нулевым элементом массива будет greetStrings(0), а не greetStrings[0].
Эти три строки кода иллюстрируют важное понятие, помогающее осмыслить значение для Scala val-переменных. Когда переменная определяется с помощью val, повторно присвоить значение данной переменной нельзя, но объект, на который она ссылается, потенциально может быть изменен. Следовательно, в данном случае присвоить greetStrings значение другого массива невозможно — переменная greetStrings всегда будет указывать на один и тот же экземпляр типа Array[String], которым она была инициализирована. Но впоследствии в элементы типа Array[String] можно вносить изменения, то есть сам массив является изменяемым.
Последние две строки листинга 3.1 содержат выражение for, которое поочередно выводит каждый элемент массива greetStrings:
for i <- 0 to 2 do
print(greetStrings(i))
В первой строке кода для этого выражения for показано еще одно общее правило Scala: если метод получает лишь один параметр, то его можно вызвать без точки или круглых скобок. В данном примере to на самом деле является методом, получающим один Int-аргумент. Код 0to2 преобразуется в вызов метода 0.to(2)29. Следует заметить, что этот синтаксис работает только при явном указании получателя вызова метода. Код println10 использовать нельзя, а код Consoleprintln10 — можно.
С технической точки зрения в Scala нет перегрузки операторов, поскольку в нем фактически отсутствуют операторы в традиционном понимании. Вместо этого такие символы, как +, -, *,
