Руководство по Scala для Java программистов

1 Введение
Этот документ дает возможность быстро ознакомиться с языком программирования и компилятором Scala.Этот документ предназначен для людей, которые уже имеют опыт в программировании и хотят посмотреть, что они могут сделать в Scala. При этом необходимо наличие основных знаний объектно-ориентированного программирования, особенно на Java.
2 Первый пример
Как первый пример мы будем использовать стандартную программу Hello world. Она не очень интересная, но позволяет легко продемонстрировать использование инструментов Scala без особого знания языка. Вот как она выглядит:
object HelloWorld {

def main(args: Array[String]) { println("Hello, world!" )
} }


Структура этой программы должна быть знакома Java-программистам: она состоит из метода main который берет аргументы командной строки, массив строк, как параметр; тело этого метода состоит из одного вызова предопределенного метода println с дружественным приветствием как аргумент. Метод main не возвращает значение (это процедурный метод).В связи с этим не нужно декларировать тип возвращаемого результата.
Что наименее известно Java программистам - это декларация object, содержащая main метод. Такая декларация знакомит с широко известным
singleton объектом, который является классом с одним экземпляром. Таким образом, приведённая выше декларация декларирует и класс с именем HelloWorld и экземпляр этого класса также с именем HelloWorld. Этот экземпляр создаётся по требованию (on demand), как только он начинает использоваться.
Проницательный читатель может заметить, что main метод не декларирован как static. Это потому что static-члены (методы или поля) не существуют в Scala. Вместо определения статических членов (методов и полей) программист Scala декларирует эти члены в singleton объектах.
2.1Компилирование примера
Для компиляции примера, мы используем scalac, компилятор Scala. scalac работает как большинство компиляторов: берёт исходные файлы как аргумент, возможно несколько опций, и создаёт один или несколько объектных файлов. Объектные файлы, которые он создаёт - стандартные java класс файлы.
Если мы сохраним приведённую выше программу в файле HelloWorld.scala, мы сможем скомпилировать еe набрав следующую команду (знак больше ‘>’ -- приглашение командной строки, и не он должен набираться):
> scalac HelloWorld.scala
Данная команда сгенерирует несколько class файлов в текущей директории. Один из них будет называться HelloWorld.class и содержать класс, который может быть прямо запущен командой scala, как это будет показано в следующей секции.
2.2 Запуск примера
Однажды скомпилированная программа Scala может быть запущена использовав команду scala. Её использование очень похоже на команду java для запуска java программ и
принимает те же опции. Предыдущий пример может быть выполнен при помощи следующей команды, которая выдаёт ожидаемый результат:
> scala -classpath . HelloWorld

Hello, world!
3 Взаимодействие с Java
Одной из сильных сторон Scala это то что она очень легко взаимодействует с кодом Java. Все классы с пакета java.lang импортированы по умолчанию, в то время другие пакеты нужно явно импортировать.
Давайте посмотрим на пример, который демонстрирует это. Мы хотим получить и форматировать текущую дату в соответствии с соглашениями принятыми в конкретной стране, например для Франции
1.
Библиотека классов Java содержит мощные утилитные классы, такие как Date и DateFormat. Так как Scala гладко взаимодействует с Java, нет нужды реализовывать эквивалентные классы в библиотеке классов Scala - мы можем просто импортировать соответствующие Java пакеты:
import java.util.{Date, Locale} import java.text.DateFormat import java.text.DateFormat._ object FrenchDate {
def main(args: Array[String]) {
val now = new Date
val df = getDateInstance(LONG, Locale.FRANCE) println(df format now)

} }
Выражение импорта в Sсala выглядит очень похоже на Java эквивалент, при этом оно более мощное. Множество классов может быть импортировано с того же пакета, окружая их фигурными скобками, как показано в первой строке. Другое отличие - это когда импортируются все имена пакета или класса, используется символ подчёркивания (_) вместо звёздочки (*). Это потому что звёздочка - корректный идентификатор в Scala (например, имя метода), как мы увидим позднее.
Таким образом, выражение импорта на третьей строке импортирует всех членов класса DateFormat. Это делает статический метод getDateInstance и статическое поле LONG непосредственно видимыми.
Внутри main метода мы сначала создадим экземпляр Java-класса Date, который по умолчанию содержит текущую дату. Далее мы определяем формат даты
, используя статический getDateInstance метод, который мы перед этим импортировали. И, наконец, мы печатаем текущую дату, отформатированную в соответствии с локализованным DateFormat экземпляром. Последняя строка показывает интересное свойство синтаксиса Scala. Метод, получающий один аргумент, может быть использован с инфиксным синтаксисом. Вот это выражение:
df format now
немного другой, немного менее многословный способ написания выражения df.format(now)
Это может показаться незначительной синтаксической деталью, но она имеет важные следствия одно из которых будет изучено в следующей главе.

Подводя итоги этой главы об интеграции с Java, нужно заметить что также возможно наследовать от Java классов и реализовать Java интерфейс напрямую в Scala.
4 Всё является объектом
Scala - чистый объектно-ориентированной язык в том смысле, что всё в нем является объектом, включая числа или функции. Это отличает его от Java т.к. Java проводит различие между примитивными типами (такие как boolean и int) и ссылочными типами, и не позволяет манипулировать функциями как значениями.
4.1 Числа являются объектами
Т.к. числа являются объектами, они также имеют методы. И действительно, такое арифметическое выражение как:
1 +2 * 3 / x
содержит исключительно вызовы методов, потому что (как мы видели из предыдущей главы) оно эквивалентно следующему выражению:

(1).+(((2).*(3))./(x))
Это также значит что +, *, и т.д. правильные идентификаторы в Scala.
Скобки вокруг чисел во второй версии нужны, потому что лексер
Scala использует правило самого длинного соответствия для токенов. Поэтому он разобьёт следующее выражение:
1.+(2)
на токены 1., +, и 2. Причина того
, что данная токенизация выбрана в том, что 1. является более длинным правильным соответствием, чем 1. Токен 1. интерпретируется как литерал 1.0, делая его Double а не Int.Запись выражения как:
(1).+(2)
не позволяет 1 интерпретировать 1 как Double.

4.2 Функции являются объектами.
Наиболее удивительно для Java программистов то, что функции также являются объектами в Scala. По этой причине возможно передавать функцию как аргумент, хранить их в переменных и возвращать их от других функций. Эта возможность манипулировать функциями как значениями - один из краеугольных камней очень интересной парадигмы программирования, называемой функциональным программированием.
 Как очень простой пример, почему использование функций как значений может быть полезно, давайте рассмотрим функцию таймера чья цель выполнять некоторые действия каждую секунду. Как мы передадим таймеру действие для выполнения? Вполне логично, как функцию. Это очень простой вид передачи функции должен быть знаком программистам: он часто используется в коде пользовательского интерфейса (UI), чтобы зарегистрировать функции обратного вызова (call back), вызываются, когда происходят некоторые события.
В следующей программе функция таймера, которая называется oncePerSecond, получает call-back функцию как аргумент. Тип этой функции записан () => Unit и это тип всех функций которые не получают аргументов и ничего не возвращают (тип Unit похож на void в C/C++). Функция main этой программы просто вызывает функцию таймера с call- back функцией, которая печатает предложение на терминал. Другими словами, эта программа бесконечно каждую секунду печатает предложение “time flies like an arrow”. object Timer {
def oncePerSecond(callback: () => Unit) { while (true) { callback(); Thread sleep 1000 }
}
def timeFlies() {

println("time flies like an arrow...") }
def main(args: Array[String]) { oncePerSecond(timeFlies)
} }
Обращаем внимание для того, чтобы напечатать строку мы используем предопределённый метод println вместо того, чтобы использовать его из System.out.
4.2.1 Анонимные функции
Пока программу легко понять, её можно немного улучшить. В начале обращаем внимание на то, что функция timeFlies определена только для того,чтобы быть переданной после в функцию oncePerSecond. Необходимость дать имя данной функции, которая используется только один раз, может показаться излишним, но, в действительности, было бы хорошо создать эту функцию только для того, чтобы передать её в oncePerSecond. В Scala возможно использовать анонимные функции, которые как раз и являются функциями без имени. Пересмотренная версия нашей программы-таймера, где используются анонимные функции вместо timeFlies, выглядит так:
object TimerAnonymous {
def oncePerSecond(callback: () => Unit) {

while (true) { callback(); Thread sleep 1000 } }
def main(args: Array[String]) { oncePerSecond(() =>
println("time flies like an arrow...")) }
}
Присутствие анонимной функции в этом примере обнаруживается с помощью правой стрелки ‘=>’ , которая отделяет список аргументов функции от его тела. В этом примере список аргументов пуст, о чем свидетельствует пустая пара скобок слева от стрелки. Тело данной функции такое же, как и тело функции timeFlies, описанной выше.
5 Классы
Как мы видели выше, Scala – объектно-ориентированный язык и, таким образом, имеет концепцию классов2.Классы в Scala описываются с применением синтаксиса близкого к синтаксису Java. Одна важная разница в том, что классы в Scala могут иметь параметры, как показано в следующем определении комплексных чисел:
class Complex(real: Double, imaginary: Double) { def re() = real
def im() = imaginary
}

Этот complex класс получает два аргумента, которые являются реальной частью и мнимой частью комплексного числа. Аргумент должен быть передан, когда создаётся экземпляр класса Complex, например: new Complex(1.5, 2.3). Класс содержит два метода, названные re и im, которые дают доступ эти двум частям.
Стоит отметить, что возвращаемый тип этих двух методов не задаётся явно. Он будет выведен компилятором автоматически, который смотрит на правую часть данных методов и делает вывод, что оба возвращают значение типа Double.
Компилятор не всегда может определить тип, как он определил его здесь, и, к сожалению, нет простого правила узнать точно, когда он определит, а когда нет. На практике это обычно не проблема, потому что компилятор жалуется, когда он не может определить тип, который явно не задан. Как простое правило, начинающие Scala программисты могут попытаться опустить декларации типа, которые по их мнению легко установить из контекста, и посмотреть согласен ли компилятор. Через некоторое время программисты должны получить хорошее представление о том, когда можно опустить декларацию типов, а когда нужно указать их точно.

5.1 методы без аргументов
Маленькая проблема методов re и im заключается в том, что для того, чтобы вызвать их, нужно поставить пустую пару скобок после их имени, как в следующем примере.
object ComplexNumbers {

def main(args: Array[String]) { val c = new Complex(1.2, 3.4)
println("imaginary part: " + c.im()) }
}
Было бы гораздо лучше иметь возможность доступа к real и imaginary частям как если бы они были полями, без написания пустой пары скобок. Это можно отлично выполнить в Scala, просто определив их как методы
без аргументов. Такие методы отличаются от методов с нулевым количеством аргументов тем, что они не имеют скобок после их имени ни при их определении, ни при их использовании. Наш класс Complex можно переписать так:
class Complex(real: Double, imaginary: Double) {

def re = real
def im = imaginary }
 5.2 Наследование и переопределение
Все классы в Scala наследуются от суперкласса. Когда суперкласс не задан, как в примере с Complex предыдущей главы, неявно используется scala.AnyRef.
Также возможно перекрывать методы, унаследованные от суперкласса в Scala. Тем не менее, необходимо указать, что метод перекрывает другой, используя override модификатор, для того, чтобы предотвратить случайное перекрытие. Например, наш Complex класс может быть доработан переопределением метода toString, унаследованного от Object.

class Complex(real: Double, imaginary: Double) { def re = real
def im = imaginary
override def toString() =

"" + re + (if (im < 0) "" else "+") + im + "I" }
6 Case -классы и поиск по шаблону.
Вид структуры данных которые часто появляются в программах ― это дерево. Например интерпретаторы и компиляторы внутри представляют программы как дерево; XML документы - деревья; несколько видов контейнеров основаны на деревьях таких как красно-чёрные деревья.
Мы рассмотрим как такие деревья представляются и управляются в Scala через простую программу калькулятор. Задача этой программы - управление очень простыми арифметическими выражениями, составленных из сумм целочисленных констант и переменных. Два примера таких выражений: 1+2 и (x + x) + (7 + y).
Сначала мы решим как представить такие выражения. Наиболее естественный способ- дерево, где ноды являются операциями(здесь сложение) и листья значениями(константами и переменные).
В Java такие деревья будут представлены абстрактным суперклассом для деревьев, и одним конкретным классом на лист или вершину. В функциональных языках программирования можно использовать алгебраические типы данных для тех же целей. Scala предоставляет концепцию case-классов. которые есть что-то среднее среднее двумя. Здесь показано как их можно использовать для определения типа деревьев для нашего примера:
abstract class Tree

case class Sum(l: Tree, r: Tree) extends Tree case class Var(n: String) extends Tree
case class Const(v: Int) extends Tree

Факт того что классы Sum, Var и Const объявлены как case-классы значит что они отличаются от стандартных классов в нескольких аспектах:
• ключевое слово new не обязательно для создания экземпляра этих классов (то есть можно писать Const(5) вместо new Const(5)),
• getter функции автоматически определены для параметров конструктора (то есть возможно получить значение параметра конструктора v некоторого экземпляра c класса Const просто написав c.v),

• Даны определения методов по умолчанию для методов equals и hashCode, которые работают со структурой экземпляров, а не с их идентичностью •Дано определени метода по умолчанию toString и печатает значение в "исходной форме" (то есть дерево для выражения x+1 печатается как Sum(Var(x),Const(1)))

... продолжение по запросу