Scala. Профессиональное программирование
Қосымшада ыңғайлырақҚосымшаны жүктеуге арналған QRRuStore · Samsung Galaxy Store
Huawei AppGallery · Xiaomi GetApps

автордың кітабын онлайн тегін оқу  Scala. Профессиональное программирование

 

Мартин Одерски, Лекс Спун, Билл Веннерс
Scala. Профессиональное программирование. 4-е изд.
2021

Научный редактор Н. Искра

Переводчик О. Сивченко

Литературный редактор Н. Хлебина

Художник В. Мостипан

Корректор Е. Павлович


 

Мартин Одерски, Лекс Спун, Билл Веннерс

Scala. Профессиональное программирование. 4-е изд.. — СПб.: Питер, 2021.

 

ISBN 978-5-4461-1827-4

© ООО Издательство "Питер", 2021

 

Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.

 

Предисловие

Когда в 2004 году, на заре своей карьеры, я сел и выбрал малоизвестный язык под названием Scala, мне и в голову не могло прийти, что впереди меня ждет множество открытий. Поначалу мой опыт использования Scala во многом напоминал работу с другими языками: пробы, ошибки, эксперименты и открытия, заблуждения и наконец просветление. В те дни у нас было очень мало источников информации: никаких учебных пособий, блогов или опытных пользователей, желавших поделиться знаниями. А уж таких ресурсов, как эта книга, у нас точно не было. Все, что мы имели, — язык с прекрасным набором новых инструментов, которыми еще никто не умел как следует пользоваться. Это открывало новые горизонты и в то же время приводило в замешательство! С опытом программирования на Java у меня сформировались определенные ожидания, однако мои ощущения от повседневного написания кода на Scala были другими. Я помню свои первые приключения со Scala, когда мы вместе еще с одним человеком работали над небольшим проектом. Решив заняться рефакторингом (ввиду регулярного обнаружения и изучения новых возможностей и методик это случалось довольно часто), я проходил несколько этапов компиляции.

Каждый раз компилятор выдавал мне список ошибок вместе с номерами строк. И всякий раз я переходил к нужному участку кода, пытался понять, что с ним не так, вносил изменения, чтобы исправить ошибки (или, скорее, переместить их в другое место). Но компилятор раз за разом направлял меня прямо к источнику проблемы. Иногда это могло повторяться на протяжении нескольких дней без единой успешной компиляции. Но, постепенно уменьшив количество ошибок с сотни до десяти, а затем и до нуля, я наконец впервые смог скомпилировать и запустить существенно переработанный проект.

И, вопреки ожиданиям, он заработал с первого раза. Я был молодым программистом, который до этого писал только на Java, Perl, Pascal, BASIC, PHP и JavaScript, и это произвело на меня неизгладимое впечатление.

На первой конференции Scala World, которую я организовал в 2015 году, во вступительной презентации Рунара Бьярнасона (Ru'nar Bjarnason) была озвучена следующая мысль: «Ограничения освобождают, а свободы сковывают». Нагляднее всего это демонстрирует процесс компиляции в Scala: понимание того, что scalac накладывает исчерпывающий набор ограничений, не позволяющих программисту допускать предсказуемые ошибки времени выполнения (хуже которых ничего нет), должно давать ощущение свободы любому программисту. Оно должно придавать уверенность, а это, в свою очередь, позволяет экспериментировать, исследовать и вносить амбициозные изменения даже при отсутствии полноценного набора тестов.

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

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

На момент написания этих строк широкое внедрение Scala 3 ожидается лишь через несколько лет, и в ближайшем будущем Scala 2 останется фактически стандартной версией языка.

В последнем выпуске Scala 2 (версия 2.13), подробно рассматриваемом в данной книге, в стандартной библиотеке появились новые, переработанные и упрощенные коллекции, вобравшие в себя множество уроков, усвоенных с момента последнего крупного изменения (Scala 2.8). Эти новые коллекции поддерживают компиляцию в Scala 2 и 3, формируя основу для большей части кода, который мы будем писать еще и в грядущем десятилетии. Учитывая то, какой интерес у людей вызывает следующий выпуск Scala, самое время обзавестись этой книгой и начать ее изучать!

Джон Претти (Jon Pretty), Краков, Польша, 14 сентября 2019 года

Благодарности

Мы благодарны за вклад в эту книгу многим людям.

Сам язык Scala — плод усилий множества специалистов. Свой вклад в проектирование и реализацию версии 1.0 внесли Филипп Альтер (Philippe Altherr), Винсент Кремет (Vincent Cremet), Жиль Дюбоше (Gilles Dubochet), Бурак Эмир (Burak Emir), Стефан Мишель (Ste'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), Стефан Мишель (Ste'phane Micheloud), Тод Мильштейн (Todd Millstein), Дэвид Поллак (David Pollak), Фрэнк Соммерс (Frank Sommers), Филип Уодлер (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, воспользовавшись ссылкой Suggest. Отсортируем их имена по убыванию количества комментариев, а затем в алфавитном порядке. Наших благодарностей заслуживают Дэвид Бизак (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) за помощь в завершении третьего издания.

Наконец, мы хотели бы поблагодарить Жюльена Ричарда-Фоя за работу над обновлением четвертого издания этой книги до версии Scala 2.13, в частности за перепроектирование библиотеки коллекций.

Введение

Эта книга — руководство по Scala, созданное людьми, непосредственно занимающимися разработкой данного языка программирования. Нашей целью было научить вас всему, что необходимо для превращения в продуктивного программиста на языке Scala. Все примеры в книге компилируются Scala версии 2.13.1.

Целевая аудитория

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

Как пользоваться книгой

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

Обнаружив незнакомое понятие, можно обратиться к глоссарию. Многие читатели бегло просматривают части книги, и это вполне нормально. Но при встрече с непонятными терминами можно выяснить, что просмотр был слишком поверхностным, и вернуться к справочным материалам.

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

Как изучать Scala

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

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

Затем можно поучаствовать в работе множества онлайн-форумов. Это позволит вам и многим другим приверженцам Scala помочь друг другу в его освоении. Есть множество рассылок, дискуссионных форумов, чатов, вики-источников и несколько информационных каналов, которые посвящены этому языку и содержат соответствующие публикации. Уделите время поиску источников информации, более всего отвечающих вашим запросам. Вы будете гораздо быстрее решать мелкие проблемы, что позволит уделять больше времени более серьезным и глубоким вопросам.

И наконец, получив при чтении книги достаточный объем знаний, приступайте к разработке собственного проекта. Поработайте с нуля над созданием какой-нибудь небольшой программы или разработайте дополнение к более объемной. Вы не добьетесь быстрых результатов одним только чтением.

Условные обозначения

При первом упоминании какого-либо понятия или термина его название дается зеленым цветом. Для небольших встроенных в текст примеров кода, таких как x+1, используется моноширинный шрифт. Большие примеры кода представлены в виде отдельных блоков, для которых тоже используется моноширинный шрифт:

def hello() = {

  println("Hello, world!")

}

Когда показывается работа с интерактивной оболочкой, ответы последней выделяются шрифтом на сером фоне:

scala> 3 + 4

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 «Трейты» охватывает существующий в Scala механизм создания композиции примесей. Показана работа трейтов, описываются примеры их наиболее частого использования и объясняется, как с помощью трейтов совершенствуется традиционное множественное наследование.

• Глава 13 «Пакеты и импорты» рассматривает вопросы программирования в целом, включая высокоуровневые пакеты, инструкции импортирования и модификаторы управления доступом, такие как protected и private.

• Глава 14 «Утверждения и тесты» показывает существующий в Scala механизм утверждений и дает обзор ряда инструментов, доступных для написания тестов в этом языке. Основное внимание уделено средству ScalaTest.

• Глава 15 «Case-классы и сопоставление с образцом» вводит двойные конструкции, поддерживающие ваши действия при написании обычных, неинкапсулированных структур данных. Case-классы и сопоставление с образцом будут особенно полезны при работе с древовидными рекурсивными данными.

• Глава 16 «Работа со списками» подробно рассматривает списки, которые, вероятно, можно отнести к самым востребованным структурам данных в программах на Scala.

• Глава 17 «Работа с другими коллекциями» показывает способы использования основных коллекций Scala, таких как списки, массивы, кортежи, наборы и отображения.

• Глава 18 «Изменяемые объекты» объясняет суть изменяемых объектов и синтаксиса для выражения этих объектов, обеспечиваемого Scala. Глава завершается практическим примером моделирования дискретного события, в котором показан ряд изменяемых объектов в действии.

• Глава 19 «Параметризация типов» на конкретных примерах объясняет некоторые из технологических приемов скрытия информации, представленных в главе 13, например создание класса для функциональных запросов. Материа­лы главы подводят к описанию вариантности параметров типа и способов их взаимодействия с скрытием информации.

• Глава 20 «Абстрактные члены» дает описание всех разновидностей поддерживаемых Scala абстрактных членов — не только методов, но и полей и типов, которые можно объявлять абстрактными.

• Глава 21 «Неявные преобразования и параметры» рассматривает две конструкции, которые могут помочь избавить исходный код от излишней детализации, позволяя предоставить ее самому компилятору.

• Глава 22 «Реализация списков» описывает реализацию класса List. В ней рассматривается работа списков в Scala, в которой важно разбираться. Кроме того, реализация списков показывает, как используется ряд характерных особенностей этого языка.

• Глава 23 «Возвращение к выражениям for» показывает, как последние превращаются в вызовы методов map, flatMap, filter и foreach.

• Глава 24 «Углубленное изучение коллекций» предлагает углубленный обзор библиотеки коллекций.

• Глава 25 «Архитектура коллекций Scala» показывает устройство библиотеки коллекций и то, как можно реализовывать собственные коллекции.

• Глава 26 «Экстракторы» демонстрирует способы применения поиска по шаблонам в отношении не только case-классов, но и любых произвольных классов.

• Глава 27 «Аннотации» показывает, как можно работать с расширениями языка с помощью аннотаций. Описывает несколько стандартных аннотаций и описывает способы создания собственных аннотаций.

• Глава 28 «Работа с XML» объясняет порядок обработки в Scala соответству­ющих данных. Показаны идиомы для создания XML, проведения синтаксического анализа данных в этом формате и их обработки после анализа.

• Глава 29 «Модульное программирование с использованием объектов» показывает способы применения объектов Scala в качестве модульной системы.

• Глава 30 «Равенство объектов» освещает ряд проблемных вопросов, которые возникают при написании метода equals. Вдобавок рассматривается ряд узких мест, которые следует обходить стороной.

• Глава 31 «Сочетание кода на Scala и Java» рассматривает проблемы, возникающие при сочетании совместно используемых кодов на Scala и Java в одном и том же проекте, и предлагает способы, позволяющие находить решения таких проблем.

• Глава 32 «Фьючерсы и многопоточность» описывает способы применения класса Future. Для программ на Scala могут использоваться примитивы многопоточных вычислений и библиотеки, применяемые на Java-платформе. Однако фьючерсы помогут избежать возникновения условий взаимных блокировок и состояний гонки, которые мешают реализовывать традиционные подходы к многопоточности в виде потоков и блокировок.

• Глава 33 «Синтаксический разбор с помощью комбинаторов» показывает способы создания парсеров с использованием имеющейся в Scala библиотеки парсер-комбинаторов.

• Глава 34 «Программирование GUI» предлагает краткий обзор библиотеки Scala, упрощающей этот вид программирования с помощью среды Swing.

• Глава 35 «Электронная таблица SCells» связывает все воедино, показывая полнофункциональное приложение электронной таблицы, написанное на языке Scala.

Ресурсы

Самые последние версии Scala, ссылки на документацию и ресурсы сообщества можно найти на сайте www.scala-lang.org.

Исходный код

Исходный код, рассматриваемый в данной книге, выпущенный под открытой лицензией в виде ZIP-файла, можно найти на сайте книги: booksites.artima.com/programming_in_scala_4ed.

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение!

На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

1. Масштабируемый язык

Scala означает «масштабируемый язык» (от англ. scalable language). Это название он получил, поскольку был спроектирован так, чтобы расти вместе с запросами своих пользователей. Язык Scala может решать широкий круг задач программирования: от написания небольших скриптов до создания больших систем1.

Освоить Scala нетрудно. Он запускается на стандартной Java-платформе и полноценно взаимодействует со всеми Java-библиотеками. Язык очень хорошо подходит для написания скриптов, объединяющих Java-компоненты. Но его сильные стороны проявляются еще лучше при создании больших систем и сред повторно используемых компонентов.

С технической точки зрения 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"), и выводит название столицы, связанное со страной France2. В этом примере используется настолько высокоуровневая система записи, что она не загромождена ненужными точками с запятыми и сигнатурами типов. И действительно возникает ощущение использования современного языка скриптов наподобие Perl, Python или Ruby. Одна из общих характеристик этих языков, применимая к данному примеру, — поддержка всеми ими в синтаксисе языка конструкции ассоциативного отображения.

Ассоциативные отображения очень полезны, поскольку помогают поддерживать понятность и краткость программ, но порой вам может не подойти их философия «на все случаи жизни», поскольку вам в своей программе нужно управлять свойствами отображений более тонко. При необходимости Scala обеспечивает точное управление, поскольку отображения в нем не являются синтаксисом языка. Это библиотечные абстракции, которые можно расширять и приспосабливать под свои нужды.

В показанной ранее программе вы получите исходную реализацию отображения Map, но ее можно будет без особого труда изменить. К примеру, можно указать конкретную реализацию, такую как HashMap или TreeMap, или вызвать метод par для получения отображения ParMap, операции в котором выполняются параллельно. Можно указать для отображения значение по умолчанию или переопределить любой другой метод созданного вами отображения. Во всех случаях для отображений вполне пригоден такой же простой синтаксис доступа, как и в показанном примере.

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

Растут новые типы

Эрик Рэймонд (Eric Raymond) в качестве двух метафор разработки программных продуктов ввел собор и базар3. Под собором понимается почти идеальная разработка, создание которой требует много времени. После сборки она долго остается неизменной. Разработчики же базара, напротив, что-то адаптируют и дополняют каждый день. В книге Рэймонда базар — метафора, описывающая разработку ПО с открытым кодом. Гай Стил (Guy Steele) отметил в докладе о «растущем языке», что аналогичное различие можно применить к структуре языка программирования4. Scala больше похож на базар, чем на собор, в том смысле, что спроектирован с расчетом на расширение и адаптацию его теми, кто на нем программирует. Вместо того чтобы предоставлять все конструкции, которые только могут пригодиться в одном всеобъемлющем языке, Scala дает вам инструменты для создания таких конструкций.

Рассмотрим пример. Многие приложения нуждаются в целочисленном типе, который при выполнении арифметических операций может становиться произвольно большим без переполнения или циклического перехода в начало. В Scala такой тип определяется в библиотеке класса scala.math.BigInt. Определение использующего этот тип метода, который вычисляет факториал переданного ему целочисленного значения, имеет следующий вид5:

def factorial(x: BigInt): BigInt =

  if (x == 0) 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)

    BigInteger.ONE

  else

    x.multiply(factorial(x.subtract(BigInteger.ONE)))

Тип BigInt — один из многих других числовых типов: больших десятичных чисел, комплексных и рациональных чисел, доверительных интервалов, полиномов, — и данный список можно продолжить. В некоторых языках программирования часть этих типов реализуется естественным образом. Например, в Lisp, Haskell и Python есть большие целые числа, в Fortran и Python — комплексные. Но любой язык, в котором пытаются одновременно реализовать все эти абстракции, разрастается до таких размеров, что становится неуправляемым. Более того, даже существуй подобный язык, нашлись бы приложения, требующие других числовых типов, которые все равно не были бы представлены. Следовательно, подход, при котором предпринимается попытка реализовать все в одном языке, не позволяет получить хорошую масштабируемость. Язык Scala, напротив, дает пользователям возможность наращивать и адаптировать его в нужных направлениях. Он делает это с помощью определения простых в использовании библиотек, которые производят впечатление средств, естественно реализованных в языке.

Растут новые управляющие конструкции

Предыдущий пример показывает, что Scala позволяет добавлять новые типы, использовать которые так же удобно, как и встроенные. Тот же принцип расширения применяется и к управляющим конструкциям. Этот вид расширения иллюстрируется с помощью Akka — API Scala для многопоточного программирования, основанного на использовании акторов.

Поскольку распространение многоядерных процессоров в ближайшие годы продолжится, то для достижения приемлемой производительности все чаще может требоваться более высокая степень распараллеливания ваших приложений. Зачастую это чревато переписыванием кода с целью распределить вычисления по нескольким параллельным потокам. К сожалению, на практике создание надежных многопоточных приложений — весьма непростая задача. Модель потоковой обработки в Java выстроена вокруг совместно используемой памяти и блокировок и часто трудно поддается осмыслению, особенно по мере масштабирования систем и увеличения их объема и сложности. Обеспечить отсутствие состояния гонок или скрытых взаимных блокировок, не выявляемых в ходе тестирования, но способных проявиться в ходе эксплуатации, довольно трудно. Возможно, более безопасным альтернативным решением станет использование архитектуры передачи сообщений, подобной применению акторов в языке программирования Erlang.

Java поставляется с богатой библиотекой параллельных вычислений, основанной на применении потоков. Программы на Scala могут задействовать ее точно так же, как и любой другой API Java. Но есть еще Akka — дополнительная библиотека Scala, реализующая модель акторов, похожую на ту, что используется в Erlang.

Акторы — абстракции параллельных вычислений, которые могут быть реализованы в качестве надстроек над потоками. Они обмениваются данными, отправляя друг другу сообщения. Актор может выполнить две основные операции: отправку сообщения и его получение. Операция отправки, обозначаемая восклицательным знаком (!), отправляет сообщение актору. Пример, в котором фигурирует актор с именем recipient, выглядит следующим образом:

recipient ! msg

Отправка осуществляется в асинхронном режиме, то есть отправляющий сообщение актор может продолжить выполнение кода, не дожидаясь получения и обработки сообщения. У каждого актора имеется почтовый ящик, в котором входящие сообщения выстраиваются в очередь. Актор обрабатывает сообщения, поступающие в его почтовый ящик, в блоке получения receive:

def receive = {

  case Msg1 => ... // обработка Msg1

  case Msg2 => ... // обработка Msg2

  // ...

}

Блок receive состоит из нескольких инструкций выбора case, в каждой из которых содержится запрос к почтовому ящику на соответствие шаблону сообщения. В ящике выбирается первое же сообщение, которое соответствует условию какой-либо инструкции выбора, и в отношении него выполняется какое-либо действие. Когда в почтовом ящике не оказывается сообщений, актор приостанавливает работу и ожидает их дальнейшего поступления.

В качестве примера рассмотрим простой Akka-актор, реализующий сервис подсчета контрольной суммы:

class ChecksumActor extends Actor {

  var sum = 0

  def receive = {

    case Data(byte) => sum += byte

    case GetChecksum(requester) =>

      val checksum = ~(sum & 0xFF) + 1

      requester ! checksum

  }

}

Сначала в акторе определяется локальная переменная sum с начальным нулевым значением. Затем — блок получения receive, который будет обрабатывать сообщения. При получении сообщения Data он прибавляет значение содержащегося в нем аргумента byte к значению переменной sum. При получении сообщения GetChecksum он вычисляет контрольную сумму из текущего значения переменной sum и отправляет результат обратно в адрес пославшего запрос requester, используя код отправки сообщения requester!checksum. Поле requester отправляется в сообщении GetChecksum, обычно оно ссылается на актор, пославший запрос.

Мы не ожидаем, что прямо сейчас вы полностью поймете пример использования актора. Скорее, существенным обстоятельством для темы масштабируемости в этом примере служит тот факт, что ни блок receive, ни инструкция отправки сообщения (!) не являются встроенными операциями Scala. Даже притом, что внешний вид и работа блока receive во многом напоминают встроенную управляющую конструкцию, на самом деле это метод, определенный в библиотеке акторов Akka. Аналогично даже притом, что оператор ! похож на встроенный оператор, он также является всего лишь методом, определенным в библиотеке акторов Akka. Обе конструкции абсолютно независимы от языка программирования Scala.

Синтаксис блока receive и оператора отправки (!) выглядят в Scala во многом так же, как в Erlang, но в последнем эти конструкции встроены в язык. Кроме того, в Akka реализуется большинство других конструкций многопоточного программирования, имеющихся в Erlang, например конструкции отслеживания сбойных акторов и истечения времени ожидания. В целом модель акторов показала себя весьма удачным средством для выражения многопоточных и распределенных вычислений. Несмотря на то что акторы должны быть определены в библиотеке, они могут рассматриваться как составная часть языка Scala.

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

1.2. Что делает Scala масштабируемым языком

На возможность масштабирования влияет множество факторов, от особенностей синтаксиса до структуры абстрактных компонентов. Но если бы потребовалось назвать всего один аспект Scala, который способствует масштабируемости, то мы бы выбрали присущее этому языку сочетание объектно-ориентированного и функционального программирования (мы немного слукавили, на самом деле это два аспекта, но они взаимосвязаны).

Scala в объединении объектно-ориентированного и функционального программирования в однородную структуру языка пошел дальше всех остальных широко известных языков. Например, там, где в других языках объекты и функции — два разных понятия, в Scala функция по смыслу является объектом. Функциональные типы являются классами, которые могут наследоваться подклассами. Эти особенности могут показаться не более чем теоретическими, но имеют весьма серьезные последствия для возможностей масштабирования. Фактически ранее упомянутое понятие актора не может быть реализовано без этой унификации функций и объектов. В данном разделе дается обзор примененных в Scala способов смешивания объектно-ориентированной и функциональной концепций.

Scala — объектно-ориентированный язык

Развитие объектно-ориентированного программирования шло весьма успешно. Появившись в языке Simula в середине 1960-х годов и в Smalltalk в 1970-х, оно теперь доступно в подавляющем большинстве языков. В некоторых областях все полностью захвачено объектами. Точного определения «объектной ориентированности» нет, однако объекты явно чем-то привлекают программистов.

В принципе, мотивация для применения объектно-ориентированного программирования очень проста: все, за исключением самых простых программ, нуждается в определенной структуре. Наиболее понятный путь достижения желаемого результата заключается в помещении данных и операций в своеобразные контейнеры. Основной замысел объектно-ориентированного программирования состоит в придании этим контейнерам полной универсальности, чтобы в них могли содержаться не только операции, но и данные и чтобы сами они также были элементами, которые могли бы храниться в других контейнерах или передаваться операциям в качестве параметров. Подобные контейнеры называются объектами. Алан Кей (Alan Kay), изобретатель языка Smalltalk, заметил, что таким образом простейший объект имеет принцип построения, аналогичный полноценному компьютеру: под формализованным интерфейсом данные в нем сочетаются с операциями7. То есть объекты имеют непосредственное отношение к масштабируемости языка: одни и те же технологии применяются к построению как малых, так и больших программ.

Хотя долгое время объектно-ориентированное программирование преобладало, немногие языки стали последователями Smalltalk по части внедрения этого принципа построения в свое логическое решение. Например, множество языков допускают использование элементов, не являющихся объектами, — можно вспомнить имеющиеся в языке Java значения примитивных типов. Или же в них допускается применение статических полей и методов, не входящих в какой-либо объект. Эти отклонения от чистой идеи объектно-ориентированного программирования на первый взгляд выглядят вполне безобидными, но имеют досадную тенденцию к усложнению и ограничению масштабирования.

В отличие от этого, Scala — объектно-ориентированный язык в чистом виде: каждое значение является объектом и каждая операция — вызовом метода. Например, когда в Scala речь заходит о вычислении 1+2, фактически вызывается метод по имени +, который определен в классе Int. Можно определять методы с именами, похожими на операторы, а клиенты вашего API смогут с помощью этих методов записать операторы. Именно так API акторов Akka позволяет вам воспользоваться выражением requester!checksum, показанным в предыдущем примере: ! — метод класса Actor.

Когда речь заходит о составлении объектов, Scala проявляется как более совершенный язык по сравнению с большинством других. В качестве примера приведем имеющиеся в Scala трейты. Они подобны интерфейсам в Java, но могут содержать также реализации методов и даже поля8. Объекты создаются путем композиции примесей, при котором к членам класса добавляются члены нескольких трейтов. Таким образом, различные аспекты классов могут быть инкапсулированы в различных трейтах. Это выглядит как множественное наследование, но есть разница в конкретных деталях. В отличие от класса трейт может добавить в суперкласс новые функциональные возможности. Это придает трейтам более высокую степень подключаемости по сравнению с классами. В частности, благодаря этому удается избежать возникновения присущих множественному наследованию классических проблем «ромбовидного» наследования, которые возникают, когда один и тот же класс наследуется по нескольким различным путям.

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 станет неоднократно выполнять функцию до тех пор, пока утверждение не будет успешно подтверждено.

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

Вторая основная идея функционального программирования заключается в том, что операции программы должны преобразовать входные значения в выходные, а не изменять данные на месте. Чтобы понять разницу, рассмотрим реализацию строк в 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 позволяет повышать ценность уже существующего кода, то есть опираться на то, что у вас уже есть, поскольку он был разработан для достижения беспрепятственной совместимости с Java9. Программы на 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. Как такое возможно без нарушения совместимости? Ведь в Java-классе String нет метода toInt! По сути, в Scala имеется универсальное решение для устранения противоречия между сложной структурой библиотеки и совместимостью. Scala позволяет определять неявные преобразования, которые всегда применяются при несовпадении типов или выборе несуществующих элементов. В рассматриваемом случае при поиске метода toInt для работы со строковым значением компилятор Scala не найдет такого элемента в классе String. Однако он найдет неявное преобразование, превращающее Java-класс String в экземпляр Scala-класса StringOps, в котором такой элемент определен. Затем преобразование будет автоматически применено, прежде чем будет выполнена операция toInt.

Код Scala также может быть вызван из кода Java. Иногда при этом следует учитывать некоторые нюансы. Scala — более богатый язык, чем Java, и потому некоторые расширенные функции Scala должны быть закодированы, прежде чем могут быть отображены на код Java. Подробности объясняются в главе 31.

Scala — лаконичный язык

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

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

// Это Java

class MyClass {

 

    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) сказал, что тестирование позволяет убедиться лишь в наличии ошибок, но не в их отсутствии15. Гарантии, которые обеспечиваются статической типизацией, могут быть простыми, но это реальные гарантии, не способные обеспечить никакие объемы тестирования.

Безопасный рефакторинг. Системы статических типов дают гарантии, позволяющие вам вносить изменения в основной код, будучи совершенно уверенными в благополучном исходе этого действия. Рассмотрим, к примеру, рефакторинг, при котором к методу нужно добавить еще один параметр. В статически типизированном языке вы можете внести изменения, перекомпилировать систему и просто исправить те строки, которые вызовут ошибку типа. Сделав это, вы будете пребывать в уверенности, что были найдены все места, требовавшие изменений. То же самое справедливо для другого простого рефакторинга, например изменения имени метода или перемещения метода из одного класса в другой. Во всех случаях проверка статического типа позволяет быть вполне уверенными в том, что работоспособность новой системы осталась на уровне работоспособности старой.

Документирование. Статические типы — документация программы, проверяемой компилятором на корректность. В отличие от обычного комментария, аннотация типа никогда не станет устаревшей (по крайней мере, если содержащий ее исходный файл недавно успешно прошел компиляцию). Более того, компиляторы и интегрированные среды разработки (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, как, собственно, и синтаксис классов, создание пакетов и импорт16. Кроме синтаксиса, 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 года The Next 700 Programming Languages («Следующие 700 языков программирования»)17. (Описываемый в ней язык Iswim, наряду с Lisp, — один из первых в своем роде функциональных языков.) Происхождение конкретной идеи трактовки инфиксного оператора в качестве функции можно проследить до 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 применяются также некоторые нововведения в области языков программирования. Например, его абстрактные типы — более объектно-ориентированная альтернатива обобщенным типам, его трейты позволяют выполнять гибкую сборку компонентов, а экстракторы обеспечивают независимый от представления способ сопоставления с образцом. Эти нововведения были представлены в статьях на конференциях по языкам программирования в последние годы18.

Резюме

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

В следующей главе вы приступите к написанию кода Scala.

1 Scala произносится как «ска'ла».

2 Пожалуйста, не сердитесь на нас, если не сможете разобраться со всеми тонкостями этой программы. Объяснения будут даны в двух следующих главах.

3 Raymond E. The Cathedral & the Bazaar: Musings on Linux and Open Source by an Accidental Revolutionary. — O’Reilly, 1999.

4 Steele Jr., Guy L. Growing a Language // Higher-Order and Symbolic Computation, 12:221–223, 1999. Транскрипция речи, произнесенной на OOPSLA в 1998 году.

5 factorial(x), или x! в математической записи, — результат вычисления 1 × 2 … x, где для 0! определено значение 1.

6 Scala поставляется со стандартной библиотекой, часть которой будет рассмотрена в книге. За дополнительной информацией можно обратиться к имеющейся в библио­теке документации Scaladoc, доступной в дистрибутиве и в Интернете по адресу www.scala-lang.org.

7 Kay A.C. The Early History of Smalltalk // History of programming languages. — II, P. 511–598. — New York: ACM, 1996 [Электронный ресурс]. — Режим доступа: http://doi.acm.org/10.1145/234286.1057828.

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

9 Изначально существовала реализация Scala, запускаемая на платформе .NET, но она больше не используется. В последнее время все большую популярность набирает реализация Scala под названием Scala.js, запускаемая на JavaScript.

10 Единственное отличие заключается в том, что переменные экземпляра, полученные в случае применения Scala, будут финальными (final). Как сделать их не финальными, рассказывается в разделе 10.6.

11 Функциональный литерал может называться предикатом, если результирующим типом будет Boolean.

12 Такое использование символа подчеркивания в качестве заместителя для аргументов рассматривается в разделе 8.5.

13 Обобщенные типы рассматриваются в главе 19, пересечения (например, A с B и с C) — в главе 12, а абстрактные типы — в главе 20.

14 Kay A.C. Письмо, адресованное Стефану Раму, с описанием термина «Объектно-ориентированное программирование». Июль 2003 [Электронный ресурс]. — Режим доступа: www.purl.org/stefan_ram/pub/doc_kay_oop_en.

15 Dijkstra E.W. Notes on Structured Programming. — Апрель 1970 [Электронный ресурс]. — Режим доступа: www.cs.utexas.edu/users/EWD/ewd02xx/EWD249.PDF.

16 Главное отличие от Java касается синтаксиса для объявления типов: вместо «Тип переменная», как в Java, задействуется форма «переменная: Тип». Используемый в Scala постфиксный синтаксис типа похож на синтаксис, применяемый в Pascal, Modula-2 или Eiffel. Основная причина такого отклонения имеет отношение к логическому выводу типов, зачастую позволяющему опускать тип переменной или тип возвращаемого методом значения. Легче использовать синтаксис «переменная: Тип», поскольку двоеточие и тип можно просто не указывать. Но в стиле языка C, применяющем форму «Тип переменная», просто так не указывать тип нельзя, поскольку при этом исчезнет сам признак начала определения. Неуказанный тип в качестве заполнителя требует какое-нибудь ключевое слово (C# 3.0, в котором имеется логический вывод типов, для этой цели задействует ключевое слово var). Такое альтернативное ключевое слово представляется несколько более надуманным и менее привычным, чем подход, который используется в Scala.

17 Landin P.J. The Next 700 Programming Languages // Communications of the ACM, 1966. — № 9 (3). — P. 157–166.

18 Для получения большей информации см.: Odersky. M., Cremet V., Ro..ckl C., Zenger M. A Nominal Theory of Objects with Dependent Types // In Proc. ECOOP’03, Springer LNCS. 2003. July. — P. 201–225; Odersky M., Zenger M. Scalable Component Abstractions // Proceedings of OOPSLA. 2005. October. — P. 41–58; Emir B., Odersky M., Williams J. Matching Objects With Patterns // Proc. ECOOP, Springer LNCS. 2007. July. — P. 273–295.

Эрик Рэймонд (Eric Raymond) в качестве двух метафор разработки программных продуктов ввел собор и базар3. Под собором понимается почти идеальная разработка, создание которой требует много времени. После сборки она долго остается неизменной. Разработчики же базара, напротив, что-то адаптируют и дополняют каждый день. В книге Рэймонда базар — метафора, описывающая разработку ПО с открытым кодом. Гай Стил (Guy Steele) отметил в докладе о «растущем языке», что аналогичное различие можно применить к структуре языка программирования4. Scala больше похож на базар, чем на собор, в том смысле, что спроектирован с расчетом на расширение и адаптацию его теми, кто на нем программирует. Вместо того чтобы предоставлять все конструкции, которые только могут пригодиться в одном всеобъемлющем языке, Scala дает вам инструменты для создания таких конструкций.

12
13

Эта программа устанавливает отображение стран на их столицы, модифицирует отображение, добавляя новую конструкцию ("Japan"->"Tokyo"), и выводит название столицы, связанное со страной France2. В этом примере используется настолько высокоуровневая система записи, что она не загромождена ненужными точками с запятыми и сигнатурами типов. И действительно возникает ощущение использования современного языка скриптов наподобие Perl, Python или Ruby. Одна из общих характеристик этих языков, применимая к данному примеру, — поддержка всеми ими в синтаксисе языка конструкции ассоциативного отображения.

Тип BigInt похож на встроенный, поскольку со значениями этого типа можно использовать целочисленные литералы и операторы наподобие * и . Тем не менее это просто класс, определение которого задано в стандартной библиотеке Scala6. Если бы класса не было, то любой программист на Scala мог бы запросто написать его реализацию, например создав оболочку для имеющегося в языке Java класса java.math.BigInteger (фактически именно так и реализован класс BigInt в Scala).

Scala не требует резко отходить от платформы Java, чтобы опередить на шаг этот язык. Scala позволяет повышать ценность уже существующего кода, то есть опираться на то, что у вас уже есть, поскольку он был разработан для достижения беспрепятственной совместимости с Java9. Программы на Scala компилируются в байт-коды виртуальной машины Java (JVM). Производительность при выполнении этих кодов находится на одном уровне с производительностью программ на Java. Код Scala может вызывать методы Java, обращаться к полям этого языка, поддерживать наследование от его классов и реализовывать его интерфейсы. Для всего перечисленного не требуются ни специальный синтаксис, ни явные описания интерфейса, ни какой-либо связующий код. По сути, весь код Scala интенсивно использует библиотеки Java, зачастую даже без ведома программистов.

16

Эрик Рэймонд (Eric Raymond) в качестве двух метафор разработки программных продуктов ввел собор и базар3. Под собором понимается почти идеальная разработка, создание которой требует много времени. После сборки она долго остается неизменной. Разработчики же базара, напротив, что-то адаптируют и дополняют каждый день. В книге Рэймонда базар — метафора, описывающая разработку ПО с открытым кодом. Гай Стил (Guy Steele) отметил в докладе о «растущем языке», что аналогичное различие можно применить к структуре языка программирования4. Scala больше похож на базар, чем на собор, в том смысле, что спроектирован с расчетом на расширение и адаптацию его теми, кто на нем программирует. Вместо того чтобы предоставлять все конструкции, которые только могут пригодиться в одном всеобъемлющем языке, Scala дает вам инструменты для создания таких конструкций.

Рассмотрим пример. Многие приложения нуждаются в целочисленном типе, который при выполнении арифметических операций может становиться произвольно большим без переполнения или циклического перехода в начало. В Scala такой тип определяется в библиотеке класса scala.math.BigInt. Определение использующего этот тип метода, который вычисляет факториал переданного ему целочисленного значения, имеет следующий вид5:

10
15

Scala означает «масштабируемый язык» (от англ. scalable language). Это название он получил, поскольку был спроектирован так, чтобы расти вместе с запросами своих пользователей. Язык Scala может решать широкий круг задач программирования: от написания небольших скриптов до создания больших систем1.

Scala произносится как «ска'ла».

Raymond E. The Cathedral & the Bazaar: Musings on Linux and Open Source by an Accidental Revolutionary. — O’Reilly, 1999.

Пожалуйста, не сердитесь на нас, если не сможете разобраться со всеми тонкостями этой программы. Объяснения будут даны в двух следующих главах.

17

Изначально существовала реализация Scala, запускаемая на платформе .NET, но она больше не используется. В последнее время все большую популярность набирает реализация Scala под названием Scala.js, запускаемая на JavaScript.

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

Когда речь заходит о составлении объектов, Scala проявляется как более совершенный язык по сравнению с большинством других. В качестве примера приведем имеющиеся в Scala трейты. Они подобны интерфейсам в Java, но могут содержать также реализации методов и даже поля8. Объекты создаются путем композиции примесей, при котором к членам класса добавляются члены нескольких трейтов. Таким образом, различные аспекты классов могут быть инкапсулированы в различных трейтах. Это выглядит как множественное наследование, но есть разница в конкретных деталях. В отличие от класса трейт может добавить в суперкласс новые функциональные возможности. Это придает трейтам более высокую степень подключаемости по сравнению с классами. В частности, благодаря этому удается избежать возникновения присущих множественному наследованию классических проблем «ромбовидного» наследования, которые возникают, когда один и тот же класс наследуется по нескольким различным путям.

factorial(x), или x! в математической записи, — результат вычисления 1 × 2 … x, где для 0! определено значение 1.

Steele Jr., Guy L. Growing a Language // Higher-Order and Symbolic Computation, 12:221–223, 1999. Транскрипция речи, произнесенной на OOPSLA в 1998 году.

Kay A.C. The Early History of Smalltalk // History of programming languages. — II, P. 511–598. — New York: ACM, 1996 [Электронный ресурс]. — Режим доступа: http://doi.acm.org/10.1145/234286.1057828.

Scala поставляется со стандартной библиотекой, часть которой будет рассмотрена в книге. За дополнительной информацией можно обратиться к имеющейся в библио­теке документации Scaladoc, доступной в дистрибутиве и в Интернете по адресу www.scala-lang.org.

18

В принципе, мотивация для применения объектно-ориентированного программирования очень проста: все, за исключением самых простых программ, нуждается в определенной структуре. Наиболее понятный путь достижения желаемого результата заключается в помещении данных и операций в своеобразные контейнеры. Основной замысел объектно-ориентированного программирования состоит в придании этим контейнерам полной универсальности, чтобы в них могли содержаться не только операции, но и данные и чтобы сами они также были элементами, которые могли бы храниться в других контейнерах или передаваться операциям в качестве параметров. Подобные контейнеры называются объектами. Алан Кей (Alan Kay), изобретатель языка Smalltalk, заметил, что таким образом простейший объект имеет принцип построения, аналогичный полноценному компьютеру: под формализованным интерфейсом данные в нем сочетаются с операциями7. То есть объекты имеют непосредственное отношение к масштабируемости языка: одни и те же технологии применяются к построению как малых, так и больших программ.

Главное отличие от Java касается синтаксиса для объявления типов: вместо «Тип переменная», как в Java, задействуется форма «переменная: Тип». Используемый в Scala постфиксный синтаксис типа похож на синтаксис, применяемый в Pascal, Modula-2 или Eiffel. Основная причина такого отклонения имеет отношение к логическому выводу типов, зачастую позволяющему опускать тип переменной или тип возвращаемого методом значения. Легче использовать синтаксис «переменная: Тип», поскольку двоеточие и тип можно просто не указывать. Но в стиле языка C, применяющем форму «Тип переменная», просто так не указывать тип нельзя, поскольку при этом исчезнет сам признак начала определения. Неуказанный тип в качестве заполнителя требует какое-нибудь ключевое слово (C# 3.0, в котором имеется логический вывод типов, для этой цели задействует ключевое слово var). Такое альтернативное ключевое слово представляется несколько более надуманным и менее привычным, чем подход, который используется в Scala.

Dijkstra E.W. Notes on Structured Programming. — Апрель 1970 [Электронный ресурс]. — Режим доступа: www.cs.utexas.edu/users/EWD/ewd02xx/EWD249.PDF.

Для получения большей информации см.: Odersky. M., Cremet V., Ro..ckl C., Zenger M. A Nominal Theory of Objects with Dependent Types // In Proc. ECOOP’03, Springer LNCS. 2003. July. — P. 201–225; Odersky M., Zenger M. Scalable Component Abstractions // Proceedings of OOPSLA. 2005. October. — P. 41–58; Emir B., Odersky M., Williams J. Matching Objects With Patterns // Proc. ECOOP, Springer LNCS. 2007. July. — P. 273–295.

Landin P.J. The Next 700 Programming Languages // Communications of the ACM, 1966. — № 9 (3). — P. 157–166.

Единственное отличие заключается в том, что переменные экземпляра, полученные в случае применения Scala, будут финальными (final). Как сделать их не финальными, рассказывается в разделе 10.6.

Такое использование символа подчеркивания в качестве заместителя для аргументов рассматривается в разделе 8.5.

Функциональный литерал может называться предикатом, если результирующим типом будет Boolean.

Kay A.C. Письмо, адресованное Стефану Раму, с описанием термина «Объектно-ориентированное программирование». Июль 2003 [Электронный ресурс]. — Режим доступа: www.purl.org/stefan_ram/pub/doc_kay_oop_en.

Обобщенные типы рассматриваются в главе 19, пересечения (например, A с B и с C) — в главе 12, а абстрактные типы — в главе 20.

11
14

2. Первые шаги в Scala

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

Запуск представленных далее примеров возможен с помощью стандартной установки Scala. Получить ее можно по адресу www.scala-lang.org/downloads, следуя инструкциям для вашей платформы. Кроме того, вы можете воспользоваться дополнительным модулем Scala для Eclipse, IntelliJ или NetBeans. Применительно к шагам, рассматриваемым в данной главе, предполагаем, что вы обратились к ди­стрибутиву Scala из scala-lang.org.19

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

2.1. Шаг 1. Осваиваем интерпретатор Scala

Проще всего приступить к изучению Scala, задействуя Scala-интерпретатор — интер­активную оболочку для написания выражений и программ на этом языке. Интерпретатор, который называется scala, будет вычислять набираемые вами выражения и выводить на экран значение результата. Чтобы воспользоваться интерпретатором, нужно в приглашении командной строки набрать команду scala20:

$ scala

Welcome to Scala version 2.13.1

Type in expressions to have them evaluated.

Type :help for more information.

 

scala>

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

scala> 1 + 2

интерпретатор выведет на экран:

res0: Int = 3

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

автоматически сгенерированное или определенное пользователем имя для ссылки на вычисленное значение (res0, означающее результат 0);

• двоеточие (:), за которым следует тип выражения (Int);

• знак равенства (=);

• значение, полученное в результате вычисления выражения (3).

Тип Int означает класс Int в пакете scala. Пакеты в Scala аналогичны пакетам в Java — они разбивают глобальное пространство имен на части и предоставляют механизм для сокрытия данных21. Значения класса 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

res1: Int = 9

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

scala> println("Hello, world!")

Hello, world!

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

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

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

scala> val msg = "Hello, world!"

msg: String = Hello, world!

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

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

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

msg2: String = Hello again, world!

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

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

msg3: String = Hello yet again, world!

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

scala> println(msg)

Hello, world!

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

scala> msg = "Goodbye cruel world!"

           ^

       error: reassignment to val

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

scala> var greeting = "Hello, world!"

greeting: String = Hello, world!

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

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

mutated greeting

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

scala> val multiLine =

    |    "This is the next line."

multiLine: String = This is the next line.

Если поймете, что набрали не то, а интерпретатор все еще ждет ввода дополнительного текста, то можете сделать отмену, дважды нажав клавишу Enter:

scala> val oops =

    |

    |

You typed two blank lines. Starting a new command.

 

scala>

Далее в книге символы вертикальной черты отображаться не будут, чтобы код читался легче и его было проще скопировать и вставить в интерпретатор из электронной версии в формате PDF.

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

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

scala> def max(x: Int, y: Int): Int = {

         if (x > y) x

         else y

       }

max: (x: Int, y: Int)Int

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

 

Рис. 2.1. Основная форма определения функции в Scala

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

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

max: (x: Int, y: Int)Int

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

scala> max(3, 5)

res4: Int = 5

А вот определение функции, которая не получает параметры и не возвращает полезный результат:

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

greet: ()Unit

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

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

scala> :quit

$

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

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

println("Hello, world, from a script!")

а затем запустите файл на выполнение27:

$ scala hello.scala

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

Hello, world, from a script!

Аргументы командной строки, указанные для скрипта Scala, можно получить из Scala-массива по имени args. В Scala индексация элементов массива начинается с нуля и обращение к элементу выполняется указанием индекса в круглых скобках. Следовательно, первым элементом в Scala-массиве по имени steps будет steps(0), а не steps[0], как в Java. Убедиться в этом на практике можно, создав новый файл по имени helloarg.scala:

// Поприветствуйте содержимое первого аргумента

println("Hello, " + args(0) + "!")

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

$ scala helloarg.scala planet

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

Hello, planet!

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

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

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

var i = 0

while (i < args.length) {

  println(args(i))

  i += 1

}

Примечание

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

Этот скрипт начинается с определения переменой, vari=0. Вывод типов относит переменную i к типу scala.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 следующий код:

var i = 0

while (i < args.length) {

  if (i != 0)

    print(" ")

  print(args(i))

  i += 1

}

println()

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

$ scala echoargs.scala Scala is even more fun

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

Scala is even more fun

Обратите внимание: в Scala, как и в Java, булевы выражения для while или if нужно помещать в круглые скобки. (Иными словами, вы не можете в Scala прибегнуть к такому выражению, как ifi<10, вполне допустимому в языках, подобных Ruby. В Scala нужно воспользоваться записью if(i<10).) Еще один прием программирования, аналогичный применяемому в Java, заключается в том, что если в блоке if используется только одна инструкция, то при желании фигурные скобки можно не ставить, как показано в конструкции if в файле echoargs.scala. Но хотя их там нет, в Scala, как и в Java, для отделения инструкций друг от друга используются точки с запятыми (стоит уточнить: в Scala точки с запятыми зачастую необязательны, что снижает нагрузку на правый мизинец при наборе текста). Но если вы склонны к многословию, то можете записать содержимое файла echoargs.scala в следующем виде:

var i = 0;

while (i < args.length) {

  if (i != 0) {

    print(" ");

  }

  print(args(i));

  i += 1;

}

println();

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

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

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

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. Если вы предпочитаете конкретизировать, то можете упомянуть название типа. Но, пойдя по этому пути, придется часть кода, в которой указывается переменная аргумента, заключать в круглые скобки (это и есть обычный синтаксис):

args.foreach((arg: String) => println(arg))

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

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

args.foreach(println)

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

 

Рис. 2.2. Синтаксис функционального литерала в Scala

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

for (arg <- args)

  println(arg)

В круглых скобках после for содержится выражение arg<-args29. Справа от обозначения <- указан уже знакомый вам массив 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

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

Резюме

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

19 Приводимые в книге примеры тестировались с применением Scala версии 2.13.1.

20 Если вы используете Windows, то команду scala нужно набирать в окне командной строки.

21 Если вы не знакомы с пакетами Java, то их можно рассматривать как средство предоставления классам полных имен. Поскольку Int входит в пакет scala, Int — простое имя класса, а scala.Int — полное. Более подробно пакеты рассматриваются в главе 13.

22 Простым именем java.lang.String является String.

23 Но в интерпретаторе новую val-переменную можно определить с именем, которое до этого уже использовалось. Этот механизм рассматривается в разделе 7.7.

24 В Java тип возвращаемого из метода значения является возвращаемым типом. В Scala то же самое понятие называется результирующим типом.

25 Функция называется рекурсивной, если вызывает саму себя.

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

27 В Unix и Windows скрипты можно запускать, не набирая слово scala, а используя синтаксис «решетка — восклицательный знак», показанный в приложении.

28 Это сокращение, называемое частично примененной функцией, рассматривается в разделе 8.6.

29 Обозначение <– можно трактовать как «в». То есть код for (arg <– args) может быть прочитан как «для arg в args».

23
25
20
28
27
21

Приводимые в книге примеры тестировались с применением Scala версии 2.13.1.

Если вы используете Windows, то команду scala нужно набирать в окне командной строки.

Простым именем java.lang.String является String.

Если вы не знакомы с пакетами Java, то их можно рассматривать как средство предоставления классам полных имен. Поскольку Int входит в пакет scala, Int — простое имя класса, а scala.Int — полное. Более подробно пакеты рассматриваются в главе 13.

В Java тип возвращаемого из метода значения является возвращаемым типом. В Scala то же самое понятие называется результирующим типом.

Но в интерпретаторе новую val-переменную можно определить с именем, которое до этого уже использовалось. Этот механизм рассматривается в разделе 7.7.

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

Функция называется рекурсивной, если вызывает саму себя.

Это сокращение, называемое частично примененной функцией, рассматривается в разделе 8.6.

В Unix и Windows скрипты можно запускать, не набирая слово scala, а используя синтаксис «решетка — восклицательный знак», показанный в приложении.

Обозначение <– можно трактовать как «в». То есть код for (arg <– args) может быть прочитан как «для arg в args».

24
19
22
26
29

3. Дальнейшие шаги в Scala

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

3.1. Шаг 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)

  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)

  print(greetStrings(i))

В первой строке кода для этого выражения for показано еще одно общее правило Scala: если метод получает лишь один параметр, то его можно вызвать без точки или круглых скобок. В данном примере to на самом деле является методом, получающим один Int-аргумент. Код 0to2 преобразуется в вызов метода (0).to(2)30. Следует заметить, что этот синтаксис работает только при явном указании получателя вызова метода. Код println10 использовать нельзя, а код Consoleprintln10 — можно.

С технической точки зрения в Scala нет перегрузки операторов, поскольку в нем фактически отсутствуют операторы в традиционном понимании. Вместо этого такие символы, как +, -, *, /, могут использоваться в качестве имен методов. Следовательно, когда при выполнении шага 1 вы набираете в интерпретаторе Scala код 1+2, в действительности вы вызываете метод по имени + в отношении Int-объекта 1, передавая ему в качестве параметра значение 2. Как показано на рис. 3.1, вместо этого 1+2 можно записать с помощью традиционного синтаксиса вызова метода: (1).+(2).

 

Рис. 3.1. Все операции в Scala являются вызовами методов

Еще одна весьма важная идея, проиллюстрированная в этом примере, поможет понять, почему доступ к элементам массивов Scala осуществляется с помощью круглых скобок. В Scala меньше особых случаев по сравнению с Java. Массивы в Scala, как и в случае с любыми другими классами, — просто экземпляры классов. При использовании круглых скобок, окружающих одно или несколько значений переменной, Scala преобразует код в вызов метода по имени apply применительно к данной переменной. Следовательно, код greetStrings(i) преобразуется в код greetStrings.apply(i). Получается, что элемент массива в Scala является просто вызовом обычного метода, ничем не отличающегося от любого своего собрата. Этот принцип не ограничивается массивами: любое использование объекта в отношении каких-либо аргументов в круглых скобках будет преобразовано в вызов метода apply. Разумеется, данный код будет скомпилирован, только если в этом типе объекта определен метод apply. То есть это не особый случай, а общее правило.

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

greetStrings(0) = "Hello"

будет преобразован в код:

greetStrings.update(0, "Hello")

Таким образом, следующий код семантически эквивалентен коду листинга 3.1:

val greetStrings = new Array[String](3)

 

greetStrings.update(0, "Hello")

greetStrings.update(1, ", ")

greetStrings.update(2, "world!\n")

 

for (i <- 0.to(2))

  print(greetStrings.apply(i))

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

Рассмотренные до сих пор в этом шаге примеры компилируются и выполняются весьма неплохо, однако в Scala имеется более лаконичный способ создания и инициализации массивов, который, как правило, вы и будете использовать (см. листинг 3.2). Данный код создает новый массив длиной три элемента, инициализируемый переданными строками "zero", "one" и "two". Компилятор выводит тип массива как Array[String], поскольку ему передаются строки.

Листинг 3.2. Создание и инициализация массива

val numNames = Array("zero", "one", "two")

Фактически в листинге 3.2 вызывается фабричный метод по имени apply, создающий и возвращающий новый массив. Метод apply получает переменное количество аргументов31 и определяется в объекте-компаньонеArray. Подробнее объекты-компаньоны будут рассматриваться в разделе 4.3. Если вам приходилось программировать на Java, то можете воспринимать это как вызов статического метода по имени apply в отношении класса Array. Менее лаконичный способ вызова того же метода apply выглядит следующим образом:

val numNames2 = Array.apply("zero", "one", "two")

3.2. Шаг 8. Используем списки

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

Как вы уже видели, массив Scala — неизменяемая последовательность объектов с общим типом. Тип Array[String], к примеру, содержит только строки. Изменить длину массива после создания его экземпляра невозможно, но вы можете изменять значения его элементов. Таким образом, массивы относятся к изменяемым объектам.

Для неизменяемой последовательности объектов с общим типом можно воспользоваться списком, определяемым Scala-классом List. Как и в случае применения массивов, в типе List[String] содержатся только строки. Список Scala, scala.List, отличается от Java-типа java.util.List тем, что списки Scala всегда неизменямые, а списки Java могут изменяться. В более общем смысле список Scala разработан с прицелом на использование функционального стиля программирования. Список создается очень просто, и листинг 3.3 как раз показывает это.

Листинг 3.3. Создание и инициализация списка

val oneTwoThree = List(1, 2, 3)

Код в листинге 3.3 создает новую val-переменную по имени oneTwoThree, инициализируемую новым списком List[Int] с целочисленными элементами 1, 2 и 332. Из-за своей неизменяемости списки ведут себя подобно строкам в Java: при вызове метода в отношении списка из-за имени данного метода может создаваться впечатление, что обрабатываемый список будет изменен, но вместо этого создается и возвращается новый список с новым значением. Например, в List для объединения списков имеется метод, обозначаемый как :::. Используется он следующим образом:

val oneTwo = List(1, 2)

val threeFour = List(3, 4)

val oneTwoThreeFour = oneTwo ::: threeFour

println(oneTwo + " и " + threeFour + " не изменились.")

println("Следовательно, " + oneTwoThreeFour + " является новым списком.")

Запустив этот скрипт на выполнение, вы увидите следующую картину:

List(1, 2) и List(3, 4) не изменились.

Следовательно, List(1, 2, 3, 4) является новым списком.

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

val twoThree = List(2, 3)

val oneTwoThree = 1 :: twoThree

println(oneTwoThree)

то вы увидите:

List(1, 2, 3)

Примечание

В выражении 1 :: twoThree метод :: является методом его правого операнда — списка twoThree. Можно заподозрить, будто с ассоциативностью метода :: что-то не то, но есть простое мнемоническое правило: если метод используется в виде оператора, например a * b, то вызывается в отношении левого операнда, как в выражении a.*(b), если только имя метода не заканчивается двоеточием. А если оно заканчивается двоеточием, то метод вызывается в отношении правого операнда. Поэтому в выражении 1 :: twoThree метод :: вызывается в отношении twoThree с передачей ему 1, то есть: twoThree.::(1). Ассоциативность операторов более подробно будет рассматриваться в разделе 5.9.

Исходя из того, что короче всего указать пустой список с помощью Nil, один из способов инициализировать новые списки — связать элементы с помощью cons-оператора с Nil в качестве последнего элемента33. Например, следующий скрипт создаст ту же картину на выходе, что и предыдущий List(1,2,3):

val oneTwoThree = 1 :: 2 :: 3 :: Nil

println(oneTwoThree)

Имеющийся в Scala класс List укомплектован весьма полезными методами, многие из которых показаны в табл. 3.1. Вся эффективность списков будет раскрыта в главе 16.

Почему со списками не следует использовать операцию добавления в конец (append)?

В классе List есть операция добавления, которая записывается как :+ (о ней говорится в главе 24), но используется очень редко. Так происходит потому, что время, которое она тратит на добавление к списку, растет линейно с размером списка, а на добавление в начало списка с помощью метода :: всегда затрачивается одно и то же время. Если нужно добиться эффективности при создании списка с помощью добавления элементов, то можно добавлять их в начало, а завершив добавление, вызвать метод реверсирования reverse. Или же можно воспользоваться изменяемым списком ListBuffer, предлагающим операцию добавления, а затем, завершив добавление, вызвать метод toList и преобразовать его в обычный список. Список типа ListBuffer будет рассмотрен в разделе 22.2.

Таблица 3.1. Некоторые методы класса List и их использование

Что используется

Что этот метод делает

List() или Nil

Создает пустой список List

List("Cool", "tools", "rule")

Создает новый список типа List[String] с тремя значениями: "Cool", "tools" и "rule"

val thrill = "Will" :: "fill" :: "until" :: Nil

Создает новый список типа List[String] с тремя значениями: "Will", "fill" и "until"

List("a", "b") :::

List("c", "d")

Объединяет два списка (возвращает новый список типа List[String] со значениями "a", "b", "c" и "d")

thrill(2)

Возвращает элемент с индексом 2 (при начале отсчета с нуля) списка thrill (возвращает "until")

thrill.count(s =>

s.length == 4)

Подсчитывает количество строковых элементов в thrill, имеющих длину 4 (возвращает 2)

thrill.drop(2)

Возвращает список thrill без его первых двух элементов (возвращает List("until"))

thrill.dropRight(2)

Возвращает список thrill без крайних справа двух элементов (возвращает List("Will"))

thrill.exists(s =>

s == "until")

Определяет наличие в списке thrill строкового элемента, имеющего значение "until" (возвращает true)

thrill.filter(s =>

s.length == 4)

Возвращает список всех элементов списка thrill, имеющих длину 4, соблюдая порядок их следования в списке (возвращает List("Will", "fill"))

thrill.forall(s => s.endsWith("l"))

Показывает, заканчиваются ли все элементы в списке thrill буквой "l" (возвращает true)

thrill.foreach(s => print(s))

Выполняет инструкцию print в отношении каждой строки в списке thrill (выводит "Willfilluntil")

thrill.foreach(print)

Делает то же самое, что и предыдущий код, но с использованием более лаконичной формы записи (также выводит "Willfilluntil")

thrill.head

Возвращает первый элемент в списке thrill (возвращает "Will")

thrill.init

Возвращает список всех элементов списка thrill, кроме последнего (возвращает List("Will", "fill"))

thrill.isEmpty

Показывает, не пуст ли список thrill (возвращает false)

thrill.last

Возвращает последний элемент в списке thrill (возвращает "until")

thrill.length

Возвращает количество элементов в списке thrill (возвращает 3)

thrill.map(s => s + "y")

Возвращает список, который получается в результате добавления "y" к каждому строковому элементу в списке thrill (возвращает List("Willy", "filly", "untily"))

thrill.mkString(", ")

Создает строку с элементами списка (возвращает "Will, fill, until")

thrill.filterNot(s => s.length == 4)

Возвращает список всех элементов в порядке их следования в списке thrill, за исключением имеющих длину 4 (возвращает List("until"))

thrill.reverse

Возвращает список, содержащий все элементы списка thrill, следующие в обратном порядке (возвращает List("until", "fill", "Will"))

thrill.sortWith((s, t) => s.charAt(0).toLower < t.charAt(0).toLower)

Возвращает список, содержащий все элементы списка thrill в алфавитном порядке с первым символом, преобразованным в символ нижнего регистра (возвращает List("fill", "until", "will"))

thrill.tail

Возвращает список thrill за исключением его первого элемента (возвращает List("fill", "until"))

3.3. Шаг 9. Используем кортежи

Еще один полезный объект-контейнер — кортеж. Как и списки, кортежи не могут быть изменены, но, в отличие от списков, могут содержать различные типы элементов. Список может быть типа List[Int] или List[String], а кортеж может содержать одновременно как целые числа, так и строки. Кортежи находят широкое применение, например, при возвращении из метода сразу нескольких объектов. Там, где на Java для хранения нескольких возвращаемых значений зачастую приходится создавать JavaBean-подобный класс, в Scala можно просто вернуть кортеж. Все делается просто: чтобы создать экземпляр нового кортежа, содержащего объекты, нужно лишь заключить объекты в круглые скобки, отделив их друг от друга запятыми. После создания экземпляра кортежа доступ к его элементам можно получить, используя точку, знак подчеркивания и индекс элемента, причем подсчет элементов начинается с единицы. Пример показан в листинге 3.4.

Листинг 3.4. Создание и использование кортежа

val pair = (99, "Luftballons")

println(pair._1)

println(pair._2)

В первой строке листинга 3.4 создается новый кортеж, содержащий в качестве первого элемента целочисленное значение 99, а в качестве второго — строку "Luftballons". Scala выводит тип кортежа в виде Tuple2[Int,String], а также присваивает этот тип паре переменных. Во второй строке выполняется доступ к полю _1, в результате чего получается первый элемент 99. Символ точки (.) во второй строке аналогичен той точке, которая используется для доступа к полю или вызова метода. В данном случае выполняется доступ к полю по имени _1. Если запустим этот скрипт на выполнение, то получим следующий результат:

99

Luftballons

Реальный тип кортежа зависит от количества содержащихся в нем элементов и от типов этих элементов. Следовательно, типом кортежа (99,"Luftballons") является Tuple2[Int,String]. А типом кортежа ('u','r',"the",1,4,"me") является Tuple6[Char,Char,String,Int,Int,String]34.

Обращение к элементам кортежа

Возникает вопрос: а почему к элементам кортежа нельзя обратиться точно так же, как к элементам списка, например pair(0)? Дело в том, что используемый в списках метод apply всегда возвращает один и тот же тип, а в кортеже все элементы могут быть разных типов: у _1 может быть один результирующий тип, у _2 — другой и т.д. Эти числа вида _N начинаются с единицы, а не с нуля, поскольку начало отсчета с единицы традиционно используется в языках со статически типизированными кортежами, например Haskell и ML.

3.4. Шаг 10. Используем множества и отображения

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

Например, в API Scala содержится основной трейт для множеств, где этот трейт аналогичен Java-интерфейсу. (Более подробно трейты рассматриваются в главе 12.) Затем Scala предоставляет два трейта-наследника: один для изменяемых, а второй для неизменяемых множеств.

На рис. 3.2 показано, что для всех трех трейтов используется одно и то же простое имя Set. Но их полные имена отличаются друг от друга, поскольку все трейты размещаются в разных пакетах. Классы для конкретных множеств в Scala API, например HashSet (см. рис. 3.2), являются расширениями либо изменяемого, либо неизменяемого трейта Set. (В то время как в Java вы реализуете интерфейсы, в Scala расширяете (иначе говоря, подмешиваете) трейты.) Следовательно, если нужно воспользоваться HashSet, то в зависимости от потребностей можно выбирать между его изменяемой и неизменяемой разновидностями. Способ создания множества по умолчанию показан в листинге 3.5.

Листинг 3.5. Создание, инициализация и использование неизменяемого множества

var jetSet = Set("Boeing", "Airbus")

jetSet += "Lear"

println(jetSet.contains("Cessna"))

 

Рис. 3.2. Иерархия классов для множеств Scala

В первой строке кода листинга 3.5 определяется новая var-переменная по имени jetSet, которая инициализируется неизменяемым множеством, содержащим две строки: "Boeing" и "Airbus". В этом примере показано, что в Scala множества можно создавать точно так же, как списки и массивы: путем вызова фабричного метода по имени apply в отношении объекта-компаньона Set. В листинге 3.5 метод apply вызывается в отношении объекта-компаньона для scala.collection.immutable.Set, возвращающего экземпляр исходного, неизменяемого класса Set. Компилятор Scala выводит тип переменной jetSet, определяя его как неизменяемый Set[String].

Чтобы добавить новый элемент в неизменяемое множество, в отношении последнего вызывается метод +, которому и передается этот элемент. Метод + создает и возвращает новое неизменяемое множество с добавленным элементом. Конкретный метод += предоставляется исключительно для изменяемых множеств.

В данном случае вторая строка кода, jetSet+="Lear", фактически является сокращенной формой записи следующего кода:

jetSet = jetSet + "Lear"

Следовательно, во второй строке кода листинга 3.5 var-переменной jetSet присваивается новое множество, содержащее "Boeing", "Airbus" и "Lear". И наконец, в последней строке выводятся данные о том, содержится ли во множестве строка "Cessna". (Как и ожидалось, выводится false.)

Если нужно изменяемое множество, то следует, как показано в листинге 3.6, воспользоваться инструкцией import.

Листинг 3.6. Создание, инициализация и использование изменяемого множества

import scala.collection.mutable

 

val movieSet = mutable.Set("Hitch", "Poltergeist")

movieSet += "Shrek"

println(movieSet)

В первой строке данного листинга выполняется импорт scala.collection.mu­table. Инструкция import позволяет использовать простое имя, например Set, вместо длинного полного имени. В результате при указании mutable.Set во второй строке компилятор знает, что имеется в виду scala.collection.mutable.Set. В этой строке movieSet инициализируется новым изменяемым множеством, содержащим строки "Hitch" и "Poltergeist". В следующей строке к изменяемому множеству добавляется "Shrek", для чего в отношении множества вызывается метод += с передачей ему строки "Shrek". Как уже упоминалось, += — метод, определенный для изменяемых множеств. При желании можете вместо кода movieSet+="Shrek" воспользоваться кодом movieSet.+=("Shrek")35.

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

import scala.collection.immutable.HashSet

 

val hashSet = HashSet("Tomatoes", "Chilies")

println(hashSet + "Coriander")

Еще одним полезным трейтом коллекций в Scala является отображение — Map. Как и для множеств, Scala предоставляет изменяемые и неизменяемые версии Map с применением иерархии классов. Как показано на рис. 3.3, иерархия классов для отображений во многом похожа на иерархию для множеств. В пакете scala.collection есть основной трейт Map и два трейта-наследника отображения Map: изменяемый вариант в scala.collection.mutable и неизменяемый в scala.collection.immutable.

 

Рис. 3.3. Иерархия классов для Scala-отображений

Реализации Map, например HashMap-реализации в иерархии классов, показанной на рис. 3.3, расширяются либо в изменяемый, либо в неизменяемый трейт. Отображения можно создавать и инициализировать, используя фабричные методы, подобные тем, что применялись для массивов, списков и множеств.

Например, в листинге 3.7 показана работа с изменяемым отображением.

Листинг 3.7. Создание, инициализация и использование изменяемого отображения

import scala.collection.mutable

 

val treasureMap = mutable.Map[Int, String]()

treasureMap += (1 -> "Go to island.")

treasureMap += (2 -> "Find big X on ground.")

treasureMap += (3 -> "Dig.")

println(treasureMap(2))

В первой строке импортируется изменяемое отображение. Затем определяется val-переменная treasureMap, которая инициализируется пустым изменяемым отображением, имеющим целочисленные ключи и строковые значения. Отображение задается пустым, поскольку фабричному методу ничего не передается (в круглых скобках в Map[Int,String]() ничего не указано)36. В следующих трех строках к отображению добавляются пары «ключ — значение», для чего используются методы -> и +=. Как уже было показано, компилятор Scala преобразует выражения бинарных операций вида 1->"Gotoisland." в код (1).->("Gotoisland."). Следовательно, когда указывается 1->"Gotoisland.", фактически в отношении объекта 1 вызывается метод по имени ->, которому передается строка со значением "Gotoisland.". Метод ->, который можно вызвать в отношении любого объекта в программе Scala, возвращает двухэлементный кортеж, содержащий ключ и значение37. Затем этот кортеж передается методу += объекта отображения, на который ссылается treasureMap. И наконец, в последней строке выводится значение, соответствующее в treasureMap ключу 2.

Если запустить этот код, то он выведет следующие данные:

Find big X on ground.

Если отдать предпочтение неизменяемому отображению, то ничего импортировать не нужно, поскольку это отображение используется по умолчанию. Пример показан в листинге 3.8.

Листинг 3.8. Создание, инициализация и использование неизменяемого отображения

val romanNumeral = Map(

1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V"

)

println(romanNumeral(4))

Учитывая отсутствие импортирования, при указании Map в первой строке данного листинга вы получаете используемый по умолчанию экземпляр класса scala.collection.immutable.Map. Фабричному методу отображения передаются пять кортежей «ключ — значение», а он возвращает неизменяемое Map-отображение, содержащее эти переданные пары. Если запустить код, показанный в листинге 3.8, то он выведет IV.

3.5. Шаг 11. Учимся распознавать функциональный стиль

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

Сначала нужно усвоить разницу между двумя стилями, отражающуюся в коде. Один верный признак заключается в том, что если код содержит var-переменные, то он, вероятнее всего, написан в императивном стиле. Если он вообще не содержит var-переменных, то есть включает толькоval-переменные, то, вероятнее всего, он написан в функциональном стиле. Следовательно, один из способов приблизиться к последнему — попытаться обойтись в программах без var-переменных.

Обладая багажом императивности, то есть опытом работы с такими языками, как Java, C++ или C#, var-переменные можно рассматривать в качестве обычных, а val-переменные — в качестве переменных особого вида. В то же время, если у вас имеется опыт работы в функциональном стиле на таких языках, как Haskell, OCaml или Erlang, val-переменные можно представлять как обычные, а var-переменные — как некое кощунственное обращение с кодом. Но с точки зрения Scala val- и var-переменные — всего лишь два разных инструмента в вашем арсенале средств и оба одинаково полезны и не отвергаемы. Scala побуждает вас к использованию val-переменных, но, по сути, дает возможность применять тот инструмент, который лучше подходит для решаемой задачи. И тем не менее, даже будучи согласными с подобной философией, вы поначалу можете испытывать трудности, связанные с избавлением от var-переменных в коде.

Рассмотрим позаимствованный из главы 2 пример цикла while, в котором используется var-переменная, означающая, что он выполнен в императивном стиле:

def printArgs(args: Array[String]): Unit = {

  var i = 0

  while (i < args.length) {

    println(args(i))

    i += 1

  }

}

Вы можете преобразовать этот код — придать ему более функциональный стиль, отказавшись от использования var-переменной, например, так:

def printArgs(args: Array[String]): Unit = {

  for (arg <- args)

    println(arg)

}

или вот так:

def printArgs(args: Array[String]): Unit = {

  args.foreach(println)

}

В этом примере демонстрируется одно из преимуществ программирования с меньшим количеством var-переменных. Код после рефакторинга (более функциональный) выглядит понятнее, он более лаконичен, и в нем труднее допустить какие-либо ошибки, чем в исходном (более императивном) коде. Причина навязывания в Scala функционального стиля заключается в том, что он помогает создавать более понятный код, при написании которого труднее ошибиться.

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

Листинг 3.9. Функция без побочных эффектов или var-переменных

def formatArgs(args: Array[String]) = args.mkString("\n")

Теперь вы действительно перешли на функциональный стиль: нет ни побочных эффектов, ни var-переменных. Метод mkString, который можно вызвать в отношении любой коллекции, допускающей последовательный перебор элементов (включая массивы, списки, множества и отображения), возвращает строку, состоящую из результата вызова метода toString в отношении каждого элемента, с разделителями из переданной строки. Таким образом, если args содержит три элемента, "zero", "one" и "two", то метод formatArgs возвращает "zero\none\ntwo". Разумеется, эта функция, в отличие от методов printArgs, ничего не выводит, но в целях выполнения данной работы ее результаты можно легко передать функции println:

println(formatArgs(args))

Каждая полезная программа, вероятнее всего, будет иметь какие-либо побочные эффекты. Отдавая предпочтение методам без побочных эффектов, вы будете стремиться к разработке программ, в которых такие эффекты сведены к минимуму. Одним из преимуществ такого подхода станет упрощение тестирования ваших программ.

Например, чтобы протестировать любой из трех показанных ранее в этом разделе методов printArgs, вам придется переопределить метод println, перехватить передаваемый ему вывод и убедиться в том, что он соответствует вашим ожиданиям. В отличие от этого функцию formatArgs можно протестировать, просто проверяя ее результат:

val res = formatArgs(Array("zero", "one", "two"))

assert(res == "zero\none\ntwo")

Имеющийся в Scala метод assert проверяет переданное ему булево выражение и, если последнее вычисляется в false, выдает ошибку AssertionError. Если же переданное булево выражение вычисляется в true, то метод просто молча возвращает управление вызвавшему его коду. Более подробно о тестах, проводимых с помощью assert, и тестировании речь пойдет в главе 14.

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

Сбалансированная позиция для программистов, работающих на Scala

Отдавайте предпочтение val-переменным, неизменяемым объектам и методам без побочных эффектов. Стремитесь применять их в первую очередь. Используйте var-переменные, изменяемые объекты и методы с побочными эффектами в случае необходимости и при наличии четкой обоснованности их применения.

3.6. Шаг 12. Читаем строки из файла

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

Листинг 3.10. Считывание строк из файла

import scala.io.Source

 

if (args.length > 0) {

 

  for (line <- Source.fromFile(args(0)).getLines())

    println(line.length.toString + " " + line)

}

else

  Console.err.println("Please enter filename")

Скрипт начинается с импорта класса Source из пакета scala.io. Затем он проверяет, указан ли в командной строке хотя бы один аргумент. Если указан, то первый аргумент рассматривается как имя открываемого и обрабатываемого файла. Выражение Source.fromFile(args(0)) пробует открыть указанный файл и возвращает объект типа Source, в отношении которого вызывается метод getLines. Этот метод возвращает значение типа Iterator[String], в котором при каждой итерации предоставляется по одной строке за исключением символа конца строки. Выражение for выполняет последовательный перебор этих строк и выводит длину каждой строки, затем пробел, а потом саму строку. Если аргументы в командной строке не указаны, то финальное условие else выведет сообщение в поток стандартного устройства вывода. Поместив данный код в файл countchars1.scala и запустив его с указанием самого этого файла:

$ scala countchars1.scala countchars1.scala

вы увидите следующий текст:

22 import scala.io.Source

0

22 if (args.length > 0) {

0

51   for (line <- Source.fromFile(args(0)).getLines())

37     println(line.length + " " + line)

1 }

4 else

46   Console.err.println("Please enter filename")

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

22 | import scala.io.Source

0 |

22 | if (args.length > 0) {

0 |

51 |   for (line <- Source.fromFile(args(0)).getLines())

37 |     println(line.length + " " + line)

1 | }

4 | else

46 |   Console.err.println("Please enter filename")

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

val lines = Source.fromFile(args(0)).getLines().toList

Завершающий выражение вызов метода toList нужен потому, что метод getLines возвращает итератор. По мере проведения итерации через итератор он истощается. Преобразование в список с помощью вызова toList дает возможность выполнять итерацию любое необходимое количество раз, не прибегая к многократному выделению памяти для хранения всех строк из файла. Получается, переменная lines ссылается на список строк с содержимым файла, указанного в командной строке. Вам нужно вычислять ширину позиции для количества символов в каждой строке дважды — по одному разу за каждую итерацию. Поэтому из данного выражения можно вывести небольшую функцию, которая подсчитывает ширину, необходимую для символов, отображающих длину переданной строки:

def widthOfLength(s: String) = s.length.toString.length

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

var maxWidth = 0

for (line <- lines)

  maxWidth = maxWidth.max(widthOfLength(line))

Здесь с помощью выражения for выполняется последовательный перебор всех строк, вычисляется ширина, необходимая для символов, показывающих длину строки. И если она больше текущего максимума, то ее значение присваивается var-переменной maxWidth, которой было присвоено начальное значение 0. (Метод max, который можно вызвать в отношении любого объекта типа Int, возвращает большее число, сравнивая значение, в отношении которого он был вызван, и переданное ему значение.) В качестве альтернативного варианта, если предпочтительнее искать максимум, не прибегая к использованию var-переменных, можно сначала определить самую длинную строку:

val longestLine = lines.reduceLeft(

  (a, b) => if (a.length > b.length) a else b

)

Метод reduceLeft применяет переданную ему функцию к первым двум элементам в списке lines, затем — к результату первого применения и к следующему элементу в lines и далее до конца списка. Результатом каждого применения будет самая длинная строка из встреченных до сих пор, поскольку переданная функция (a,b)=>if(a.length>b.length)aelseb возвращает самую длинную из двух переданных строк. Метод reduceLeft возвратит результат последнего применения функции, который в данном случае будет самым длинным строковым элементом списка lines.

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

val maxWidth = widthOfLength(longestLine)

Теперь останется только вывести строки с надлежащим форматированием. Это можно сделать следующим образом:

for (line <- lines) {

  val numSpaces = maxWidth - widthOfLength(line)

  val padding = " " * numSpaces

  println(padding + line.length + " | " + line)

}

В этом выражении for еще раз выполняет последовательный перебор элементов списка lines. Для каждой строки сначала вычисляется количество пробелов, устанавливаемых перед указанием длины строки, и присваивается переменной numSpaces с помощью выражения ""*numSpaces. И наконец, выводится информация с надлежащим форматированием. Весь скрипт показан в листинге 3.11.

Листинг 3.11. Вывод отформатированного количества символов каждой строки файла

import scala.io.Source

 

def widthOfLength(s: String) = s.length.toString.length

 

if (args.length > 0) {

 

  val lines = Source.fromFile(args(0)).getLines().toList

 

  val longestLine = lines.reduceLeft(

    (a, b) => if (a.length > b.length) a else b

  )

  val maxWidth = widthOfLength(longestLine)

 

  for (line <- lines) {

    val numSpaces = maxWidth - widthOfLength(line)

    val padding = " " * numSpaces

    println(padding + line.length + " | " + line)

  }

}

else

  Console.err.println("Пожалуйста, введите имя файла ")

Резюме

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

30 Этот метод to фактически возвращает не массив, а иную разновидность последовательности, содержащую значения 0, 1 и 2, последовательный перебор которых выполняется выражением for. Последовательности и другие коллекции будут рассматриваться в главе 17.

31 Списки аргументов переменной длины или повторяемые параметры рассматриваются в разделе 8.8.

32 Использовать запись new List не нужно, поскольку List.apply() определен в объекте-компаньоне scala.List как фабричный метод. Более подробно объекты-компаньоны рассматриваются в разделе 4.3.

33 Причина, по которой в конце списка нужен Nil, заключается в том, что метод :: определен в классе List. Если попытаться просто воспользоваться кодом 1 :: 2 :: 3, то он не пройдет компиляцию, поскольку 3 относится к типу Int, у которого нет метода ::.

34 Концептуально можно создавать кортежи любой длины, однако на данный момент библиотека Scala определяет их только до Tuple22.

35 Множество в листинге 3.6 изменяемое, поэтому повторно присваивать значение movieSet не нужно и данная переменная может относиться к val-переменным. В отличие от этого использование метода += с неизменяемым множеством в листинге 3.5 требует повторного присваивания значения переменной jetSet, поэтому она должна быть var-переменной.

36 Явная параметризация типа, "[Int, String]", требуется в листинге 3.7 из-за того, что без какого-либо значения, переданного фабричному методу, компилятор не в состоянии выполнить логический вывод типов параметров отображения. В отличие от этого компилятор может выполнить вывод типов параметров из значений, переданных фабричному методу map, показанному в листинге 3.8, поэтому явного указания типов параметров там не требуется.

37 Используемый в Scala механизм, позволяющий вызывать метод –> в отношении любого объекта, — неявное преобразование — будет рассмотрен в главе 21.

36
35
32

Явная параметризация типа, "[Int, String]", требуется в листинге 3.7 из-за того, что без какого-либо значения, переданного фабричному методу, компилятор не в состоянии выполнить логический вывод типов параметров отображения. В отличие от этого компилятор может выполнить вывод типов параметров из значений, переданных фабричному методу map, показанному в листинге 3.8, поэтому явного указания типов параметров там не требуется.

Множество в листинге 3.6 изменяемое, поэтому повторно присваивать значение movieSet не нужно и данная переменная может относиться к val-переменным. В отличие от этого использование метода += с неизменяемым множеством в листинге 3.5 требует повторного присваивания значения переменной jetSet, поэтому она должна быть var-переменной.

Используемый в Scala механизм, позволяющий вызывать метод –> в отношении любого объекта, — неявное преобразование — будет рассмотрен в главе 21.

Этот метод to фактически возвращает не массив, а иную разновидность последовательности, содержащую значения 0, 1 и 2, последовательный перебор которых выполняется выражением for. Последовательности и другие коллекции будут рассматриваться в главе 17.

Использовать запись new List не нужно, поскольку List.apply() определен в объекте-компаньоне scala.List как фабричный метод. Более подробно объекты-компаньоны рассматриваются в разделе 4.3.

Списки аргументов переменной длины или повторяемые параметры рассматриваются в разделе 8.8.

Концептуально можно создавать кортежи любой длины, однако на данный момент библиотека Scala определяет их только до Tuple22.

Причина, по которой в конце списка нужен Nil, заключается в том, что метод :: определен в классе List. Если попытаться просто воспользоваться кодом 1 :: 2 :: 3, то он не пройдет компиляцию, поскольку 3 относится к типу Int, у которого нет метода ::.

33
31
30
34
37

4. Классы и объекты

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

4.1. Классы, поля и методы

Классы — «чертежи» объектов. После определения класса из него, как по чертежу, можно создавать объекты, воспользовавшись для этого ключевым словом new. Например, при наличии следующего определения класса:

class ChecksumAccumulator {

  // Сюда помещается определение класса

}

с помощью кода:

new ChecksumAccumulator

можно создавать объекты ChecksumAccumulator.

Внутри определения класса помещаются поля и методы, которые в общем называются членами класса. Поля, которые определяются либо как val-, либо как var-переменные, являются переменными, относящимися к объектам. Методы, определяемые с помощью ключевого слова def, содержат исполняемый код. В полях хранятся состояние или данные объекта, а методы используют эти данные для выполнения в отношении объекта вычислений. При создании экземпляра класса среда выполнения приложения резервирует часть памяти для хранения образа состояния получа

...