Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
62 Глава 2 • Первые шаги в Scala из одного оператора, вы сможете целиком написать ее в одну строку. Таким образом, у вас появляется альтернативный вариант реализации функции max : def max(x: Int, y: Int) = if x > y then x else y После того как вы определили функцию, вы можете вызвать ее по имени, например: val bigger = max(3, 5) // 5 А вот определение функции, которая не принимает никаких параметров и не возвращает какоголибо интересного результата: scala> def greet() = println("Hello, world!") def greet(): Unit Когда определяется функция приветствия greet() , REPL откликается сле дующим приветствием: def greet (): Unit . Разумеется, слово greet — это имя функции. Пустота в скобках показывает, что функция не получает параметров. А Unit — результирующий тип функции greet . Он показывает, что функция не возвращает никакого интересного значения. Тип Unit в Scala подобен типу void в Java. Фактически каждый метод, возвращающий void в Java, отображается на метод, возвращающий Unit в Scala. Таким образом, методы с результирующим типом Unit выполняются только для того, чтобы проявились их побочные эффекты. В случае с greet() побочным эффектом будет дружеское приветствие, выведенное на стандартное устройство вывода. При выполнении следующего шага код Scala будет помещен в файл и запу щен в качестве скрипта. Если нужно выйти из REPL, то это можно сделать с помощью команды : quit : scala> :quit $ Шаг 4 . Пишем Scala-скрипты Несмотря на то что язык Scala разработан, чтобы помочь программистам создавать очень большие масштабируемые системы, он вполне может подой ти и для решения менее масштабных задач наподобие написания скриптов. Скрипт — это просто исходный файл Scala, который содержит функцию верхнего уровня, определяемую как @main . Поместите в файл по имени hello.scala следующий код: @main def m() = println("Hello, world, from a script!") Шаг 4 . Пишем Scala-скрипты 63 а затем запустите файл на выполнение: $ scala hello.scala И вы получите еще одно приветствие: Hello, world, from a script! В этом примере функция, отмеченная @main , называется m (от слова main), но это имя не имеет значения для выполнения скрипта. Чтобы скрипт сработал, вам необходимо запустить Scala и указать имя файла, содержащего функцию main , а не имя этой функции. Вы можете получить доступ к аргументам командной строки, переданным вашему скрипту, приняв их в качестве параметров вашей основной функ ции. Например, вы можете принять строковые аргументы, взяв параметр со специальной аннотацией типа String* , что означает от нуля до многих по вторяющихся параметров типа String 1 . Внутри основной функции параметр будет иметь тип Seq[String] , то есть последовательность строк. В Scala по следовательности начинаются с нуля, и чтобы получить доступ к элементу, необходимо указать его индекс в круглых скобках. Таким образом, первым элементом в последовательности Scala с именем steps будет steps(0) . Чтобы попробовать это, введите в новый файл с именем helloarg.scala следующее: @main def m(args: String*) = // Поприветствуйте содержимое первого аргумента println("Hello, " + args(0) + "!") а затем запустите его на выполнение: $ scala helloarg.scala planet В данной команде planet передается в качестве аргумента командной строки, доступного в скрипте при использовании выражения args(0) . Поэтому вы должны увидеть на экране следующий текст: Hello, planet! Обратите внимание на наличие комментария в скрипте. Компилятор Scala проигнорирует символы между парой символов // и концом строки, а также все символы между сочетаниями символов /* и */ . Вдобавок в этом примере показана конкатенация String значений, выполненная с помощью операто ра + . Весь код работает вполне предсказуемо. Выражение "Hello, " + "world!" будет вычислено в строку "Hello, world!" 1 Повторяющиеся параметры описаны в разделе 8.8. 64 Глава 2 • Первые шаги в Scala Шаг 5 . Организуем цикл с while и принимаем решение с if Чтобы попробовать в работе конструкцию while , наберите следующий код и сохраните его в файле printargs.scala : @main def m(args: String*) = var i = 0 while i < args.length do println(args(i)) i += 1 ПРИМЕЧАНИЕ Хотя примеры в данном разделе помогают объяснить суть циклов while, они не демонстрируют наилучший стиль программирования на Scala . В следу- ющем разделе будут показаны более рациональные подходы, позволяющие избежать повторения последовательностей с помощью индексов . Этот скрипт начинается с определения переменой, var i = 0 . Вывод типов относит переменную i к типу Int , поскольку это тип ее начального значе ния 0 . Конструкция while на следующей строке заставляет блок (две строки кода снизу) повторно выполняться, пока булево выражение i < args.length будет вычисляться в false . Метод args.length вычисляет длину после довательности args . Блок содержит две инструкции, каждая из которых набрана с отступом в два пробела, что является рекомендуемым стилем от ступов для кода на Scala. Первая инструкция, println(args(i)) , выводит на экран i й аргумент командной строки. Вторая, i += 1 , увеличивает значение переменной i на единицу. Обратите внимание: Javaкод ++i и i++ в Scala не работает. Чтобы в Scala увеличить значение переменной на единицу, нужно использовать одно из двух выражений: либо i = i + 1 , либо i += 1 . Запустите этот скрипт с помощью команды, показанной ниже: $ scala printargs.scala Scala is fun И вы увидите: Scala is fun Далее наберите в новом файле по имени echoargs.scala следующий код: @main def m(args: String*) = var i = 0 Шаг 5 . Организуем цикл с while и принимаем решение с if 65 while i < args.length do if i != 0 then print(" ") print(args(i)) i += 1 println() В целях вывода всех аргументов в одной и той же строке в этой версии вме сто вызова println используется вызов print . Чтобы эту строку можно было прочитать, перед каждым аргументом, за исключением первого, благодаря использованию конструкции if i != 0 then вставляется пробел. При первом проходе цикла while выражение i != 0 станет вычисляться в false , поэтому перед начальным элементом пробел выводиться не будет. В самом конце добавлена еще одна инструкция println , чтобы после вывода аргументов произошел переход на новую строку. Тогда у вас получится очень красивая картинка. Если запустить этот скрипт с помощью команды: $ scala echoargs.scala Scala is even more fun то вы увидите на экране такой текст: Scala is even more fun Обратите внимание, что в Scala, в отличие от Java, вам не нужно помещать логическое выражение while или if в круглые скобки. Еще одно отличие от Java состоит в том, что вы можете опустить фигурные скобки в блоке, даже если он содержит более одного оператора, при условии, что вы сделаете со ответствующий отступ для каждой строки. И хотя вы не видели ни одной точки с запятой, Scala использует их для разделения операторов, как и Java, за исключением того, что в Scala эти знаки очень часто являются необяза тельными, что дает некоторое облегчение вашему правому мизинцу. Если бы вы были более многословны, вы могли бы написать скрипт echoargs.scala в стиле Java следующим образом: @main def m(args: String*) = { var i = 0; while (i < args.length) { if (i != 0) { print(" "); } print(args(i)); i += 1; } println(); } 66 Глава 2 • Первые шаги в Scala Начиная со Scala 3, вместо фигурных скобок рекомендуется использовать стиль на основе отступов, называемый «тихим синтаксисом». В Scala 3 также были добавлены маркеры окончания кода, помогающие понять, где заканчиваются более крупные области с отступом. Маркеры окончания кода состоят из ключевого слова end и следующего за ним токена спецификатора, который является либо идентификатором, либо ключевым словом. Пример показан в листинге 10.9. Шаг 6 . Перебираем элементы с foreach и for-do Возможно, при написании циклов while на предыдущем шаге вы даже не осознавали того, что программирование велось в императивном стиле. Обыч но он применяется с такими языками, как Java, C++ и Python. При работе в этом стиле императивные команды в случае последовательного перебора элементов в цикле выдаются поочередно и зачастую изменяемое состояние совместно используется различными функциями. Scala позволяет програм мировать в императивном стиле, но, узнав этот язык получше, вы, скорее всего, перейдете преимущественно на функциональный стиль. По сути, одна из основных целей этой книги — помочь освоить работу в функциональном стиле, чтобы она стала такой же комфортной, как и работа в императивном. Одна из основных характеристик функционального языка — то, что его функции относятся к конструкциям первого класса, и это абсолютно спра ведливо для языка Scala. Например, еще один, гораздо более лаконичный вариант вывода каждого аргумента командной строки выглядит так: @main def m(args: String*) = args.foreach(arg => println(arg)) В этом коде в отношении массива args вызывается метод foreach , в который передается функция. В данном случае передается функциональный литерал с одним параметром arg . Тело функции — вызов println(arg) . Если набрать показанный ранее код в новом файле по имени pa.scala и запустить этот файл на выполнение с помощью команды: $ scala pa.scala Concise is nice то на экране появятся строки: Concise is nice Шаг 6 . Перебираем элементы с foreach и for-do 67 В предыдущем примере компилятор Scala вывел тип arg , причислив эту переменную к String , поскольку String — тип элемента последовательно сти, в отношении которого вызван метод foreach . Если вы предпочитаете конкретизировать, то можете упомянуть название типа. Но, пойдя по этому пути, придется часть кода, в которой указывается переменная аргумента, заключать в круглые скобки (это и есть обычный синтаксис): @main def m(args: String*) = args.foreach((arg: String) => println(arg)) При запуске этот скрипт ведет себя точно так же, как и предыдущий. Если же вы склонны не к конкретизации, а к более лаконичному изложению кода, то можете воспользоваться специальными сокращениями, принятыми в Scala. Если функциональный литерал функции состоит из одной инструк ции, принимающей один аргумент, то обозначать данный аргумент явным образом по имени не нужно 1 . Поэтому работать будет и следующий код: @main def m(args: String*) = args.foreach(println) Резюмируем усвоенное: синтаксис для функционального литерала пред ставляет собой список поименованных параметров, заключенный в круглые скобки, а также правую стрелку, за которой следует тело функции. Этот синтаксис показан на рис. 2.2. Рис. 2.2. Синтаксис функционального литерала в Scala Теперь вы можете поинтересоваться: что же случилось с теми проверенными циклами for , которые вы привыкли использовать в таких императивных язы ках, как Java или Python? Придерживаться функционального направления в Scala возможно с помощью только одного функционального родственни ка императивной конструкции for , который называется выражением for. 1 Это сокращение, которое называется частично применяемой функцией, описано в разделе 8.6. 68 Глава 2 • Первые шаги в Scala Поскольку вы не сможете понять всю его эффективность и выразитель ность, пока не доберетесь до раздела 7.3 (или не заглянете в него), здесь о нем будет дано лишь общее представление. Наберите в новом файле по имени forargs.scala следующий код: @main def m(args: String*) = for arg <- args do println(arg) Между for и do находится arg <- args 1 . Справа от символа <– расположена уже знакомая вам последовательность args . Слева от <– указана переменная arg , относящаяся к val , а не к var переменным (так как она всегда относится к val переменным, записывается только arg , а не val arg ). Может показаться, что arg относится к var переменной, поскольку она будет получать новое значение при каждой итерации, однако в действительности она относится к val переменной: arg не может получить новое значение внутри тела выра жения. Вместо этого для каждого элемента массива args будет создана новая val переменная по имени arg , которая будет инициализирована значением элемента, и тело for будет выполнено. Если скрипт forargs.scala запустить с помощью команды: $ scala forargs.scala for arg in args то вы увидите: for arg in args Диапазон применения выражения for значительно шире, но для начала это го примера достаточно. Мы расскажем вам больше о for в шаге 12 главы 3 и в разделе 7.3. Резюме В данной главе мы привели основную информацию о Scala. Надеемся, вы воспользовались возможностью создать код на этом языке. В следующей главе мы продолжим вводный обзор и рассмотрим более сложные темы. 1 Вы можете интерпретировать символ <– как in . Следовательно, выражение for arg <– args do можно прочитать как for arg in args do 3 Дальнейшие шаги в Scala В этой главе продолжается введение в Scala, начатое в предыдущей главе. Здесь мы рассмотрим более сложные функциональные возможности. Когда вы усвоите материал главы, у вас будет достаточно знаний, чтобы начать создавать полезные скрипты на Scala. Мы вновь рекомендуем по мере чтения текста получать практические навыки с помощью приводимых примеров. Лучше всего осваивать Scala, начиная создавать код на данном языке. Шаг 7 . Параметризуем массивы типами В Scala создавать объекты или экземпляры класса можно с помощью клю чевого слова new . При создании объекта в Scala вы можете параметризовать его значениями и типами. Параметризация означает «конфигурирование» экземпляра при его создании. Параметризация экземпляра значениями произ водится путем передачи конструктору объектов в круглых скобках. Например, код Scala, который показан ниже, создает новый объект java.math.BigInteger , выполняя его параметризацию значением "12345" : val big = new java.math.BigInteger("12345") Параметризация экземпляра типами выполняется с помощью указания одного или нескольких типов в квадратных скобках. Пример показан в ли стинге 3.1. Здесь greetStrings — значение типа Array[String] («массив строк»), инициализируемое длиной 3 путем его параметризации значе нием 3 в первой строке кода. Если запустить код в листинге 3.1 в качестве скрипта, то вы увидите еще одно приветствие Hello, world! . Учтите, что при параметризации экземпляра как типом, так и значением тип стоит 70 Глава 3 • Дальнейшие шаги в Scala первым и указывается в квадратных скобках, а за ним следует значение в круглых скобках. Листинг 3.1. Параметризация массива типом val greetStrings = new Array[String](3) greetStrings(0) = "Hello" greetStrings(1) = ", " greetStrings(2) = "world!\n" for i <- 0 to 2 do print(greetStrings(i)) ПРИМЕЧАНИЕ Хотя код в листинге 3 .1 содержит важные понятия, он не показывает рекомендуемый способ создания и инициализации массива в Scala . Более рациональный способ будет показан в листинге 3 .2 . Если вы склонны делать более явные указания, то тип greetStrings можно обозначить так: val greetStrings: Array[String] = new Array[String](3) С учетом имеющегося в Scala вывода типов эта строка кода семантически эквивалентна первой строке листинга 3.1. Но в данной форме показано сле дующее: часть параметризации, которая относится к типу (название типа в квадратных скобках), формирует часть типа экземпляра, однако часть параметризации, относящаяся к значению (значения в круглых скобках), в формировании не участвует. Типом greetStrings является Array[String] , а не Array[String](3) В следующих трех строках кода в листинге 3.1 инициализируется каждый элемент массива greetStrings : greetStrings(0) = "Hello" greetStrings(1) = ", " greetStrings(2) = "world!\n" Как уже упоминалось, доступ к массивам в Scala осуществляется за счет по мещения индекса элемента в круглые, а не в квадратные скобки, как в Java. Следовательно, нулевым элементом массива будет greetStrings(0) , а не greetStrings[0] Эти три строки кода иллюстрируют важное понятие, помогающее осмыс лить значение для Scala val переменных. Когда переменная определяется Шаг 7 . Параметризуем массивы типами 71 с помощью val , повторно присвоить значение данной переменной нельзя, но объект, на который она ссылается, потенциально может быть изменен. Следовательно, в данном случае присвоить greetStrings значение другого массива невозможно — переменная greetStrings всегда будет указывать на один и тот же экземпляр типа Array[String] , которым она была инициали зирована. Но впоследствии в элементы типа Array[String] можно вносить изменения, то есть сам массив является изменяемым. Последние две строки листинга 3.1 содержат выражение for , которое по очередно выводит каждый элемент массива greetStrings : for i <- 0 to 2 do print(greetStrings(i)) В первой строке кода для этого выражения for показано еще одно общее пра вило Scala: если метод получает лишь один параметр, то его можно вызвать без точки или круглых скобок. В данном примере to на самом деле является методом, получающим один Int аргумент. Код 0 to 2 преобразуется в вызов метода 0.to(2) 1 . Следует заметить, что этот синтаксис работает только при явном указании получателя вызова метода. Код println 10 использовать нельзя, а код Console println 10 — можно. С технической точки зрения в Scala нет перегрузки операторов, поскольку в нем фактически отсутствуют операторы в традиционном понимании. Вместо этого такие символы, как + , - , * , / , могут использоваться в качестве имен методов. Следовательно, когда при выполнении шага 1 вы набираете в интерпретаторе Scala код 1 + 2 , в действительности вы вызываете метод по имени + в отношении Int объекта 1 , передавая ему в качестве параметра значение 2 . Как показано на рис. 3.1, вместо этого 1 + 2 можно записать с по мощью традиционного синтаксиса вызова метода: 1.+(2) Еще одна весьма важная идея, проиллюстрированная в этом примере, по может понять, почему доступ к элементам массивов Scala осуществляется с помощью круглых скобок. В Scala меньше особых случаев по сравнению с Java. Массивы в Scala, как и в случае с любыми другими классами, — просто экземпляры классов. При использовании круглых скобок, окру жающих одно или несколько значений переменной, Scala преобразует код в вызов метода по имени apply применительно к данной переменной. 1 Этот метод to фактически возвращает не массив, а иную разновидность последо вательности, содержащую значения 0, 1 и 2, последовательный перебор которых выполняется выражением for . Последовательности и другие коллекции будут рассматриваться в главе 15. |