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

Авторы: Билл Веннерс
Мартин Одерски
Лекс Спун

Перевод: Купаев Михаил
Чистяков Влад

Источник: First Steps to Scala
Материал предоставил: RSDN Magazine #2-2007
Опубликовано: 30.07.2007
Версия текста: 1.0
Шаг 1. Скачиваем и устанавливаем Scala
Шаг 2. Учимся использовать интерпретатор Scala
Шаг 3. Определим некоторые значения и переменные.
Шаг 4. Определим несколько методов
Шаг 5: Пишем скрипты на Scala
Шаг 6. Конструкции while и if
Шаг 7. Перебор с foreach и for
Шаг 8. Параметризуем массивы типами
Шаг 9. Используем списки и кортежи
Шаг 10. Используем Set и Map
Шаг 11. Понимание классов и синглтон-объектов
Шаг 12. Понимание trait-ов и mixin-ов
Заключение
Ресурсы

Scala – статически типизированный, объектно-ориентированный язык программирования, в котором смешиваются императивный и функциональный стили программирования. Scala рассчитана на легкую интеграцию с приложениями, исполняемыми на современных виртуальных машинах, в первую очередь, виртуальной машине Java (JVM). Основной компилятор Scala, scalac, генерирует файлы Java-классов, которые могут исполняться под JVM. Однако существует и другой компилятор Scala, генерирующий бинарные файлы, исполняемые под .NET CLR, поскольку Scala задумана для интеграции с мирами как Java, так и .NET. В мире Java язык Scala можно использовать параллельно с языком Java – или как альтернативу Java – для создания приложений, исполняемых под JVM.

Скала разрабатывается с 2003 группой Мартина Одерского (Martin Odersky) в EPFL, в Лозанне, Швейцария. Он участвовал в создании первой версии generic-ов Java и был автором текущего компилятора javac. Работа над Scala мотивировалась желанием обойти ограничения, накладываемые требованиями обратной совместимости с Java. Так что Scala – это не расширение Java, но она сохраняет возможность взаимодействия с Java.

Одна из причин, способных заставить вас обратиться к программированию на Scala, состоит в том, что Scala позволяет увеличить производительность разработчика по сравнению с Java, сохраняя скорость исполнения JVM, существующие инвестиции в Java-код, знания и множество API, имеющихся для JVM. Scala обладает краткостью языков типа Ruby или Python, но при этом статически типизирована, как и Java. Еще одна причина в том, что Scala поставляется с Erlang-подобной библиотекой Actors, которая существенно упрощает параллельное программирование, но работает под JVM.

По-итальянски Scala означает "лестница". В данной статье мы проведем вас по 12 ступеням, которые помогут вам набраться знаний в Scala. Лучше всего использовать эту статью, проверяя каждый пример с помощью интерпретатора или компилятора Scala. Шаг 1 объясняет, как скачать и установить дистрибутив Scala.

Шаг 1. Скачиваем и устанавливаем Scala

Для исполнения примеров из этой статьи, вам нужно скачать Scala со страницы http://www.scala-lang.org/downloads/index.html. Примеры этой статьи написаны для Scala 2.5.0-RC1, так что вам нужно скачать более свежую версию, чем 2.5.0-RC1. После скачивания архива создайте каталог (возможно, с именем scala), и распакуйте архив в этот пустой каталог. Среди создаваемых при распаковке подкаталогов будет каталог bin с исполняемыми файлами Scala, включая компилятор и интерпретатор. Для удобства использования Scala добавьте путь к каталогу bin в переменную среды PATH. Единственное дополнительное требование – установить Java 1.4 или выше, скачать Java можно с http://java.sun.com/. Можно также использовать Scala через модули расширения Eclipse и IntelliJ, но здесь мы будем считать, что вы используете дистрибутив Scala со scala-lang.org.

Шаг 2. Учимся использовать интерпретатор Scala

Самый простой путь начать работать со Scala – использование интерпретатора Scala, интерактивной оболочки для написания программ и выражений Scala. Просто введите выражение в интерпретаторе, и он вычислит его значение и выведет результат. Интерактивная оболочка Scala называется просто scala. Используется она так:

$ scala
This is an interpreter for Scala.
Type in expressions to have them evaluated.
Type :help for more information.

scala> 

Если вы напишете выражение и нажмете на ввод:

scala> 1 + 2

Интерпретатор выведет:

unnamed0: Int = 3

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

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

В последних строках может использоваться идентификатор unnamedX. Например, если до этого unnamed0 был установлен в 3, unnamed0 * 3 будет равно 9:

scala> unnamed0 * 3
unnamed1: Int = 9

Чтобы вывести необходимое, но не достаточное приветствие Hello, world!, введите:

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

Тип результата здесь – scala.Unit, Scala-аналог void в Java. Главная разница между Unit в Scala и void в Java состоит в том, что Scala позволяет записать значение типа Unit, а именно (), а в Java нет значений типа void. (другими словами, как 1, 2 и 3 являются потенциальными значениями типа int в Scala и Java, так () является единственным значением типа Unit в Scala.) За этим исключением Unit и void эквивалентны. В частности, каждый возвращающий void метод в Java отображается на возвращающий Unit метод в Scala.

Шаг 3. Определим некоторые значения и переменные.

Scala различает значения, которые назначаются один раз и никогда не изменяются, и переменные, которые могут изменяться. Значения определяются с помощью ключевого слова val, а переменные – с помощью ключевого слова var. Вот определение значения:

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

Здесь вводится msg как имя для значения "Hello world!". Тип приведенного выше значения – java.lang.String, поскольку строки Scala – это также и строки Java (на самом деле все Java-классы доступны в Scala).

Этот пример также показывает важную и очень полезную возможность Scala: наследование типов. Заметьте, что в определении значения ничего не говорилось ни о java.lang.String, ни о просто String. Интерпретатор Scala вывел тип значения который будет ему назначен при инициализации. Поскольку msg было инициализировано значением "Hello, world!", и поскольку тип "Hello, world!" – java.lang.String, компилятор присваивает msg тип java.lang.String.

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

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

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

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

Возвращаясь к нашему исходному msg, теперь оно определено, и вы можете использовать значение msg:

scala> println(msg)
Hello, world!
unnamed3: Unit = ()

Что вы не можете сделать с msg, поскольку это значение, а не переменная – переназначить его. Например, посмотрите, как интерпретатор отреагирует на следующее:

scala> msg = "Goodbye cruel world!"
<console>:5 error: assignment to non-variable 
  val unnamed4 = {msg = "Goodbye cruel world!";msg}

Если вам требуется переназначение, вам придется использовать переменную, как в:

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

Поскольку greeting – это переменная (определенная через var), а не значение (определенное через val), вы можете изменить ее значение позже. Если у вас испортится настроение, например, вы можете изменить greeting на:

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

Шаг 4. Определим несколько методов

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

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

Определения методов начинаются не с val или var, а с def. За именем метода, в данном случае max, идет список параметров в круглых скобках. За каждым параметром метода должно следовать указание типа, предваряемое двоеточием, как принято в Scala, так как компилятор Scala (и интерпретатор тоже, но для простоты дальше будем называть их обоих компилятором) не выводит типы параметров методов. В данном примере метод max принимает два параметра, x и y, оба типа int. За закрывающей скобкой списка параметров метода max вы найдете еще одно указание типа “: Int”. Оно показывает тип возвращаемого значеия метода max.

Иногда компилятор Scala потребует от вас указать возвращаемый тип метода. Если метод рекурсивен, например, вы должны явно указать возвращаемый тип метода. В случае max вы можете опустить указание возвращаемого типа, компилятор выведет его. Таким образом, метод max можно записать как:

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

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

Имя, список параметров и возвращаемый тип, если он указан, образуют сигнатуру метода. Вслед за сигнатурой метода идет знак равенства, а за ним – тело метода. Поскольку тело метода max состоит всего из одного выражения, его необязательно помещать в фигурные скобки, но если хотите – можно. Так что можно написать так:

scala> def max3(x: Int, y: Int) = { if (x < y) y else x }
max3: (Int,Int)Int

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

Определив метод, его можно вызвать по имени:

scala> max(3, 5)
unnamed6: Int = 5

Заметьте, что если метод не принимает параметров, как в:

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

его можно вызывать как со скобками, так и без них:

scala> greet()
Hello, world!
unnamed7: Unit = ()

scala> greet
Hello, world!
unnamed8: Unit = ()

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

Шаг 5: Пишем скрипты на Scala

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

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

и выполните

>scala hello.scala

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

Hello, world, from a script!

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

// Скажем hello первому аргументу
println("Hello, " + args(0) + "!")

и запустите его:

>scala helloarg.scala planet

В этой команде "planet" передается как аргумент командной строки, к которому скрипт обращается как к args(0). То есть вы увидите:

Hello, planet!

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

Кстати, под Unix можно исполнять Scala-скрипты как скрипты оболочки, если разместить в начале файла директиву, как в следующем файле helloarg:

#!/bin/sh
exec scala $0 $@
!#
println("Hello, " + args(0) + "!")

#!/bin/sh должна быть самой первой строкой файла. После задания разрешения на исполнение:

>chmod +x helloarg

вы сможете запускать Scala-скрипты как скрипты оболочки, просто сказав:

>./helloarg globe

что выдаст:

Hello, globe!

Шаг 6. Конструкции while и if

Циклы while в Scala пишутся примерно так же, как в Java. Попробуйте использовать while, введя в файл printargs.scala следующее:

var i = 0
while (i < args.length) 
{
  println(args(i))
  i += 1
}

Этот скрипт начинается с определения переменной var i = 0. Выведение типов присвоит i тип Scala.Int, поскольку это тип ее исходного значения, 0. Конструкция while на следующей строке приводит к повторению исполнения блока кода в фигурных скобках до тех пор, пока булево выражение i < args.length не станет равно false. args.length дает длину массива args, аналогично тому, как вы получаете длину массива в Java. Блок содержит два выражения, отбитых на два пробела внутрь, что есть рекомендованный в Scala стиль отбивки. Первое выражение, println(args(i)), выводит i-й аргумент командной строки. Второе выражение, i += 1, увеличивает i на единицу. Заметьте, что ++i и i++ из Java в Scala не работают. Запустите этот скрипт следующей командой:

>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). Поскольку i != 0 будет равно false на первом же цикле while, перед первым аргументом пробел вставлен не будет. Наконец, добавляем еще один println в конец, чтобы получить пустую строку после вывода всех аргументов.

Если запустить этот скрипт следующей командой:

>scala echoargs.scala Scala is even more fun

будет выдано:

Scala is even more fun

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

var i = 0;

while (i < args.length) 
{
  if (i != 0) 
  {
    print(" ");
  }

  print(args(i));
  i += 1;
}
println();

Если поместить этот код в файл echoargsverbosely.scala и запустить его командой:

> scala echoargsverbosely.scala In Scala semicolons are often optional

появится результат:

In Scala semicolons are often optional

Обратите внимание, что поскольку у вас не было параметров для передачи методу println, можно было оставить его без скобок, и компилятор был бы этим вполне доволен. Но при данном стиле оформления кода требуется всегда использовать скобки при вызове методов, способных иметь побочные эффекты – в сочетании с тем, что при выводе на стандартное устройство println и в самом деле будет иметь побочные эффекты – вы должны использовать скобки даже в краткой версии echoargs.scala.

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

Шаг 7. Перебор с foreach и for

Вы могли не осознавать этого, но когда вы писали циклы while на предыдущем шаге, это было программирование в императивном стиле. В императивном стиле, таком, который обычно используется в языках типа Java, C++ и C, вы выдаете одну императивную команду за раз, повторяете действия в циклах и часто изменяете состояние, совместно используемое разными функциями или методами. Scala позволяет программировать императивно, но когда вы лучше узнаете 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, так как массив строковый. Если вы хотите выразиться более явно, можете указать имя типа, но вам придется обернуть аргументы в скобки (что есть обычная форма синтаксиса). Попробуйте написать в файле epa.scala следующее.

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

Этот скрипт при исполнении ведет себя так же, как и предыдущий. Введите команду:

scala epa.scala Explicit can be nice too

Вы получите:

Explicit
can
be
nice 
too

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

args.foreach(println)

Суммируя, синтаксис анонимной функции – это заключенный в круглые скобки список именованных параметров, стрелка вправо и тело функции. Этот синтаксис иллюстрирует рисунок 1.


Рисунок 1. Синтаксис анонимной функции в Scala.

К этому моменту вы можете задаться вопросом, что случилось с теми надёжными циклами, которые вы привыкли использовать в императивных языках типа Java. Чтобы вести программиста в функциональном направлении, в Scala доступен только функциональный родственник императивного for (называемый for comprehension). В этой статье вы не увидите их во всем блеске, но кое-что мы вам покажем. В новом файле forprintargs.scala напишите следующее:

for (arg <- args)
  println(arg)

Скобки после for в этом for comprehension содержат arg <- args. Слева от символа <-, который можно назвать "в", находится декларация нового значения (не переменной) arg. Справа от <- находится знакомый массив args. При выполнении этого кода значение arg будет сопоставлено по очереди с каждым элементом массива args, и для каждого значения будет выполнено тело for, println(arg). For comprehensions в Scala могут делать гораздо больше этого, но эта простая форма аналогична функциональности из Java 5:

// ...
for (String arg : args)     // Помните, это Java, а не Scala
{
  System.out.println(arg);
}
// ...

Или Ruby:

for arg in ARGV   # Помните, это Ruby, а не Scala
  puts arg
end

При запуске forprintargs.scala командой:

scala forprintargs.scala for is functional

Вы увидите:

for
is
functional

Шаг 8. Параметризуем массивы типами

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

val s = new String("Hello, world!")
println(s)

В предыдущем примере вы параметризовали экземпляр String исходным значением "Hello, world!". О параметризации можно думать как о конфигурировании экземпляра в той точке программы, где он создается. Вы конфигурируете экземпляр значениями, передавая объекты конструктору экземпляра в скобках, так же, как при создании экземпляра в Java. Если поместить предыдущий код в файл paramwithvalues.scala и запустить его, вы увидите знакомое Hello, world!

Кроме параметризации экземпляров значениями в точке создания, в Scala можно параметризовать их типами. Этот вид параметризации похож на указание типа в угловых скобках при создании экземпляра generic-типа в Java 5 и выше. Основное различие – вместо угловых скобок в Scala используются квадратные:

val greetStrings = new Array[String](3)

greetStrings(0) = "Hello"
greetStrings(1) = ", "
greetStrings(2) = "world!\n"

for (i <- 0 to 2)
  print(greetStrings(i))

В этом примере greetStrings это значение типа Array[String] (назовем это "массивом строк"), которое инициализируется длиной 3 с помощью передачи значения 3 конструктору в круглых скобках в первой строек кода. Введите этот код в новый файл paramwithtypes.scala, запустите с помощью scala paramwithtypes.scala, и вы получите очередное Hello, world! Заметьте, что при параметризации экземпляра одновременно типом и значением сперва идет тип в квадратных скобках, а потом уже значение – в круглых.

Пожелай вы указать все явно, вы могли бы явно указать тип greetStrings:

val greetStrings: Array[String] = new Array[String](3)
// ...

С учетом выведения типов в Scala эта строка кода семантически эквивалентна реальной первой строке кода из paramwithtypes.scala. Но эта форма показывает, что часть, связанная с параметризацией типом (имя типа в квадратных скобках) формирует часть типа экземпляра, а часть, параметризующая значением (значение в квадратных скобках) – нет. Тип greetStrings – Array[String], а не Array[String](3).

Следующие три строки кода в paramwithtypes.scala инициализируют каждый из элементов массива greetStrings:

// ...
greetStrings(0) = "Hello"
greetStrings(1) = ", "
greetStrings(2) = "world!\n"
// ...

Как уже говорилось, доступ к массивам в Scala производится через индекс, помещаемый в круглые скобки, а не в квадратные, как в Java. Поэтому нулевой элемент массива – это greetStrings(0), а не greetStrings[0], как в Java.

Эти три строки кода иллюстрируют важную концепцию Scala, касающуюся значения val. Когда вы определяете значение через val, значение нельзя изменить, но объект по ссылке потенциально может изменяться. Так что в этом случае вы не можете переназначить greetStrings на другой массив; greetStrings всегда будет указывать на тот же экземпляр Array[String], которым он был инициализирован. Но вы можете изменять элементы самого Array[String], так что сам массив вполне может изменяться.

Последние две строки paramwithtypes.scala содержат for comprehension, который выводит все элементы массива greetStrings:

// ...
for (i <- 0 to 2)
  print(greetStrings(i))

Первая строка кода в этом for comprehension иллюстрирует другое общее правило Scala: если метод принимает только один параметр, его можно вызывать без точки и скобок. to на самом деле метод, определенный в классе scala.Int, и принимающий один аргумент, также типа int. Код 0 to 2 трансформируется в вызов метода 0.to(2) (этот метод to на самом деле возвращает не Array, а итератор Scala, возвращающий значения 0, 1 и 2). Scala технически не имеет перегрузки операторов, поскольку в ней нет операторов в традиционном смысле. Такие символы, как +, -, * и /, в Scala не имеют специальных значений, но их можно испоьзовать в именах методов. Таким образом, выражение 1 + 2, которое было первым написанным вами в интерпретаторе Scala-кодом на шаге 1, в сущности означает 1.+(2), где + – это имя метода, определенного в классе scala.Int.

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

Аналогично, когда присвоение выполняется для значения или переменной, за которой идут какие-то аргументы в скобках, компилятор трансформирует их в вызов метода update, принимающего два параметра. Например:

greetStrings(0) = "Hello" 

в сущности, превратится в:

greetStrings.update(0, "Hello")

Таким образом, следующий Scala-код семантически эквивалентен коду из paramwithtypes.scala:

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

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

Одна из главных идей функционального стиля программирования заключается в том, что у методов не должно быть побочных эффектов. Единственным эффектом метода должно быть вычисление значения или значений, возвращаемых методом. Некоторые преимущества этого подхода в том, что методы становятся менее запутанными, а потому более надежными и простыми в использовании. Еще одно преимущество функционального стиля в статически типизированном языке состоит в том, что типы всего, попадающего в метод и выходящего из него, проверяются, и логические ошибки скорее всего заявят о себе как ошибки типизации. Чтобы применить эту функциональную философию к миру объектов, объекты нужно сделать неизменяемыми. Простой пример неизменяемого объекта в Java – это String. Если вы создаете String со значением "Hello, ", он будет хранить это значение всю свою оставшуюся жизнь. Если вы позже вызовете concat("world!") для этого String, он не добавит "world!" к себе. Вместо этого он создаст и возвратит новехонький String со значением "Hello, world!".

Как вы видели, в Scala Array – это изменяемая коллекция объектов, разделяющих общий тип. Array[String] содержит только элементы типа String, например. Хотя вы не можете изменить длину Array после создания его экземпляра, вы можете изменять значения его элементов. Поэтому массивы в Scala – изменяемые объекты. Неизменяемой, и потому более функционально-ориентированной последовательностью объектов, имеющих одинаковый тип, в Scala является List. Как и Array, List[String] содержит только объекты типа String. scala.List отличается от типа java.util.List в Java в том, что List в Scala всегда неизменен (а в Java может изменяться). Но еще важнее то, что List в Scala рассчитан на функциональный стиль программирования. Создать List просто:

val oneTwoThree = List(1, 2, 3)

Здесь создается новое значение oneTwoThree, которое содержит ссылку на новый List[Int] с целочисленными значениями 1, 2 и 3 (вам не нужно писать new List, так как “List” определен как фабричный метод объекта-синглтона scala.List. Подробнее об объектах-синглтонах в Scala сказано в шаге 11.). Поскольку объекты List неизменяемы, они кое в чем ведут себя как String в Java, например, когда вы вызываете для них метод, который, как казалось бы по его названию, должен изменить список, он на самом деле создает новый список и возвращает его. Например, у списка есть метод :::, который конкатенирует переданный список и список, для которого был вызван :::. Вот как его использовать:

val oneTwo = List(1, 2)
val threeFour = List(3, 4)
val oneTwoThreeFour = oneTwo ::: threeFour
println(oneTwo + " and " + threeFour + " were not mutated.")
println("Thus, " + oneTwoThreeFour + " is a new List.")

Создайте файл listcat.scala с этим кодом, запустите его командой scala listcat.scala, и вы увидите:

List(1, 2) and List(3, 4) were not mutated.
Thus, List(1, 2, 3, 4) is a new List.

Достаточно.

На самом деле внимательный читатель мог заметить что-то неладное с ассоциативностью метода :::, но это правило легко запомнить. Если метод используется как оператор, как в a * b или a ::: b, метод вызывается у левого операнда, как в a.*(b), если имя метода не заканчивается двоеточием. Если имя метода заканчивается двоеточием, метод вызывается у правого операнда, как в b.:::(a).

Возможно, наиболее часто вы будете применять к списку оператор конкатенации "::" (который произносится как “cons”). Этот оператор присоединяет новый элемент к началу существующего списка List, и возвращает результирующий List. Например, если вы внесете следующий код в файл consit.scala:

val twoThree = List(2, 3)
val oneTwoThree = 1 :: twoThree
println(oneTwoThree)

И запустите его командой scala consit.scala, вы увидите:

List(1, 2, 3)

При том, что кратчайший способ указать пустой список это Nil, одним из способов инициализировать новые списки является запись в строку, разделенных оператором ::, с Nil в качестве последнего элемента. Например, если в файле consinit.scala будет написано:

val oneTwoThree = 1 :: 2 :: 3 :: Nil
println(oneTwoThree)

То после его запуска будет выведено:

List(1, 2, 3)

List в Scala набит полезными методами, часть из них показана в таблице 1.

Что этоЧто оно делает
List()Создает пустой список
NilСоздает пустой список
List("Cool", "tools", "rule")Создает новый List[String] с тремя значениями "Cool", "tools" и "rule"
val thrill = "Will" :: "fill" :: "until" :: NilСоздает новый List[String] с тремя значениями "Will", "fill" и "until"
thrill(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 List , длина которых равна 4 (возвращает List("Will", "fill"))
thrill.forall(s => s.endsWith("l"))Возвращает true, все ли элементы списка thrill заканчиваются на букву "l", и false в обратном случае (в данном примере возвращает 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. То есть как бы отображает (map) один список на другой (возвращает List("Willy", "filly", "untily")).
thrill.remove(s => s.length == 4)Возвращает копию списка thrill, из которой удалены все элементы, длина которых равна 4 (возвращает List("until")).
thrill.reverseВозвращает List, содержащий все элементы thrill List в обратном порядке (возвращает List("fill", "until", "Will")).
thrill.sort((s, t) => s.charAt(0).toLowerCase < t.charAt(0).toLowerCase)Возвращает отсортированную копию списка thrill (возвращает List("fill", "until", "Will")).
thrill.tailВозвращает список thrill за вычетом его первого элемента (возвращает List("fill", "until")).
Таблица 1. Некоторые методы List и их использование.

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

val pair = (99, "Luftballons")
println(pair._1)
println(pair._2)

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

99
Luftballons

Фактический тип кортежа зависит от числа его элементов и их типов. Таким образом, тип (99, "Luftballons") - Tuple2[Int, String]. Тип ('u', 'r', 1, 4, "me") - Tuple6 [Char, Char, String, Int, Int, String].

Шаг 10. Используем Set и Map

Поскольку Scala должна помочь вам использовать как функциональный, так и императивный стили, в ее библиотеке коллекций делается упор на различение изменяемых и неизменяемых классов коллекций. Например, Array всегда изменяемы, а List всегда неизменны. Когда дело доходит до Set и Map, также предоставляет изменяемые и неизменяемые альтернативы, но другим путем. Для Set и Map Scala моделирует изменяемость в иерархии классов.

Например, Scala API содержит базовый trait для Set, где trait похож на Java-интерфейс (подробнее об этом будет сказано на шаге 12). Scala предоставляет два суб-trait-а, один для изменяемых Set, другой – для неизменяемых. Как можно увидеть на рисунке 2, все три trait-а разделяют одно простое имя Set. Их полностью квалифицированные имена, конечно, различаются, поскольку все они находятся в разных пакетах. Конкретные классы Set в Scala API, такие как показанные на рисунке 2 классы HashSet, расширяют изменяемые либо неизменяемые trait-ы Set (там где в Java вы реализуете интерфейсы, в Scala вы "расширяете" trait-ы). Так что если вам захочется использовать HashSet, вы можете выбирать между изменяемым и неизменяемым вариантами, в зависимости от потребностей.


Рисунок 2. Иерархия классов для Set в Scala.

Чтобы испытать Set в Scala, наберите следующий код в файле jetset.scala:

import scala.collection.mutable.HashSet

val jetSet = new HashSet[String]
jetSet += "Lear"
jetSet += ("Boeing", "Airbus")
println(jetSet.contains("Cessna"))

Первая строка jetSet.scala импортирует изменяемый HashSet. Как и в Java, импорт позволяет использовать в исходном коде простое имя класса, HashSet. После пустой строки, в третьей строке создается новый HashSet, содержащий только элементы типа String, и хранящий результирующую ссылку в jetSet. Заметьте, что как и в случае List и Array, при создании Set его нужно параметризовать типом (в данном случае String), так как все объекты в Set должны иметь один тип. Две следующие строки добавляют три объекта в изменяемый Set с помощью метода +=. Как и большинство других символов, похожих на операторы в Scala, на самом деле это метод, определенный в классе HashSet. Если бы вам захотелось, вместо jetSet += "Lear", можно было написать jetSet.+=("Lear"). Поскольку метод += принимает переменное число аргументов, ему можно одновременно передать несколько объектов. Например, jetSet += "Lear" добавляет в HashSet один элемент типа String, а jetSet += ("Boeing", "Airbus") – два. Наконец, последняя строка выводит, содержит ли Set конкретную строку (как и ожидалось, она выводит false).

Еще один полезный класс коллекций в Scala – Map. Как и в случае Set, Scala предоставляет изменяемую и неизменяемую версии Map, используя иерархию классов. Как показано на рисунке 3, иерархия классов для Map очень похожа на иерархию для Set. Есть базовый trait Map в пакете scala.collection, и два суб- trait-а Map: изменяемый Map в scala.collection.mutable и неизменяемый – в scala.collection.immutable.


Рисунок 3. Иерархия классов для Map в Scala.

Реализации Map, такие как HashMap, показанные в иерархии классов на рисунке 3, реализуют изменяемый или неизменяемый trait. Чтобы увидеть Map в действии, создайте файл treasure.scala со следующим кодом.

import scala.collection.mutable.HashMap

val treasureMap = new HashMap[Int, String]
treasureMap += 1 -> "Go to island."
treasureMap += 2 -> "Find big X on ground."
treasureMap += 3 -> "Dig."
println(treasureMap(2))

В первой строке treasure.scala импортируется изменяемая форма HashMap. После пустой строки создается новый экземпляр изменяемого HashMap, с ключами типа Int и значениями типа String, и и помещает ссылку на HashMap в значение treasureMap. В следующих трех строках вы добавляете пары ключ/значение в HashMap, используя метод ->. Как показано в предыдущих примерах, компилятор Scala трансформирует выражение бинарной операции типа 1 -> "Go to island." в 1.->("Go to island."). То есть, когда вы говорите 1 -> "Go to island.", вы в действительности вызываете метод -> для Int со значением 1, и передаете ему String со значением "Go to island." Этот метод ->, который вы можете вызвать для любого объекта Scala-программы, возвращает кортеж из двух элементов, содержащий ключ и значение. Затем этот кортеж вы передаете методу += объекту HashMap, на который ссылается treasureMap. Наконец, последняя строка выводит значение, соответствующее ключу 2 в treasureMap. При исполнении этот код выведет:

Find big X on ground.

Поскольку map – это очень полезная конструкция в программировании, Scala предоставляет для Map фабричный метод, по духу похожий на фабричный метод, показанный в шаге 9 и позволяющий создавать List без ключевого слова new. Чтобы попробовать в действии этот краткий способ создания map, создайте файл numerals.scala со следующим кодом:

val romanNumeral = Map(1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V")
println(romanNumeral(4))

В numerals.scala вы используете тот факт, что trait неизменяемого Map автоматически импортируется в любой исходный код Scala. То есть когда вы пишете Map в первой строке кода, интерпретатор Scala знает, что вы имеете в виду scala.collection.immutable.Map. В этой строке вы вызываете фабричный метод объекта-синглтона неизменяемого Map, передавая ему пять кортежей ключ/значение в качестве параметров. Этот фабричный метод возвращает экземпляр неизменяемого HashMap, содержащий переданные пары ключ/значение. Имя фабричного метода – apply, но, как упомянуто в шаге 8, если написать Map(...), это будет трансформировано компилятором в Map.apply(...). Если вы выполните скрипт numerals.scala, он выведет IV.

Шаг 11. Понимание классов и синглтон-объектов

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

class SimpleGreeter 
{
  val greeting = "Hello, world!"
  def greet() = println(greeting)
}

val g = new SimpleGreeter
g.greet()

На самом деле greetSimply.scala – это Scala-скрипт, но он содержит определение класса. Однако этот первый пример показывает, что в Scala, как и в Java, классы инкапсулируют поля и методы. Поля определяются с помощью val или var. Methods определяются через def. Например, в классе SimpleGreeter, greeting – это поле, а greet – это метод. Для использования этого класса вы создаете новый экземпляр с new SimpleGreeter, и помещаете ссылку на этот экземпляр в значение g. Затем вы вызываете экземплярный метод greet для g. При запуске этого скрипта вы будете потрясены видом очередного Hello, world!.

Хотя классы в Scala во многом похожи на Java, в некоторых отношениях они силно различаются. Одно из различий между Java и Scala касается конструкторов. В Java у классов есть конструкторы, которые могут принимать параметры, тогда как в Scala классы могут принимать параметры напрямую. Нотация Scala короче – параметры классов могут использоваться напрямую в теле классов, не нужно определять поля и писать присвоения, копирующие параметры конструктора в поля. Это может дать существенную экономию, особенно в маленьких классах. Чтобы увидеть это в действии, введите следующий код в файл greetFancily.scala:

class FancyGreeter(greeting: String) 
{
  def greet() = println(greeting)
}

val g = new FancyGreeter("Salutations, world")
g.greet

Вместо определения конструктора, принимающего String, как это делается в Java, в greetFancily.scala, вы помещаете параметр greeting этого конструктора в скобках сразу за именем самого класса, перед открытием фигурных скобок тела класса FancyGreeter. При определении таким способом greeting в сущности становится значением (не переменной – его нельзя переназначить) поля, доступным везде в теле класса. Реально вы передаете его println в теле метода greet. Если вы запустите этот скрипт командой scala greetFancily.scala, вы получите воодушевляющее:

Salutations, world!

Это здорово и кратко, но что, если вы захотите проверять строку, переданную первичному конструктору FancyGreeter, на null, и генерировать NullPointerException? К счастью, это возможно. Любой код, находящийся внутри блока кода класса верхнего уровня (фигурные скобки, окружающие само определение класса) и не являющийся частью определения метода, компилируется в тело первичного конструктора. В сущности, первичный конструктор будет добавлять final-поле для каждого параметра конструктора (в круглых скобках, следующих за названием класса). Например, чтобы проверить передаваемый параметр на null, создайте класс greetCarefully.scala со следующим кодом:

class CarefulGreeter(greeting: String) 
{

  if (greeting == null) 
  {
    throw new NullPointerException("greeting was null")
  }

  def greet() = println(greeting)
}
new CarefulGreeter(null)

В greetCarefully.scala выражение if находится в середине тела класса, что в Java не скомпилировалось бы. Компилятор Scala помещает это if в тело первичного конструктора, сразу после кода, инициализирующего поля. То есть если вы передаете внутрь в первичном конструкторе null, как в последней строке скрипта greetCarefully.scala, первичный конструктор сперва проинициализирует значением null поле greeting. Затем он будет исполнять выражение if, проверяющих равенство null поля greeting, и поскольку так оно и есть, выдаст NullPointerException. Если запустить greetCarefully.scala, вы увидите исключение NullPointerException.

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

class RepeatGreeter(greeting: String, count: Int) 
{

  def this(greeting: String) = this(greeting, 1)

  def greet() = 
  {
    for (i <- 1 to count)
      println(greeting)
  }
}

val g1 = new RepeatGreeter("Hello, world", 3)
g1.greet()
val g2 = new RepeatGreeter("Hi there!")
g2.greet()

Первичный конструктор RepeatGreeter принимает не только строковый параметр greeting, но и целочисленный счетчик числа раз выводов приветствия. Однако RepeatGreeter содержит и определение второго конструктора, метода this, принимающего один строковый параметр greeting. Тело этого конструктора состоит из единственного выражения: вызова первичного конструктора, параметризованного переданным greeting и значением счетчика 1. Последние четыре строки скрипта greetRepeatedly.scala создают два экземпляра RepeatGreeter, по одному для каждого конструктора, и вызывают у каждого greet. Если запустить greetRepeatedly.scala, он выведет:

Hello, world
Hello, world
Hello, world
Hi there!

Еще одно отличие Scala от Java состоит в том, что в Scala-классах не может быть статических полей или методов. Вместо этого Scala позволяет создать singleton-объекты с помощью ключевого слова object. Singleton-объект нельзя, да и не нужно, создавать с помощью new. В сущности он автоматически создается при первом использовании, и как следует из слова “singleton”, он может существовать только в одном экземпляре. Singleton-объект может иметь то же имя, что и класс. Компилятор Scala трансформирует поля и методы singleton-объекта в статические поля и методы результирующего бинарного Java-класса. Создайте файл WorldlyGreeter.scala со следующим кодом:

// Класс WorldlyGreeter
class WorldlyGreeter(greeting: String) 
{
  def greet() = 
  {
    val worldlyGreeting = WorldlyGreeter.worldify(greeting)
    println(worldlyGreeting)
  }
}

// Singleton-объект WorldlyGreeter
object WorldlyGreeter 
{
  def worldify(s: String) = s + ", world!"
}

В этом файле вы определяете класс с помощью ключевого слова class, и singleton-объект с помощью ключевого слова object. Оба типа называются WorldlyGreeter. С точки зрения Java-программиста это можно рассматривать следующим образом: любые статические методы, которые в Java вы поместили бы в класс WorldlyGreeter, в Scala вы помещаете в singleton-объект WorldlyGreeter. На самом деле, когда компилятор Scala создает байткод для этого файла, он создает Java-класс WorldlyGreeter, который содержит экземплярный метод greet (определенный в классе WorldlyGreeter в исходном коде Scala) и статический метод worldify (определенный в singleton-объекте WorldlyGreeter). Заметьте также, что в первой строке метода greet класса WorldlyGreeter вызывается метод worldify singleton-объекта, с помощью синтаксиса, похожего на способ вызова статических методов в Java: имя singleton-объекта, точка и имя метода.

// Вызываем метод singleton-объекта у класса WorldlyGreeter 
// ...
val worldlyGreeting = WorldlyGreeter.worldify(greeting)
// ...

Чтобы исполнить этот код, придется создать приложение. Создайте файл WorldlyApp.scala со следующим кодом:

// singleton-объект с методом main, поволяющим
// этому singleton-объекту запускаться как приложение
object WorldlyApp 
{
  def main(args: Array[String]) 
  {
    val wg = new WorldlyGreeter("Hello")
    wg.greet()
  }
}

Разница между Scala и Java в том, что если Java требует поместить public-класс в файл с тем же именем, что и класс – например, класс SpeedRacer помещается в файл SpeedRacer.java – в Scala вы можете называть .scala-файл как угодно, независимо от помещаемого в них кода. Однако, когда речь идет не о скриптах, рекомендуется называть файлы по именам содержащихся в них классов, как и в Java, чтобы программистам проще было искать классы по именам файлов. Это подход, который мы используем в двух файлах этого примера, WorldlyGreeter.scala и WorldlyApp.scala.

Ни WorldlyGreeter.scala, ни WorldlyApp.scala не являются скриптами, поскольку кончаются определениями. Скрипт же должен заканчиваться результирующим выражением. То есть если попробовать запустить один из этих файлов как скрипт, например, так:

scala WorldlyGreeter.scala # Это не сработает!

Интерпретатор Scala пожалуется, что WorldlyGreeter.scala не заканчиваться результирующим выражением. Вместо этого вам потребуется скомпилировать эти файлы с помощью компилятора Scala, а затем запустить получившиеся файлы классов. Один из способов сделать это – использовать scalac, основного компилятора Scala. Просто введите:

scalac WorldlyApp.scala WorldlyGreeter.scala

Учитывая, что компилятор scalac запускает новый экземпляр JVM при каждом его вызове, и что JVM часто имеет заметную задержку запуска, дистрибутив Scala также включает daemon-компилятор Scala по имени fsc (быстрый компилятор Scala). Он используется так:

fsc WorldlyApp.scala WorldlyGreeter.scala

При первом запуске fsc создаст локальный серверный демон, подключенный к порту вашего компьютера. Затем он через порт отправит демону список файлов, и демон скомпилирует файлы. При следующем запуске fsc демон уже будет работать и fsc просто отправит список файлов демону, который немедленно скомпилирует файлы. При использовании fsc вы ждете запуска JVM только в первый раз. Если вы захотите остановить fsc, это делается с помощью fsc -shutdown.

Запуск команд scalac или fsc породит файлы Java-классов, которые затем можно запустить с помощью команды scala, той же команды, что использовалась для вызова интерпретатора в предыдущих примерах. Однако вместо имени файла с расширением .scala, содержащего интерпретируемый Scala-код, как в предыдущих примерах, в данном случае вы даете ему имя класса, содержащего метод main.

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

Аналогично Java, любой Scala-класс с методом main, принимающим один параметр типа Array[String] и возвращающим Unit, может служить точкой входа приложения.

Как говорилось на шаге 2, Unit в Scala аналогичен void в Java. Если в Java метод main должен возвращать void, в Scala он должен возвращать Unit.

В данном примере у WorldlyApp есть метод main с подходящей сигнатурой, и можно запустить этот пример командой:

scala WorldlyApp

После чего вы, к глубочайшему своему изумлению, увидите:

Hello, world!

Вы уже, возможно, видели такой результат раньше, но в этот раз он был сгенерирован необычным образом:

Шаг 12. Понимание trait-ов и mixin-ов

Как впервые говорилось в шаге 10, Scala включает конструкцию trait, по духу похожую на интерфейсы Java. Одно из главных различий между интерфейсами Java и trait-ами состоит в том, что если все методы в Java-интерфейсах по определению абстрактны, то методы trait-ов Scala могут иметь реальные тела с реальным кодом. Вот пример:

trait Friendly 
{
  def greet() = "Hi"
}

В этом примере метод greet возвращает строку "Hi". Если вы пришли из Java, этот метод greet может показаться вам забавным, как будто greet() – это что-то вроде поля, инициализируемого строковым значением "Hi". На самом же деле в отсутствие явного выражения return, методы Scala возвращают значение последнего выражения. В данном случае значение последнего выражения - "Hi", которое и возвращается. Более пространный способ выразить то же самое выглядит так:

trait Friendly 
{
  def greet(): String = 
  {
    return "Hi"
  }
}

Независимо от того, как написан метод, суть в том, что trait-ы в Scala могут содержать не абстрактные методы. Еще одно различие между интерфейсами в Java и trait-ами в Scala – интерфейсы вы реализуете, а trait-ы – расширяете. Несмотря на эту разницу (implements/extends), наследование при определении новых типов в Scala работает аналогично Java. И в Java, и в Scala класс может расширять один (и только один) другой класс. В Java интерфейс может расширять ноль или более интерфейсов. Аналогично и в Scala, trait может расширять ноль или более trait-ов. В Java класс может реализовать несколько интерфейсов. Аналогично в Scala класс может расширять несколько trait-ов. Кстати, implements не является ключевым словом в Scala.

Вот пример:

class Dog extends Friendly 
{
  override def greet() = "Woof"
}

В этом примере класс Dog расширяет trait Friendly. Эти отношения наследования означают во многом то же, что и реализация интерфейса в Java. Вы можете хранить ссылку на экземпляр Dog в переменной или значении типа Friendly. Например:

var pet: Friendly = new Dog
println(pet.greet())

Когда вы вызываете метод greet для ссылки Friendly pet, будет использоваться динамическое связывание, как и в Java, для определения, какая реализация метода должна быть вызвана. В данном случае класс Dog переопределяет метод greet, так что будет вызвана реализация greet из Dog. При исполнении этого кода вы должны получить Woof (реализация greet в Dog), а не Hi (реализация greet в Friendly). Обратите внимание, еще одно отличие от Java состоит в том, что чтобы переопределить метод в Scala нужно перед def написать override. Если попробовать переопределить метод, не указав override, Scala-код не скомпилируется.

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

trait ExclamatoryGreeter extends Friendly 
{
  override def greet() = super.greet() + "!"
}

Trait ExclamatoryGreeter расширяет trait Friendly и переопределяет метод greet. Метод greet из ExclamatoryGreeter сперва вызывает метод greet суперкласса, присоединяет восклицательный знак к тому, что возвращает метод greet, и возвращает результирующую строку. Вы можете подмешать поведение этого trait-а во время создания экземпляра, используя ключевое слово with. Например:

val pup: Friendly = new Dog with ExclamatoryGreeter
println(pup.greet())

При такой первой строке кода компилятор Scala создаст синтетический тип, который расширяет класс Dog и trait ExclamatoryGreeter, и создаст его экземпляр (синтетический тип генерируется автоматически компилятором, а не пишется вручную программистом). При вызове метода синтетического типа это приводит к вызову правильной реализации. При запуске этот код выведет "Woof!". Ссылка на новый экземпляр синтетического типа будет присвоена значению pup. Заметьте, что если бы тип pup не был явно определен как Friendly, компилятор Scala вывел бы тип pup как Dog with ExclamatoryGreeter.

Чтобы попробовать, создайте файл friendly.scala со следующим кодом:

trait Friendly 
{
  def greet() = "Hi"
}

class Dog extends Friendly 
{
  override def greet() = "Woof"
}

class HungryCat extends Friendly 
{
  override def greet() = "Meow"
}

class HungryDog extends Dog 
{
  override def greet() = "I'd like to eat my own dog food"
}

trait ExclamatoryGreeter extends Friendly 
{
  override def greet() = super.greet() + "!"
}

var pet: Friendly = new Dog
println(pet.greet())

pet = new HungryCat
println(pet.greet())

pet = new HungryDog
println(pet.greet())

pet = new Dog with ExclamatoryGreeter
println(pet.greet())

pet = new HungryCat with ExclamatoryGreeter
println(pet.greet())

pet = new HungryDog with ExclamatoryGreeter
println(pet.greet())

При исполнении скрипт friendly.scala выведет:

Woof
Meow
I'd like to eat my own dog food
Woof!
Meow!
I'd like to eat my own dog food!

Заключение

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

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

Ресурсы


Эта статья опубликована в журнале RSDN Magazine #2-2007. Информацию о журнале можно найти здесь