Учебное пособие по Scala. Вольный перевод.


В связи с малым количеством (на момент публикации этого поста) полноценных учебных материалов по языку программирования Scala на русском языке , постарался сделать простой перевод краткого пособия.
Оригинальная статья лежит здесь http://www.scala-lang.org/docu/files/ScalaTutorial.pdf
Корректура текста будет осуществляться после публикации, по ходу получения отзывов, комментариев и повторной вычитки. Возможно, через некоторое время выложу PDF-версию.

Scala Tutorial.

Авторы: Michel Schinz, Philipp Haller. Версия 1.3. 15 марта, 2009

Учебное пособие по Scala. Вольный перевод.

Автор перевода: Вит. 30 марта 2010.

Введение

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

Первый пример

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

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

Структура такой программы должна быть знакома Java-программистам. Она состоит из main-метода, который получает аргументы командной строки в виде массива строк в качестве параметров. Тело метода состоит из единственного метода println, в качестве аргумента которому передаётся «Hello, World!». Метод main ничего не возвращает, поэтому нет необходимости явно указывать тип возвращаемых данных.

Java программиста может смутить в самом начале слово object в котором содержится main метод. Таким образом обозначается то, что обычно называют синглетоном (singleton) — это класс, объект которого может существовать лишь в одном экземпляре. Такое обозначение определяет сразу и класс HelloWorld и объект этого класса, который тоже называется HelloWorld.  Этот объект будет создаваться по первому требованию, в момент первого его использования.

Наверное проницательный читатель заметил, что метод main не объявлен как static. Это потому, что в Scala нет статических членов (методов или полей класса). Вместо определения статических членов, Scala программист объявит их в синглтон объектах.

Компиляция примера

Для того, чтобы скомпилировать пример мы используем Scala компилятор  – scalac. Он работает также как и большинство других компиляторов – в качестве аргументов указываем файлы с исходным кодом (и если нужно некоторые дополнительные параметры) и в результате получаем один или несколько файлов. Созданные файлы являются обычными стандартными java class-файлами.

Если мы сохраним наш пример в файле HelloWorld.scala, то мы сможем скомпилировать его следующей командой:

scalac HelloWorld.scala

В результате будут созданы несколько class-файлов в текущей директории. Один из них будет называть HelloWorld.class и содержать класс, который можно будет использовать через команду scala. Об этом будет написано в следующем разделе.

Запуск примера

Скомпилированная Scala программа может быть запущена использую команду scala. Способ её использования очень похож на команду java,  используемую для запуска Java программ, и принимает такие же  параметры.  Рассмотренный нами выше пример может быть запущен следующей командой:

scala -classpath . HelloWorld

Которая выведет на экран:

HelloWorld!

Взаимодействие с Java

Одна из сильных сторон Scala то, что она может очень легко взаимодействовать с Java-кодом.  Все классы из пакета java.lang уже импортированы неявно (т.е. по умолчанию), а  классы из других пакеты можно импортировать напрямую, явным образом.

Давайте взглянем на следующий пример. Мы хотим получить и отформатировать текущую дату в соответствие с форматом, принятым в некой стране, например во Франции.

Библиотека 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)
	}
}

Выражение import в Scala выглядит очень похожим на аналогичное выражение в Java, тем не менее import Scala более могуществен. Сразу несколько классов может быть импортировано из одного и того же пакета если их обернуть в фигурные скобки так, как показано в первой строке. Другим отличием является то, что если импортируются все содержимое пакета или класса, в Scala используется знак подчеркивание (_) вместо знака звездочка (*).  Это потому, что в Scala звездочка (как мы увидим поздее) является полноценным идентификатором (и например может быть названием метода).

Выражение import на третьей строке таким образом импортирует все члены класса DateFormat. В результате становятся видимыми статический метод getDateInstance и статическое поле LONG.

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

df format now

Это такое же самое, но чуть более простое написания выражения

df.format(now)

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

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

Всё является объектами

Scala – чистый объектно-ориентированный язык в том смысле, что в нем всё является объектами, включая числа или функции. Этим Scala отличается от Java, поскольку в Java разделены примитивные типа (например boolean или int) и ссылочные типы, а также нет возможности работать с функциями как со значением.

Числа – это объекты

Поскольку числа являются объектами, они также имеют методы. На самом деле, следующее арифметическое выражение:

1 + 2 * 3 / x

состоит исключительно из вызовов методов и эквиваленто следующему выражению:

(1).+(((2).*(3))./(x))

Это также означает, что символы +, * – валидные идентификаторы в Scala. Скобки вокруг чисел необходимы, поскольку лексический анализатор Scala использует наиболее длинное правило сопоставления для токенов. То есть разобьёт следующие выражение

1.+(2)

на токены 1., +, и 2. Дело в том, что 1. (цифра один с точкой) более длинное совпадение, чем 1 (просто цифра один) . Токен 1. интерпретируются как литерал 1.0, в результате получается Double вместо Int. Другими словами, если мы запишем выражение как

(1).+(2)

то никто не подумает, что 1 это Double.

Функции – это объекты

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

В качестве простого примера, показывающего чем могут быть полезны функции в качестве значений, давайте рассмотрим функцию-таймер. Её задача будет “делать что-то” каждую секунду. Но как мы можем сказать функции-таймеру, что именно нужно делать? Конечно же передав ей функцию-действие! Этот очень простой способ передачи функции должен быть знаком множеству программистов – он очень часто используется при написании программ с пользовательским интерфейсов, для того чтобы зарегистрировать функцию обработчик, которая бы срабатывала на определенное событие.

В следующей программе, функция-таймер будет называться oncePerSecond и будет получать в качестве аргумента функцию-обработчик. Тип этой функции написан как ()=>Unit, такое обозначение применяется для всех типов функций которые не принимают аргументов и ничего не возвращают (Unit – это аналог void в С/C++). В main-функции этого примера мы вызываем функцию-таймер, передав ей в качестве аргумента функцию, которая выводит строку на экран. Другими словами, эта программа будет до бесконечности каждую секунду печатать фразу “times 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.

Анонимные функции

Теперь мы ещё немножко упростим нашу программу. Во-первых функция timeFlies определяется только для того, чтобы потом быть переданной в функцию oncePerSecond. На самом деле, выделять отдельную функцию с именем timeFlies нам особо смысла нет. Она используется в коде только в одном месте и было бы здорово создать функцию и сразу передать ее в oncePerSecond. Это возможно сделать в Scala используя анонимные функции, которые и являются функциями без имени. Переработанная версия нашей программы-таймера с использование анонимных функций будет выглядеть так:

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.

Классы

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

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

Этот класс может принимать два аргумента, которые являются действительной и мнимой частью комплексного числа. Эти аргументы должны быть переданы при создании объектов класса Complex: new Complex(1.5, 2.3). Этот класс содержит два метода re и im для получения этих двух частей.

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

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

Методы без аргументов

Небольшая проблема методов re и im заключается в том, что для того, чтобы вызвать их необходимо указывать пару пустых скобок после имени метода, как например в следующем примере:

object ComplexNumbers {
    def main(args: Array[String]) {
        val c = new Complex(1.2, 3.4)
        println("imaginary part: " + c.im())
     }
}

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

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

Наследование и переопределение

Все классы в Scala наследуются от суперкласса. Когда суперкласс не указан, как например в предыдущей главе в примере Complex, то будет по-умолчанию использоваться класс scala.AnyRef.

Также можно переопределить (override) метод наследованный в суперклассе. В 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"
}

Кейс-классы и сопоставление с образцом.

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

На примере небольшой программы-калькулятора мы увидим как деревья представлены в Scala и как ими можно манипулировать. Задача этой программы будет работа с очень простыми арифметическими выражениями состоящих из суммы, целых констант и переменных. Вот парочка примеров таких выражений: 1+2 и (x+x) +(7+y).

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

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

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 объявлены как кейс-классы и это значит то, что они отличаются от стандартных классах в следующем:

  • ключевое слово new не обязательно для создания объектов этих классов (можно написать Const(5) вместо new Const(5)),
  • get-функция автоматически определяется для параметров конструктора (таким образом возможно получить значение параметра v некоторого объекта c класса Const просто написав c.v)
  • методы equals и hashCode по умолчанию реализованы таким образом что они работают со структурой объекта, а не с его идентификатором.
  • метод toString определяется по-умолчанию таким образом, что выдает значение в виде «исходного кода» (например выражение x+1 выдаст Sum(Var(x), Const(1))),
  • экземпляры этих классов могут быть про сканированы используя сопоставление по шаблону (мы увидим это далее)

Теперь, так как мы определили типы данных для представления наших арифметических выражений, мы можем начать определять операции для работа с ними. Представим, что наши переменные живут в некоторой среде обитания (enviroment) и мы сейчас приступим к описанию функции, которая вычисляет выражение в именно в этой конкретной среде. Задача среды – дать нам возможность узнать какое конкретно значение имеет переменная в ней. Например, выражение x+1 при вычислении в среде в которой переменной x присвоено значение 5, {x → 5}, даст в результате 6.

Поэтому мы хотим найти способ запрограммировать эту среды обитания наших переменных. Мы можем конечно использовать некоторые ассоциативные структуры данных, такие как хэш-таблица, но гораздо интересней то, что мы можем использовать непосредственно функции! Среда на самом деле ничто иное, как функция которая связывает значение переменной и её имя. Среда {x → 5} приведенное выше может быть легко написано на Scala в виде:

{ case “x” => 5 }

Это обозначение задаёт функцию, которая при передачи x в качестве аргумента возвращает целое число 5 или кидает исключение во всех других случаях.

Прежде чем написать функцию-вычислитель давайте присвоим имя типу среды. Мы можем конечно использовать тип String=>Int для среды, но если мы введём отдельное имя, то это упростит программу и сделает дальнейшие изменения легче. В Scala это делается следующим обозначением:

type Enviroment = String => Int

С этого момента тип Enviroment может быть использован как сокращённое обозначение типа функции из String в Int.

Теперь мы можем дать определении функции-вычислителя. В принципе всё очень просто:

  • значение сумму двух выражений – сумма каждого значения выражения;
  • значение переменной получается напрямую из окружения;
  • и значение константы есть сама по себе константа.

Выразить это на Scala можно так:

def eval(t:Tree, env:Enviroment): Int = t match {
    case Sum(left,right) => eval(left,env) + eval (right, env)
    case Var(n) => env(n)
    case Const(v) => v
}

Функция-вычислитель (eval) работает осуществляя сопоставление по шаблону на дереве t. Поясним приведённый выше код функции:

  1. вначале она проверяет является ли дерево t классом Sum, и если это так, то привязывает левую ветку к переменной left и правую ветку к переменной right, а затем выполняет вычисление выражений следующих за стрелкой; эти выражения могут использовать переменные слева от стрелки (left и right)
  2. если первая проверка не увенчалась успехом, значит дерево не Sum, и она переходит на проверку — «может быть t это Var?» Если это так, то происходит привязка имени содержащегося в узле Var к переменной n и переходит к правой части выражения,
  3. если вторая проверка также провалилась, то есть t это не Sum и не Var, она проверяет «может быть это Const?», и если это так, она привязывает значение содержавшееся в узле Const к переменной v и выполняет правую часть,
  4. в итоге, если все проверки провалились, появиться исключение, что будет являться сигналом что о провале сопоставлении по шаблону. Это может случиться только если еще есть какие-то объявленные, но неучтённые подклассы Tree.

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

Временный пользователь объектно-ориентированного подхода программирования может быть удивлён почему мы не определили eval как метод класса Tree и его подклассов. Действительно, мы могли бы это сделать, так как Scala позволяет определять методы в кейс-классах также, как и в нормальных классах. Принятие решения использовать ли сопоставление по шаблону или методы по большому счету будет является делом вкуса, но при выборе следует учитывать такой фактор как расширяемость:

  • когда используются методы легко добавить новый тип узлов просто определив для этого подкласс Tree. С другой стороны добавление новой операции для всего дерева больно утомительна – требуется изменять все подклассы Tree;
  • когда используется сопоставление по шаблону ситуация обратная: добавление нового типа узла требует изменение всех функций, которые осуществляют сопоставление по шаблонам, для того, чтобы учесть во внимание новый узел; с другой стороны добавление новой операции осуществляется просто добавлением её как новой независимой функцией.

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

  1. производная суммы есть сумма её производных,
  2. производная некоторой переменной v по v равно 1
  3. производная от константы равна 0.

Эти правила могут быть переведены на Scala язык:

def derive(t: Tree, v: String): Tree = t match {
    case Sum(l, r) => Sum(derive(l, v), derive(r, v))
    case Var(n) if (v == n) => Const(1)
    case _ => Const(0)
}

Эта функция определяет два новых понятия связанных с сопоставлением по шаблону. Первое это то, что case – выражение для переменной может иметь «предохранитель» (guard) в виде выражения следующего после ключевого слова if. Этот предохранитель предотвращает сопоставление по шаблону если выражение не true. Здесь он используются для того, чтобы убедиться в том, что мы возвращаем константу 1, только если имя переменной такое же что и имя по которой мы берём производную. Другая используемая здесь новая фишка в сопоставлении по шаблону – использование символа-джокера (wild-card) “_”, который может подходить к любому значению.

Мы не будем сейчас изучать всю мощь сопоставлений по шаблону и остановимся на этой теме прямо сейчас для того, чтобы оставить этот документ краткими. Мы ведь все ещё хотим увидеть как две наши функции будут работать в реальном примере? Для этих целей давайте напишем простую main функцию которая осуществляет несколько простых операций над выражением (x+x)+(7+y): сначала она вычисляет её значение при {x->5, y ->7}, а затем вычисляет её производную по x и по y.

def main(args: Array[String]) {
    val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
    val env: Environment = { case "x" => 5 case "y" => 7 }
    println("Expression: " + exp)
    println("Evaluation with x=5, y=7: " + eval(exp, env))
    println("Derivative relative to x:\n " + derive(exp, "x"))
    println("Derivative relative to y:\n " + derive(exp, "y"))
}

Выполнение этой программы выведет на экран:

Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y)))
Evaluation with x=5, y=7: 24
Derivative relative to x:
Sum(Sum(Const(1),Const(1)),Sum(Const(0),Const(0)))
Derivative relative to y:
Sum(Sum(Const(0),Const(0)),Sum(Const(0),Const(1)))

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

Типаж

Кроме наследования от суперкласса, в Scala классы также могут наследовать код из одного или несколько типажей (traits).

Возможно, наиболее простой способ для понимания типажей Java-программистами это “закрыть глаза и представить, что есть интерфейсы, которые могут содержать методы с кодом…”. Так вот эти сущности и будет аналогом типажей.  В Scala когда класс наследуют trait, он имплементирует интерфейсы типажа и наследует весь код содержащийся в типаже.

Для того, чтобы увидеть пользу типажей, давайте рассмотрим классический пример: упорядоченные объекты. В Java объекты, которые можно сравнивать, реализуют интерфейс Comparable. В Scala мы можем сделать проще чем в Java определив аналог Comparable как типаж, который мы назовём Ord.

Когда объекты сравниваются могут использоваться около шести различных операций сравнения: меньше (<), меньше или равно (<=), равно (=), не равно (!=), больше или равно (>=), больше (>). Тем не менее, определение их всех утомительно, особенно если учесть что четыре из них могут быть определены используя два оставшихся. Таким образом если у нас есть операции равно и меньше, мы можем выразить другие оставшиеся через них. В Scala все это можно легко понять из следующего определения типажа:

trait Ord {
    def < (that: Any): Boolean
    def <=(that: Any): Boolean = (this < that) || (this == that)
    def > (that: Any): Boolean = !(this <= that)
    def >=(that: Any): Boolean = !(this < that)
}

Это определение создаёт сразу новый тип называющийся Ord, которые играет туже роли, что и интерфейс Comparable в Java, и содержит в себе реализацию по умолчанию для трех операций сравнений через четвертый и одну абстрактную. Операция равенства и не равенства здесь не нужны, так как они по умолчанию существуют во всех объектах.

Тип Any который использовался выше в качестве типа, является супер типом для всех типов в Scala. Он может быть рассмотрен как более широкая версия Object в Java, так как он также является супертипом для таких базовых типов как Int, Float и т.д.

Для того чтобы сделать объекты класса сравниваемыми, достаточно определить операции равно и меньше, а затем включить их в класс Ord. В качестве примера давайте определим класс Date который представляет собой дату в григорианском календаре. Такие даты состоят из дня, месяца и года, которые представлены в виде целого числа. Поэтому мы запишем первые строки класса Date следующим образом:

class Date(y: Int, m: Int, d: Int) extends Ord {
    def year = y
    def month = m
    def day = d
    override def toString(): String = year + "" + month + "" + day

Важной частью здесь является определение extends Ord которое следует после имени класса и параметров.  Оно определяет что Date класс наследует типаж Ord.

Затем мы перепишем метод equals, наследованный из Object, для того, чтобы он правильно сравнивал даты на основе их полей. Реализация equals по умолчанию не пригодна для использования, поскольку в Java объекты сравниваются физически (объекты в памяти). Мы приведём следующее определение:

override def equals(that: Any): Boolean =
    that.isInstanceOf[Date] && {
        val o = that.asInstanceOf[Date]
        o.day == day  && o.month == month  && o.year == year
    }

Этот метод использует предопределённый метод isInstanceOf и asInstanceOf. Первый из них (isInstanceOf) соответствует оператору instanceof в Java, и возвращает true в том и только в том случае если объект к которому он применяется является объектом данного типа. Второй (asInstanceOf) соответствует оператору приведения типа в Java: если объект является объектом данного типа, он будет рассмотрен таким образом, иначе будет брошено исключение ClassCastException.

В итоге, последним методом который будет быть определён является операция меньше. Она использует предопределённым методом error, который кидает исключение с со строкой-сообщением.

def < (that: Any): Boolean = {
    if (!that.isInstanceOf[Date])
        error("cannot compare " + that + " and a Date")
    val o = that.asInstanceOf[Date]
    (year < o.year) ||
    (year == o.year && (month < o.month ||
    (month == o.month && day < o.day)))
}

Этим мы завершаем определение класса Date. Экземпляры этого класса могут быть трактоваться как дата и  как сравниваемые объекты. Более того, они определяют шесть предикатов сравнения упомянутых выше: equals и < указаны непосредственно в определение класса Date и другие наследуются от типажа Ord.

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

Обобщения

Последней чертой Scala которую мы рассмотрим в этом учебнике являются обобщения (genericity).

Обобщения – это возможность создавать параметризованный по типам код. Например, программист который пишет библиотеку для связанных списков сталкивается с проблемой «какой тип задать для элементов этого списка?». Поскольку список может быть использован в различных ситуациях, поэтому нельзя сразу указать какие типы будут у его элементов (например все Int-ы). Это может оказаться крайне деспотичным и чрезмерно ограниченным решением.

Java программист прибегает к использованию Object, который является супертипом для всех объектов. Это решение тем не менее далеко от идеального, так как не дает возможность работать с примитивными типами (int, long, float и т.д.) и заставляет вставлять в программный код большое количество операторов динамического приведения типов.

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

class Reference[T] {
    private var contents: T = _
    def set(value: T) { contents = value }
    def get: T = contents
}

Класс Referencе является параметризованным типом, обозначенным T, который является типом этого элемента. Этот тип использует в теле класса как тип переменной contents и аргумент метода set,а также тип возвращаемого значения метода get.

Приведённый выше код вводит переменную в Scala, которая не требует явного преобразования типа в коде. Тем не менее интересно увидеть, что первоначальное значение данной переменной равно _ , что представляет собой значение по умолчанию. Это значение по умолчанию является 0 для численных типов, false для Boolean, () для Unit и null для всех объектных типов.

Для того чтобы использовать Reference класс, необходимо указать, какой тип будет использоваться в качестве параметра T, это и будет типом элемента содержащегося в ячейки. Для примера создадим ячейку содержащую Int. Мы можно написать это в следующем виде:

object IntegerReference {
    def main(args: Array[String]) {
        val cell = new Reference[Int]
        cell.set(13)
        println("Reference contains the half of " + (cell.get * 2))
    }
}

Как мы можем увидеть в этом примере, нет необходимости перед использование приводить тип к Int значение возвращаемое из get-метода. Также не получиться сохранить ничего кроме Int в переменной cell, так как она была объявлена как хранительница только Int‘ов.

Заключение

Этот документ даёт краткое представление о языке Scala и демонстрирует некоторые основные примеры. Любопытный читатель может перейти к чтению «Scala на-примерах» (Scala By Example), который содержит более сложные примеры, а также в случае необходимости обратиться к спецификации языка (Scala Language Specification).

Scala
Любое использование либо копирование материалов или подборки материалов сайта, элементов дизайна и оформления допускается лишь с разрешения правообладателя и только со ссылкой на источник: programador.ru

Телеграм канал: @prgrmdr
Почта для связи: vit [at] programmisty.com