Основы

Интерпретатор

Scala имеет интерпретатор командной строки REPL. По сути он не является интерпретатором, а представляет из себя программу, которая считывает введенные данные, компилирует их в байт-код JVM, выполняет и возвращяет результат. REPL - read-eval-print loop. REPL включается в себя некоторые способности командной строки, например автозавершение команд по нажатию TAB. Чтобы воспользоваться программой небходимо ввести программу scala с командной строки.

Объявление значений и переменных

Если попробовать ввести в REPL число, например, то увидим нечто похожее.

scala> 12
res0: Int = 12

REPL говорит нам, что записал 12 в значение (val) res0. Теперь можно обратиться к данному значению.

scala> res0 + 10
res1: Int = 22

Вместо имен “resN” можно использовать свои имена переменных. Если переменную объявить как val, то переменная будет значением, то есть недоступна для изменения или перезаписи. А вот если объявить как var, то это будет самая настоящая переменная с возможностью перезаписи и изменения. Если переменная была объявлена как ленивая lazy, то вычислена она будет при первом обращении к ней.

scala> val question = "Are you ready?"
question: java.lang.String = Are you ready?

scala> question = "Are you stupid?"
<console>:8: error: reassignment to val
question = "Are you stupid?"
^

scala> var answer = "Yes!"
answer: java.lang.String = Yes!

scala> answer = "No :("
answer: java.lang.String = No :(

Как можно обнаружить REPL отвечает нам как varName: varType, в Scala тип всегда указывается через через двоеточие. Можно всегда указывать явно, хотя в большинстве случаев Scala может вычислить самостоятельно.

var age: Int = 45

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

var heigth, width: Int = 10

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

scala> var `do` = 10
do: Int = 10

Часто используемые типы.

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

scala> var float = 1.toFloat
float: Float = 1.0

Можно выделить 10 “простых” типов в Scala.

  • Byte - целое число от -128 до 127.
  • Short - целое число от -32768 до 32767.
  • Int - целое число от -2147483648 до 2147483647.
  • Long - целое число от -9223372036854775808 до 9223372036854775807. Можно объявить как int с символом l в конце.
  • Float - десятичное число от -3.4028235-e38 до 3.4028235+e38. Можно объявить как int с символом f в конце.
  • Double - десятичное число от -1.7976931348623157+e308 до 1.7976931348623157+e308. Можно объявить как int с символом d в конце.
  • Char - символ, литералами являются одинарные кавычки. Можно создать через код в виде числа int с вызовом toChar. Можно использовать код \uXXXX, \n или \t
  • String - строка, простейшими литералами являются двойные кавычки. Использование:
    • " - можно использовать специальные символы переноса строки и прочих.
    • """ - многострочная строка.
    • s" или s""" - строка, в которой можно подставлять переменные через $varName, или же выражения ${1 + $varName}.
    • f" или f""" - строка, в которой можно подставлять переменные подобно функции printf через $varName%format, или же выражения ${1 + $varName}%format.
    • raw" или raw""" - строка, специальные символы будут отображены. То есть \n не перенесет строку, а так и останется \n.
    • Так же можно реализовать свои специальные литералы через implicit-классы, которые будут использовать StringContext.

Методы в Scala

Scala позволяет использовать стандартные арифметические (+, -, *, /, %) и подразрядные операции (&, |, ^, «, »). Есть лишь один аспект, по сути эти операторы являются методами. Методы в Scala можно вызвать двумя способами.

  • Dot notation - через точку.
  • Operator notation - для методов с одним параметром.
scala> 1.+(2)
res1: Int = 3

scala> 1 + 2
res0: Int = 3

Scala позволяет использовать в качестве имен любые символы, поэтому “+” - это имя метода. Основные правила вызова функций и методов:

  • Если аргумент один, то () можно заменить на {}.
  • Если аргумент один и используется “operator notation”, то () не использовать.
  • Если функция не принимает аргументов, то () можно не использовать. Общие правила здесь таковы: если метод изменяет объект, то скобки все-таки необходимо использовать, иначе не стоит.
  • Если имя метода заканчивается на “:”, то метод правоассоциативен, то есть в “operator notation” параметр будет слева.

Еще один важный момент. В Scala не операторов ++ и –, вместо них используются +=1 и -=1.

Метод apply

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

scala> "Moscow"(4)
res6: Char = o

В действительности это является неявным вызовом метода apply. В частности, определения метода является типичной идиомой при конструировании объекта-компаньона.

scala> "Moscow".apply(4)
res7: Char = o

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

Условные выражения

Синтаксис конструкций if/else if/else аналогичен Java/C++, за одним приятным исключением. Все управляющие конструкции в Scala возвращают значение. Отсюда вытекает, то что выражения имеют тип и могут быть присвоены в переменную.

scala> val x = 1; val y = 2;
x: Int = 1
y: Int = 2

scala> val z = if (x > 1) "Yes" else if (y > 2) "Oh, yea!" else "No :("
z: String = No :(

В иерархии типов Scala общим родительским является тип Any, общим дочерним Nothing, а за тип void отвечает Unit, который имеет значение ().

scala> val z = if (true) 2 else "3"
z: Any = 2

scala> val a = ()
a: Unit = ()

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

scala> val i = if (true) {
  | //doSomething
  | println("haha")
  | }
haha
i: Unit = ()

Ввод и вывод

Для вывода значения используется несколько функций.

  • print - обычный вывод.
  • println - вывод строки.
  • printf - функция из C.
scala> print("Hello")
Hello
scala> println("Hello")
Hello

scala> printf("Hello, %s", "%username%")
Hello, %username%

Для чтения введенных данных используется группа функций read. readLine(str: String) - чтение строки, а readType, где “Type” - имя типа, для чтения параметров определенного типа.

val userName = readLine("Enter your name...")
println(s"Hello, $userName")

println("Enter your age")
val age = readInt()

val result = if (age < 18) "child" else "adult"
printf("You is %s\n", result)
Enter your name...Peter
Hello, Peter
Enter your age
Sorok
java.lang.NumberFormatException: For input string: "Sorok"

Циклы

Самое первое, что необходимо узнать о циклах в Scala, здесь нет инструкций break и continue. Есть альтернативная функция break из пакета scala.util.control.Breaks, но его не рекомендуется использовать из-за скорости выполнения.

В Scala есть стандартные циклы while и do/while.

while (true) {
    // doSomething
}

do {
    //Something
} while (true)

В Scala нет цикла типа for(init;check;update), но есть конструкция for (variable <- expr), это обеспечивает последовательное присвоение в variable итерируемого значения. У объекта RichInt есть два метода, которые возвращают объекты типа Range, являющиеся итерируемыми. Метод n to Nвернет в числа от n до N вклюичтельно, а метод until невключительно.

scala> for (i <- 1 to 5) print(i)
12345

scala> for (i <- 1 until 5) print(i)
1234

Стоит определить термины отностительно циклов for.

  • for (variable <- iterator) - генератор.
  • for (variable <- iterator; variable2 = variable + 10) - определение.
  • for (variable <- iterator if booleanExpression) - ограничитель.

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

scala> for (i <- 1 to 3; j <- 1 to 3) print(i + "-" + j + "\t")
1-1    1-2    1-3    2-1    2-2    2-3    3-1    3-2    3-3

В определениях переменные являются var. Ограничители выглядят как if booleanExpression, итерация запуститься только в случае выполнения условий. Каждый ограничитель следует за генератором, которому он принадлежит, и не должен содержать переменные, находящиеся в объяалении цикла после него.

scala> for (i <- 1 to 3 if i == 2; j <- 1 to 3 if i != j) print(i + "-" + j + "\t")
2-1    2-3

Если тело цикла начинается с ключевого yield, то цикл вернет коллекцию.

scala> for (i <- 1 to 3; j <- 1 to 3) yield i + j
res1: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 3, 4, 3, 4, 5, 4, 5, 6)

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

scala> for {
  | i <- 1 to 3
  | m: Float = i.toFloat / 2
  | if i > 1
  | } {
  | println(m)
  | }
 1.0
 1.5

Функции

В отличии от Java/C++ в Scala есть функции. Функция определяется с помощью ключевого слова def. Функция состоит из имени, параметров и тела, можно указать тип параметра и возвращаемого значения.

def foo(a: String): Unit = {
    println(a(0))
}

Несколько свойств функций.

  • Возвращаемым значением является последнее выражение.
  • Возвращаемое значение можно опустить, но обязательно указывать при рекурсии.
  • Использовать return необходимо только в случаях немедленного выхода из функции.
  • Если функция возвращает значение типа Unit, то функцию называют процедурой, и знак = можно опустить.
def multi25(n: Int): Float = n.toFloat * 2.5

def echo(s: String) {
    print(s)
}

def factorial(n: Int): Int = {
    if (n <= 0) return 1
    n * factorial(n - 1)
}

Аргументам можно задать значение по-умолчанию через =.

scala> def foo(a: String = "ha-ha") = print(a)
foo: (a: String)Unit

scala> foo()
ha-ha

Также при вызове функции можно указывать какие именно параметры передаются.

scala> def bar(a: String = "AAA", b: String = "BBB") = println(a + b)
bar: (a: String, b: String)Unit

scala> bar(b = "CCC")
AAACCC

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

scala> def foo(args: Int*) = println(args.size)
foo: (args: Int*)Unit

scala> foo(10, 11, 45, 90)
4

Внутри функции переменная args будет доступна как последовательность Seq. Можно напрямую в качестве передать коллекцию, но тогда необходимо сообщить компилятору, что это именно последовательность через уточнение типа: _*.

scala> foo(1 to 6: _*)
6

Исключения

Бросить исключение в Scala можно также как и в других языках, то есть throw new Exception. А вот перехват отличается, помимо конструкции try/catch/finally необходимо знать, что catch-блок должен состоять из инструкций pattern matching, о котором будет говориться в следующих главах. Пока достаточно рассмотреть пример и запомнить его синтаксис.

try {
    bar.foo(a)
} catch {
    case _: MalformedException => {
        bar.oof()
    }
    case ex: WrongArgumentException => {
        log(ex)
        bar.oof()
    }
    case _ => {
        log("Something wrong...")
        bar.oof()
    }
} finally {
    bar.close()
}

Первый блок case перехватывает все MalformedException и выполняет блок за =>. Второй case перехватывает все WrongArgumentException и присваивает в переменную ex. А последний case выполняется для всех остальный исключений.

Массивы

Индексные массивы

Массивы бывают фиксированной и переменной длины, они типизированы типом, который их наполняет, тип указывается в квадратных скобках. Массивом фиксированой длины является тип Array. Для инициализации пустого массива определенной длины используется конструкция new Array[T](length: Int). Для инициализации наполненного массива конструкция Array(value1, value2, ... valueN).

scala> new Array[Float](10)
res7: Array[Float] = Array(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)

scala> Array(1.0, 2.5, 3.75, 4)
res8: Array[Double] = Array(1.0, 2.5, 3.75, 4.0)

Для доступа к элементам массива используются круглые скобки.

scala> val a = new Array[Int](5)
a: Array[Int] = Array(0, 0, 0, 0, 0)

scala> a(2) = 10

scala> val aOf2 = a(2)
aOf2: Int = 10

Массивом переменной длины является тип ArrayBuffer из пакета scala.collection.mutable. Для создания пустого массива можно воспользоваться ArrayBuffer[T]() с new или без. Основные методы.

  • +=(n: T) - добавить элемент в конец.
  • ++= - добавить другую коллекцию в конец.
  • trimEnd(n: Int) - удалить n-элементов с конца.
  • insert(position: Int, values: T*) - вставить на позицию position значения values.
  • remove(position: Int, length: Int = 1) - удалить с позиции position значения.
scala> val b = new scala.collection.mutable.ArrayBuffer[Int]()
b: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer()

scala> b += 1
res45: b.type = ArrayBuffer(1)

scala> b(0)
res46: Int = 1

Для конвертации этих массивов используются методы toArray/toBuffer. Обойти массивы можно несколькими способами.

    for (i <- 0 until b.length) println(b(i))

    for (elem <- b) println(b)

    b.foreach(println(_))

    b foreach {
        (elem: T =) => {
        println(elem)
    }
 }

Можно использовать yield для создания новых коллекций. Например, удвоить элементы.

scala> val c = Array(1, 2, 3, 4, 5)
c: Array[Int] = Array(1, 2, 3, 4, 5)

scala> val doubleC = for (elem <- c) yield elem * 2
doubleC: Array[Int] = Array(2, 4, 6, 8, 10)

Для подобных типичный операций были предусмотрены методы.

  • sum - вернет сумму элементов.
  • size/length - вернет количество элементов.
  • max - вернет больший элемент.
  • min - вернет меньший элемент.
  • mkString(delimiter: String) - вернет строку с элементами и разделителем.

Создать матрицу можно с помощью метода Array.ofDim[T](row: Int, col: Int).

scala> Array.ofDim[Double](3, 3)
res57: Array[Array[Double]] = Array(Array(0.0, 0.0, 0.0), Array(0.0, 0.0, 0.0), Array(0.0, 0.0, 0.0))

Ассоциативные массивы

Ассоциативные массивы в Scala представлены классами Map[KeyType, ValueType] для неизменяемых массивов, и scala.collection.mutable.Map[KeyType, ValueType] для изменяемых. Ассоциативные массивы представляют из себя коллекцию пар. Пару можно создать через (Value1, Value2) или Value1 -> Value2.

scala> "age" -> 42
res2: (String, Int) = (age,42)

scala> ("age", 42)
res3: (String, Int) = (age,42)

Неизменяемые массивы можно создать только одним способом, через вызов apply объекта-компаньона.

scala> val m = Map[Int, Int](1 -> 2, 3 -> 4)
m: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2, 3 -> 4)

По-аналогии можно создать наполненный изменяемый массив, а чтобы создать пустой необходимо воспользоваться классом scala.collection.mutable.HashMap.

scala> val m = new scala.collection.mutable.HashMap[String, Int]
m: scala.collection.mutable.HashMap[String,Int] = Map()

Доступ к элементам осуществляется также, как и в индексных массивах.

scala> val m = Map('e' -> 45, 'y' -> 66)
m: scala.collection.immutable.Map[Char,Int] = Map(e -> 45, y -> 66)

scala> m('e')
res5: Int = 45

Проверить существование ключа можно с помощью метода contains(key: T). И чтобы избавиться от постоянных проверок на существование был добавлен метод getOrElse(key: T, defaultValue: T2)

scala> m.contains('t')
res10: Boolean = false

scala> m.getOrElse('t', 1)
res11: Int = 1

У ассоциативных массивов есть метод get(key: T), который возвращает объект типа Option, который представлен типами Some(Type) и None. Так, если ключ не будет найден, то метод get вернет None, иначе Some(value).

scala> m.get('e')
res8: Option[Int] = Some(45)

scala> m.get('t')
res9: Option[Int] = None

Изменять значения можно только в изменяемых ассоциативных массивах.

scala> val m = scala.collection.mutable.Map('a' -> 10, 'b' -> 20)
m: scala.collection.mutable.Map[Char,Int] = Map(b -> 20, a -> 10)

scala> m('b') = 21

scala> m
res17: scala.collection.mutable.Map[Char,Int] = Map(b -> 21, a -> 10)

scala> m += ('c' -> 31)
res18: m.type = Map(b -> 21, a -> 10, c -> 31)

scala> m -= ('b')
res19: m.type = Map(a -> 10, c -> 31)

scala> m
res20: scala.collection.mutable.Map[Char,Int] = Map(a -> 10, c -> 31)

Несколько методов для изменяемых массивов.

  • += - добавить одну или несколько пар к массиву.
  • -= - удалить пару из массива.

Чтобы обойти ассоциативные массивы необходимо использовать цикл for и pattern matching.

scala> for ((key, value) <- m) println(key + " : " + value)
a : 10
c : 31

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

Кортежи

Пара - это простейший случай кортежа. Кортеж (Tuple) создается с помощью круглых скобок, а его типом будет TupleN[Type1, Type2 ... TypeN].

scala> Tuple3(1, 'e', true)
res22: (Int, Char, Boolean) = (1,e,true)

scala> def foo(a: Tuple4[Int, Int, Char, Boolean]) = println("YES")
foo: (a: (Int, Int, Char, Boolean))Unit

scala> foo(Tuple4(1,2,'r',false))
YES

scala> foo((1, 2, 'w', true))
YES

Обратиться к элементам кортежа можно с помощью методов _n, где n - номер элемента в кортеже начиная с 1.

scala> val t = (1, true, 'F', "Igor")
t: (Int, Boolean, Char, String) = (1,true,F,Igor)

scala> t._4
res27: String = Igor

Также удобно использовать сопоставление с образцом.

scala> val (id, isMan, clas, name) = t
id: Int = 1
isMan: Boolean = true
clas: Char = F
name: String = Igor

scala> val (id, isMan, _, name) = t
id: Int = 1
isMan: Boolean = true
name: String = Igor

Например, у индексного массива, есть метод zip(another: Array[T]), который вернет массив пар.

scala> val a = Array(1, 2, 3, 4, 7)
a: Array[Int] = Array(1, 2, 3, 4, 7)

scala> val a1 = Array('e', 't', 'b', 'Q', '4')
a1: Array[Char] = Array(e, t, b, Q, 4)

scala> a.zip(a1)
res28: Array[(Int, Char)] = Array((1,e), (2,t), (3,b), (4,Q), (7,4))

Классы

Объявление классов

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

class Person {
    val age = 10

    def say(phrase: String) {
        print(phrase)
    }

    def sayHi() {
        say("Hi\n")
    }
}

Для создания экземпляра класса используется ключевое слово new. По-умолчанию все свойства и методы являются публичными, а ключевого слова public нет в принципе. Все поля должны быть инициализированы. Обратиться к свойству или методу можно через точку, а к своим методам изнутри как с использованием this, как и напрямую как функции или переменной.

scala> val john = new Person()
john: Person = Person@1c48b34d

scala> john.sayHi
Hi

scala> john.sayHi()
Hi

Если метод не принимает параметров, то скобки вызова можно опустить. Есть негласное правило: “Если метод изменяет внутреннее состояние объекта (метод-мутатор), то скобки указывать необходимо, а для методов-акцессоров необходимо опустить”. Можно обязать вызывать метод без скобок, если не указать их при объявлении.

def sayHi {
    say("Hi\n")
}

Методы доступа

Свойства и методы можно сделать недоступными из вне, для этого их необходимо сделать приватными с помощью ключевого слова private или защищенными (доступными в наследниках класса) protected. При этом можно оставить доступ через методы доступа. При объявлении свойства публичным Scala неявно создает два метода valName - getter и valName_=(value: Type) - setter. В этом можно убедиться, если скомпилировать класс через scalac и посмотреть байт код через javap -private.

bash$ scalac Person.scala
bash$
bash$ javap -private Person.class
Compiled from "Person.scala"
public class Person {
    private final int age;
    public int age();
    public Person();
}

Эти методы можно переопределить.

class Person {
    private var privateAge: Int = 10
    def age = privateAge
    def age_=(value: Int) = privateAge = value
}

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

  • Если поле private, то и методы private.
  • Если поле val, то генерируется только getter.
  • Если поле private[this], то методы не генерируются вообще.

Когда поле объявляется как private, то поле будет доступно только внутри класса. Именно класса, то есть экземпляры одного класса будет иметь доступ к приватным методам и свойствам друг друга. Чтобы ограничить видимость внутри экземпляра, следует объявить метод или свойство как private[this].

class Person {
    private[this] var name: String = "John"
    private val age = scala.util.Random.nextInt(100)

    def name_=(value: String) = name = value

    def age_?(person: Person) = person.age
    //def name_?(person: Person) = person.name
}

Последний метод name_? становится не валидным в данном случае. В квадратных скобках также можно указать на внешний класс, если речь идет о вложенных классах. Классическим методом именования методов доступа являются имена setXxx и getXxx, чтобы добавить поддержку необходимо перед именем свойства объявить аннотацию @BeanProperty из пакета scala.beans. Правила генерации аналогичны вышесказанным.

Конструкторы

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

class Person {
    var name: String = ""
    var age: Int = 0

    def this(name: String) = {
        this()
        this.name = name
    }

    def this(name: String, age: Int) = {
        this(name)
        this.age = age
    }
}

Главый конструктор вплетается в определение класса. Его параметры следуют сразу за именем класса, причем если аргумент объявлен как (private) val/var, то он автоматически становиться полем класса. Также при конструировании выполняются все инструкции внутри класса.

class Person(name: String, val age: Int, private val weight: Int) {
    println("Hello, I'm " + name)

    val names = scala.collection.mutable.ArrayBuffer[String](name)
}

Если аргумент используется хотя бы в одном методе, то он становится private[this] свойством. Можно объявить главный конструктор как private, тогда клиент будет обязан использовать дополнительные конструкторы.

Вложенные классы

Scala позволяет вкладывать один класс в другой.

class Group {
    class Member(val name: String) {
        def sayHi = println("Hello, I'm " + name + ". And I'm alcoholic.")
    }
}

Создать внутренний класс можно через new variable.InnerClass. Для каждого объекта вложенный объект будет иметь свой тип (varName.InnerClass).

scala> val alco1 = new Group
alco1: Group = Group@7e8958a

scala> val john = new alco1.Member("John")
john: alco1.Member = Group$Member@19257c13

scala> val alco2 = new Group
alco2: Group = Group@53b1d6ee

scala> val peter = new alco2.Member("Peter")
peter: alco2.Member = Group$Member@5b41efb

scala> val container = scala.collection.mutable.ArrayBuffer(peter)
container: scala.collection.mutable.ArrayBuffer[alco2.Member] = ArrayBuffer(Group$Member@5b41efb)

scala> container += john
<console>:17: error: type mismatch;
found   : alco1.Member
required: alco2.Member
    container += john
                 ^

Чтобы обойти это можно использовать проекцию типов, определяя тип как Outer#Inner. Тогда внутренний тип будет определен, как конкретный тип любого внешнего типа.

scala> val container = scala.collection.mutable.ArrayBuffer[Group#Member](peter)
container: scala.collection.mutable.ArrayBuffer[Group#Member] = ArrayBuffer(Group$Member@5b41efb)

scala> container += john
res4: container.type = ArrayBuffer(Group$Member@5b41efb, Group$Member@19257c13)

Чтобы обратиться к вышестоящему классу необходимо воспользоваться определением собственного типа.

class Group(val name: String) { outer =>
    class Member(val name: String) {
        def sayHi = println("Hello, I'm " + name + ". And I'm alcoholic. #" + outer.name)
    }
}
scala> val g = new Group("TT")
g: Group = Group@98d1a14

scala> new g.Member("Peter")
res5: g.Member = Group$Member@1006e28f

scala> res5.sayHi
Hello, I'm Peter. And I'm alcoholic. #TT

Объекты

Singleton

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

object God {
    def createHuman = println("Ha-ha-ha! Only I can do humans!")
    def doApocalypse = scala.sys.exit(Int.MaxValue)
}
scala> God.createHuman
Ha-ha-ha! Only I can do humans!

scala> God.doApocalypse
bash$ echo $?
255

Объект является реализацией шаблона проектирования Singleton, он может наследовать другие классы и трейты, но не может иметь наследников. Когда объект имеет одноименный класс, то он является объектом-компаньоном. Такие сущности должны определяться в одном файле, они имеют доступ к приватным методам и свойствам друг друга.

import scala.beans.BeanProperty

case class Kelvin(val value: BigInt)
case class SolarRadius(val value: Double)

class Star(val name: String, @BeanProperty val temperature: Kelvin, @BeanProperty val radius: SolarRadius) {
    def getLuminosity: Double =
        4 *
            scala.math.Pi *
            scala.math.pow(radius.value, 2) *
            Star.STEFAN_BOLTZMANN_CONST *
            scala.math.pow(temperature.value.toDouble, 4)
}

object Star {
    private val STEFAN_BOLTZMANN_CONST: Double = 5.67036713e-8
}
scala> val rigel = new Star("Rigel", Kelvin(12130), SolarRadius(74))
rigel: Star = Star@5d491a2b

scala> rigel.getLuminosity
res0: Double = 8.447489980015033E13

Очень часто объект компаньон содержит в себе метод apply, создающий экземпляр класса-компаньона.

    def apply(name: String, temperature: Kelvin, radius: SolarRadius): Star = new Star(name, temperature, radius)

Объект, представляющий приложение

Выполнение каждой программы начинается с вызова метода main(args: Array[String]) на объекте представляющего приложение.

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

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

object Hello extends App {
    println("Hello!")
    println("App runing with args: " + args)
}

Enumeration

Чтобы создать перечисление необходимо подмешать трейт Enumeration к объекту. Каждое значение объявляется как val Name = Value. Каждое значение константы представляет из себя тип EnumName.Value. Константы создаются с помощью метода Value также.

object Color extends Enumeration {
    val Red, Green, Yellow = Value
}

Это вызывает путаницу, но все-таки имя Value имеет значения:

  • Метод трейта Enumeration, которы принимает id и/или имя значения enum’а.
  • Внутренний класс Enumeration.

Как было упомянуто метод Value может принимать Int как идентификатор (если не передано, то инкрементируется), или имя (если не передано, то совпадает с именем свойства), или вообще ничего.

object Color extends Enumeration {
    val Red = Value(1)
    val Green = Value("Green")
    val Yellow = Value(3, "Yellow")
}

В вышестоящем примере типом цвета будет Color.Value. При передаче в качестве параметра следует использовать именно его.

def whichColor(color: Color.Value) = println(color)

Можно импортировать значения Enum и использовать их явно.

import Color._

println(Red)

Также можно указать псевдоним типа на Enumerator как Value и упростить именование. В примере ниже тип каждого значения совпадает с именем объекта.

object Color extends Enumeration {
    type Color = Value
    val Red, Green, Yellow = Value
}

import Color._

def whichColor(color: Color) = println(color)

У класса Value есть два метода. Метод id вернет числовое значение, а метод toString имя. У объекта-наследника Enumeration есть методы values - вернет множество всех значений, apply(id: Int) - вернет Value по id, withName(name: String) - вернет Value по имени.

Пакеты

Пакеты

Пакеты применяются для управления именами, так существует класс Map из пакетов scala.collection.immutable и scala.collection.mutable. Чтобы добавить элемент в пакет, необходимо объявить его внутри пакета. Объявить пакет можно с помощью ключевого слова package.

package com {
    package google {
        class Android
    }
}

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

package com {
    package google {
        object Android {
            def name = "Android"
        }

        package program {
            class Gmail {
                def mobileName = Android.names + " Gmail"
            }
        }
    }
}

Любой пакет неявно имеет корень _root_, можно начинать создание экземплара класса с _root_. Объявление package может содержать цепочку пакетов.

package com.google {
    class Android

    package program {
        class Gmail
    }
}

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

package com.google
package program

class Gmail

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

package com {
    package object google {
        def getName = "Google"
    }

    package google {
        class Android {
            def producer = getName
        }
    }
}

Приватные и защищенные методы можно ограничить областью видимости внутри пакета с помощью квалификатора private[packageName]. В этом случае методы и свойства будут доступны везде внутри пакета.

package com.google {
    object Android {
        private[google] def version = 4.1.1
    }

    class Android(val version: random.pack.Version)

    class Gmail {
        def checkVersion(android: Android) = android.version.gtThen(Android.version)
    }
}

Импортирование

С помощью импортирования можно использовать короткие имена вместо длинных.

import com.google.Android
import random.pack.Version

val android = new Android(Version("4.2.1"))

Можно испортировать все члены пакета, класса или объекта с помощью символа _.

import com.google._
import random.pack.Version

val android = new Android(Version("4.2.1"))
val gmail = new Gmail

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

import scala.collection.mutable.{Map, ArrayBuffer}

Можно дать псевдоним с помощью конструкции {originalName => aliasName}.

import scala.collection.mutable.{Map => MutableMap, ArrayBuffer}

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

import java.lang._
import scala._
import Predef._

Пакету scala даны особые права на переопределение имен из пакета java.lang. Predef содержит много полезных функций, хотя их можно было поместить в пакет scala, но пакет Predef появился раньше исторически.

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

Классы

Отнаследовать класс можно с помощью ключевого слова extends. Класс-наследник может обратиться к родительским методам, не помеченным как private.

class Human {
    def jump = moveTheBones

    private[this] def moveTheBones = ()
}

class Woman extends Human

При переопределении методов обязательно исользовать ключевое слово override.

class Woman extends Human {
    override def jump = println("Jump up boobs!")
}

Если метод помечен как final, то его нельзя переопределить. Обратиться к родительскому методу можно с помощью слова super.

class Woman extends Human {
    override def jump = println("Jump up boobs!") + super.jump
}

Проверка и приведение типов

Чтобы проверить экземпляр класса на принадлежность к типу необходимо использовать метод isInstanceOf[Type]: Boolean. Класс наследник является типом класса-родителя.

scala> class A
defined class A

scala> class B extends A
defined class B

scala> val b = new B
b: B = B@44cf2e58

scala> b.isInstanceOf[A]
res0: Boolean = true

Чтобы привести класс к типу необходимо воспользоваться методом asInstanceOf[Type]: Type.

scala> b.asInstanceOf[A]
res1: A = B@44cf2e58

Чтобы проверить на принадлежность к определенному классу без учета родителей необходимо воспользоваться методом getClass.

scala> b.getClass
res2: Class[_ <: B] = class B

И методом объекта Predef classOf[Type].

scala> classOf[B]
res4: Class[B] = class B

scala> b.getClass == classOf[B]
res5: Boolean = true

Конструкторы и переопределение полей

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

class Person(val name: String)

class Employee(override val name: String, val salary: Int) extends Person(name)

class Robot(val id: Int) extends Person("#" + id)

В подклассе можно переопределить поля и методы суперкласса.

  • def может переопределить def.
  • val может переопределить val и def без параметров.
  • var может переопределить абстрактное var.
class MilitaryRobot(private[this] val uniqueKey: Int) extends Robot(uniqueKey) {
    override val id = Int.MinValue
    override val name = ""
    private def key = java.security.MessageDigest.getInstance("MD5").digest(uniqueKey.toString.getBytes)
}

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

val speaker = new Person("Andy") {
    def speak(phrase: String) {
        println(phrase + "... hm-hm")
    }

    def shutUp = println("NO!")
}

В данном случае его тип будет Person{def speak(phrase: String): Unit; def shutUp: Unit}. Это будет объект структурного типа и его можно передать в качестве параметра.

def inviteSpeaker(speaker: Person{def speak(phrase: String): Unit}) = speaker.speak("Hello everyone!")

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

class Prize {
    val bonusCount: Int = 10

    val bonuses: Array[String] = {
        val seq = for (i <- 1 to bonusCount) yield getBonusId
        seq.toArray
    }

    final def getBonusId: String = util.Random.alphanumeric.take(10).mkString
 }

class SuperPrize extends Prize {
    override val bonusCount: Int = 1
}
scala> val prize = new Prize
prize: Prize = Prize@63bbc6d0

scala> val superPrize = new SuperPrize
superPrize: SuperPrize = SuperPrize@7db33444

scala> prize.bonuses
res13: Array[String] = Array(6pXtkN36s9, HTfLvz9cgA, kOYQTTDmL4, jwChw55tEs, cvsKGNrkGb, 0vuBzPDdaG, Jv8kJdj33H, uC1XEYPdF5, rsKdv9AKpr, cRNJtgZGXc)

scala> superPrize.bonuses
res14: Array[String] = Array()

При вызове родительского конструктора в строке “val seq = for (i <- 1 to bonusCount) yield getBonusId” вызывается метод чтения “bonusCount”, но он переопределен в наследнике, при этом конструктор подкласса еще не был вызван, чтобы выполнить “override val bonusCount: Int = 1” поэтому поле было определено начальным значением, что для Int равно 0. Есть несколько способов обойти это ограничение. Сделать val финальным, ленивым или использовать опережающее определение. Его смысл в том, что когда будет обращение к переопределенному методу, то он уже будет реализован.

class SuperPrize extends {
    override val bonusCount: Int = 1
} with Prize

Абстрактные классы

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

abstract class Person(val name: String) {
    def id: Int
}

При наследовании абстрактного класса override для абстрактного метода или поля писать не нужно.

class Woman(name: Name) extends Person(name) {
    def id: Int = 1
}

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

abstract class Person {
    val name: String
}

val john = new Person {
    val name = "John"
}

Иерархия классов

Базовым классом, то есть общим суперклассом, для всех классов в Scala является класс Any. Он определяет методы isInstanceOf и asInstanceOf, а также методы сравнения и вычисления хэш-кода. У него есть два прямых наследника - AnyVal для типов-значений вроде Int, Boolean, Double или Unit; AnyRef для всех остальных. С противоположной стороны иерархии стоят типы Nothing и Null для AnyRef.

У класса AnyRef есть метод eq(obj: AnyRef), который проверяет, что объекты ссылаются на одну и ту же область в памяти. Метод equals вызывает на eq. Чтобы организовать сравнение объектов по своему усмотрению, необходимо переопределить метод equals(other: Any), а также метод hashCode.

Работа с файлами

Чтение

Прочитать файл можно с помощью объекта Source из пакета scala.io.

  • fromFile(filefame: String, charset: String): scala.io.BufferedSource - вернет итератор с символами из файла.
  • fromURL(filefame: String, charset: String): scala.io.BufferedSource - вернет итератор с символами по ссылке.
  • fromString(string: String): scala.io.BufferedSource - вернет итератор с символами из строки.
  • stdin: scala.io.BufferedSource - вернет итератор из стандартного ввода.
  • close - заканчивает работу с ресурсом (закрывает файл).

Над полученным объектом scala.io.BufferedSource можно проводить раличные действия.

  • getLines: Iterator[String] - вернет построчный итератор, который, например, можно привести к массиву методом toArray.
  • mkString: String - вернет весь итератор в качестве строки.
  • buffered: scala.collection.BufferedIterator[Char] - вернет итератор с символами, не перемещая указателя.
scala> import scala.io.Source
import scala.io.Source

scala> val source = Source.fromFile("content.txt", "UTF-8")
source: scala.io.BufferedSource = non-empty iterator

scala> for (i <- source.getLines) i.split(";").map { print(_)}
RU4111110002123US4000001111456UA4000000010999

Scala не имеет встроенной поддержки работы с бинарными данными, для этого следует исследовать Java инструменты.

Запись

Для записи в файл используется класс java.io.PrintWriter(filename: String).

  • print(str: String) - запишет строку в файл без переноса строки.
  • println(str: String) - запишет строку в файл c переноса строки.
  • printf(pattern: String, values: AnyRef*) - запишет строку в файл подобно функции printf, но требует именно типа AnyRef.
  • close - заканчивает работу с ресурсом (закрывает файл).
scala> import java.io.PrintWriter
import java.io.PrintWriter

scala> val names = new PrintWriter("names.txt")
names: java.io.PrintWriter = java.io.PrintWriter@3212a8d7

scala> names.print("Ann")

scala> names.println("John")

scala> names.println("Bob")

scala> names.close()

scala> scala.io.Source.fromFile("names.txt", "UTF-8").mkString
res7: String =
"AnnJohn
Bob
"

Взаимодействие с файлами системы

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

scala> case class %%(min: Int, max: Int) extends Serializable
defined class $percent$percent

scala> val zero100 = %%(0,100)
zero100: %% = %%(0,100)

scala> import java.io._
import java.io._

scala> val out = new ObjectOutputStream(new FileOutputStream("zero100.txt"))
out: java.io.ObjectOutputStream = java.io.ObjectOutputStream@5fb7183b

scala> out.writeObject(zero100)

scala> out.close()

scala> val in = new ObjectInputStream(new FileInputStream("zero100.txt"))
in: java.io.ObjectInputStream = java.io.ObjectInputStream@33b082c5

scala> in.readObject().asInstanceOf[%%]
res15: %% = %%(0,100)

Взаимодействие с командной оболочкой

Для взаимодействия с командной оболочкой следует использовать пакет scala.sys.process. Он содержит неявное преобразование строк в ProcessBuilder.

  • !: Int - вернет код завершения команды.
  • !!: String - вернет результат завершения команды.
  • #[bash operator] - использовать операраторы консоли (>, >>, && и прочие).
scala> import scala.sys.process._
import scala.sys.process._

scala> "ls -la" !!
warning: there was one feature warning; re-run with -feature for details
res27: String =
"total 24
drwxr-xr-x  5 zinvapel  staff  170  9 мар 19:22 .
drwxr-xr-x  3 zinvapel  staff  102  9 мар 18:30 ..
-rw-r--r--  1 zinvapel  staff   60  9 мар 18:32 content.txt
-rw-r--r--  1 zinvapel  staff   12  9 мар 19:10 names.txt
-rw-r--r--  1 zinvapel  staff   79  9 мар 19:23 zero100.txt
"

scala> "ls -la" #| "grep names" !!
warning: there was one feature warning; re-run with -feature for details
res28: String =
"-rw-r--r--  1 zinvapel  staff   12  9 мар 19:10 names.txt
"

Регулярные выражения

За регулярные выражения отвечает класс scala.util.matching.Regex. Получить экземпляр можно просто вызвав метод r: scala.util.matching.Regex на строке.

  • findAllIn(string: String): scala.util.matching.Regex.MatchIterator - вернет итератор со всеми совпадениями.
  • findFirstIn(string: String): Option[String] - вернет первое совпадение.
  • findPrefixOf(string: String): Option[String] - вернет первое совпадение, если оно в начале сроки поиска.
  • replaceAllIn(string: String, replacement: String): String - заменит все совпадения.
  • replaceFirstIn(string: String, replacement: String): String - заменит первое совпадение.

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

scala> val ageName = "([0-9]+) ([a-zA-Z]+)".r
ageName: scala.util.matching.Regex = ([0-9]+) ([a-zA-Z]+)

scala> val ageName(age, name) = "24 Ann"
age: String = 24
name: String = Ann

Трейты

Трейты как интерфейсы

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

trait Breakable {
    def break: Unit
}

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

trait Jumpable {
    def jump: Int
}

trait Runnable {
    def run: Int
}

class Person extends Jumpable with Runnable {
    def jump: Int = { println("jump"); 0 }
    def run: Int = { println("run"); 0 }
}

Все интерфейсы Java можно использовать как трейты.

Трейты с конкретными реализациями

Трейты могут содержать конкретную реализацию.

trait Jumpable {
    def jump: Int = { println("jump"); 5 }
}

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

trait Jumpable {
    def jump: Int = 0
}

trait HighJumpable extends Jumpable {
    override def jump: Int = 10
}

trait Affable {
    def sayHi: Unit
}

class Human extends Affable with Jumpable  {
    def sayHi: Unit = println("Hi! I can jump on " + jump + " meteres")
}
scala> val ann = new Human
ann: Human = Human@1e08f0cd

scala> ann.sayHi
Hi! I can jump on 0 meteres

scala> val jack = new Human with HighJumpable
jack: Human with HighJumpable = $anon$1@dc72335

scala> jack.sayHi
Hi! I can jump on 10 meteres

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

trait Sounding {
    def beep: Unit = println("beeeeep")
}

trait StarSounding extends Sounding {
    override def beep: Unit = { println("`"); super.beep }
}

trait LineSounding extends Sounding {
    override def beep: Unit = { println("--"); super.beep }
}
scala> val beep = new {} with LineSounding with StarSounding
beep: LineSounding with StarSounding = $anon$1@2900553d

scala> beep.beep
`
--
beeeeep

scala> val beep = new {} with StarSounding with LineSounding
beep: StarSounding with LineSounding = $anon$1@24c186cc

scala> beep.beep
--
`
beeeeep

При этом если вызов super ссылается на абстрактный метод, то компилятор вызовет ошибку, что логично, абстрактный метод не имеет реализации.

trait Sounding {
    def beep: Unit
}

trait StarSounding extends Sounding {
    override def beep: Unit = println("`")
}

trait LineSounding extends Sounding {
    override def beep: Unit = { println("--"); super.beep } // Здесь возникнет ошибка
}

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

trait LineSounding extends Sounding {
    abstract override def beep: Unit = { println("--"); super.beep }
}

Трейт может содержать одновременно и абстрактные методы и конкретные реализации.

Поля в трейтах

Как уже было сказано, в отличие от Java трейты Scala могут иметь поля, причем как абстрактные, так и конкретные. Абстрактным полем является поле без начального значения.

trait A {
    val a: Int // Абстрактное поле
}

trait B {
    val b: Int = 10 // Конкретное поле
}

Как и везде, при реализации абстрактного поля ключевое слово override не требуется, а при переопределении конкретного поля оно нужно.

Конструирование трейтов

Как и классы, трейты имеют конструктор. Но при этом в трейтах конструктор только один - главный конструктор, не имеющий конструктор. Порядок конструирования трейтов.

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

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

trait Singer {
    val octave: Int
    val max: Int = octave + 1
}

class Person(val name: String)

val baskov = new Person("Kolya") with Singer {
    val octave = 5
}

println(baskov.max) //Выведет 1

Это происходит потому что фактически создается анонимный подкласс, а класс Person with Singer становится супер классом. При инициализации Singer.max в поле octave еще нет значения. Можно использовать опережающее определение для этого случая.

val kolyabascov = new {
    val octave = 5
} with Person("Kolya Baskov") with Singer

println(kolyabascov.max) //Выведет 6

Наследование классов

Трейты могут наследовать классы. При этом суперкласс трейта становится суперклассом класса, подмешающего трейт. При этом класс может иметь свой суперкласс, но это должен быть тот же суперкласс, что и у трейта или же его наследник. Трейт гарантирует, что класс, куда он будет подмешан будет определенного типа. Существует иной способ гарантировать это. Использовать в трейте определение собственного типа. Для этого следует начать объявление трейта с конструкции this: Type =>.

trait A {
    this: Exception =>
}

Здесь удобно будет использовать структурные типы, которые просто описывают методы, которые должны быть у класса.

trait A {
    this: { def foo(bar: Int): Float } =>
}

Операторы

Инфиксные операторы

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

scala> val `for` = (1,2,3,4,5)
for: (Int, Int, Int, Int, Int) = (1,2,3,4,5)

scala> val `for` = Array(1,2,3,4,5)
for: Array[Int] = Array(1, 2, 3, 4, 5)

scala> for (i <- `for`) println(i)
1
2
3
4
5

Выражение называется инфиксным, если оператор находится между двумя аргументами. Методы, которые принимают один аргументы можно записывать в инфиксной форме. То есть использовать вместо a.op(b) - a op b

class A {
    def and(b: A) = Some(10)
}

new A and new A

Унарные операторы

Оператор называют унарным, если он принимает один параметр. Все методы, которые не принимают параметров являются постфиксными унарными операторами. Префиксных унарных операторов всего 4: +, -, !, ~, а именами методов для них является unary_OPERATOR.

Приоритет операторов

Инфиксные операторы более приоритены, чем постфиксные. Приоритет операторов, начинающихся с символов таков.

  • Все символы, отличные от менее приоритетных.
    • / %
    • -
  • :
  • <

  • ! =
  • &
  • Операторы присвоения. a op= b (a = a op b)

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

class A {
def ten_plus_:(b: Int) = 10 + b
}

90 ten_plus_: new A // Выведет 100

Apply, update и unapply

При попытки вызвать не функцию вызывается метод apply, а при попытке приравнять такое выражение к чему-либо вызывается метод update.

class A {
    def apply(arg: Int) = println(arg)
    def update(arg: Int, value: Int) = println("Update with " + arg + " and " + value)
}

val a = new A
a(10) // 10
a(7) = 10 // Update with 7 and 10

Можно определить сколько угодно различных методов apply с различными сигнатурами. Объект с методом unapply называется экстракторами. Unapply является противоположностью метода apply, принимает в качестве аргумента объект некоторого типа, а возвращает набор значений. Используется при объявлении переменных и при сопоставлении с образцом. Резутат следует обернуть в тип Option, тогда в случае успеха (тип Some) сработает совпадение с образцом. При этом есть возможность воспользоваться wildcard для извлечения только конкретных значений.

class MyInt(val i: Int)

object MyInt {
    def unapply(mi: MyInt) = if (mi.i > 10) Some(mi.i) else None
}
scala> val MyInt(a) = new MyInt(9)
scala.MatchError: MyInt@40f1be1b (of class MyInt)
... 33 elided

scala> val MyInt(a) = new MyInt(90)
a: Int = 90

scala> case class A(s: String, i: Int)
defined class A

scala> val A(name, _) = A("ololo", 10)
name: String = ololo

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

  • unapply(object: S): Option[T] - при совпадении с образцом S(t).
  • unapply(object: S): Option[(T1, T2 .. Tn)] - при совпадении с образцом S(t1, t2 .. tn). - unapply(object: S): Boolean - при совпадении с образцом S().
class A(val j: Int)

object A {
    def unapply(a: A) = if (a.j > 0) true else false
}

new A(2) match {
    case a @ A() => println("A")
}

Экстракторы могут извлекать и произвольное число значений. Если необходимо определить экстрактор, который принимает объекты определённого типа и возвращает коллекцию значений, длина которой неизвестна на этапе компиляции, то следует воспользоваться методом unapplySeq(object: S): Option[Seq[T]].

case class Name(value: String)

class Person(val name: Name)

object Person {
    def unapplySeq(p: Person): Option[Seq[String]] = Some(p.name.value.trim.split(" "))
}

val marquez = new Person(Name("Габриэль Хосе де ла Конкордиа «Габо» Гарсиа Маркес"))

marquez match {
    case Person(firstName, lastName @ _*) => println(firstName + " " + lastName.last)
}

Функции высшего порядка

Функции

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

scala> def foo(a: Int) = a * 10
foo: (a: Int)Int

scala> val f = foo _
f: Int => Int = <function1>

Функции необязательно давать имена, тогда функция называется анонимной. Она объявляется по форме.

  • (arg1: Type1, arg2: Type2): ReturnType => Body - в общем виде.
  • (arg1, arg2): ReturnType => Body - если не требуется уточнение типа, например функция принимается в качестве параметра.
  • (arg1: Type1) => Body - если возвращаемый тип не требует уточнения.
  • arg: Type => Body - с одним аргументом.
  • Body operation (_: Type) - _ единственный аргумент, который используется в одном месте.
  • Body operation _ - _ единственный аргумент, который используется в одном месте и не требует уточнения типа.
  • () => Body - без параметров.
scala> (a: Int) => a * 5
res2: Int => Int = <function1>

scala> res2(11)
res3: Int = 55

В функции нет необходимость пользоваться return, тем не менее эта возможность остается. Для композиции функций в Scala определены две функции. Если есть две функции f и g, выражение f.compose(g) вернёт новую функцию, которая при вызове сначала выполнит функцию g и затем применит f к результату. Аналогично f.andThen(g) вернёт функцию, которая сначала вызовет f и затем g, на результате, который был получен из f.

Функции высшего порядка

При передаче функции в качестве параметра необходимо правильно указать её тип. Тип указывается как (parameterType) => returnType. Функции, принимающие в качестве аргумента или возвращающие другую функцию называются функциями высшего порядка.

scala> def tenIntToFloat(how: (Int) => Float) = how(10)
tenIntToFloat: (how: Int => Float)Float

scala> tenIntToFloat(_.toFloat)
res8: Float = 10.0

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

def mult(a: Int) = {
    (x: Int) => a * x
}
scala> val mult2 = mult(2)
mult2: Int => Int = <function1>

scala> mult2(10)
res9: Int = 20

В частности можно не присваивать возвращенную функцию в переменную, а использовать сразу, воспользовавшись каррированием.

scala> mult(3)(10)
res10: Int = 30

Зачастую такие функции обозначаются как function(agr: Type)(arg: Type): ReturnType. лбую функцию можно привратить в каррированную с помощью метода curried.

scala> def foo(a: Int, b: Int) = a + b
foo: (a: Int, b: Int)Int

scala> (foo _) curried
warning: there was one feature warning; re-run with -feature for details
res0: Int => (Int => Int) = <function1>

scala> res0(1)(2)
res1: Int = 3

Абстракция управляющих конструкций

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

def foo(f: () => Unit) = {
    // do something
    f()
    // do something
}

Здесь при передаче аргумента придется использовать конструкцию типа () => block, из-за указаных скобок в объявлении высшей функции.

def foo(f: => Unit) = {
    // do something
    f
    // do something
}

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

scala> def run(func: => Unit) {
     | func
     | println("Stop running")
     | }
run: (func: => Unit)Unit

scala> run {
     | println("Do something in inner function")
     | }
Do something in inner function
Stop running

Коллекции

Основные трейты коллекций

Верхним трейтом иерархии коллекций является Traversable. Этот трейт определяет возможность обхода коллекции. Он предоставляет методы.

  • map[B](f: (A) => B): CC[B] - возвращается коллекция, где каждый элемент преобразован с помощью f.
  • foreach[U](f: Elem => U): Unit - изменяется коллекция, при выполнении f над каждым элементом.
  • find(p: (A) => Boolean): Option[A] - возвращает первый элемент, который удовлетворяет функции предикату.
  • filter(p: (A) => Boolean): Traversable[A] - возвращает коллекцию со всеми элементами, удовлетворяющих функции-предикату.
  • partition(p: (A) ⇒ Boolean): (Traversable[A], Traversable[A]) - разделяет коллекцию на две половины, основываясь на результате функции-предиката.
  • groupBy[K](f: (A) => K): Map[K, Traversable[A]] - группирует коллецию с помощью функции f.
  • Функции преобразования коллекции одного типа в другой - toArray, toBuffer, toIndexedSeq, toIterable, toIterator, toList, toMap, toSeq, toSet, toStream, toString, toTraversable

Прямым потоском Traversable является трейт Iterable. Методы Iterable:

  • iterator: Iterator - возвращает объект, который добавляет возможность итерирования.
  • ++(i: Iterable): Iterable и ++:(i: Iterable): Iterable - возвращает коллекцию, содержащую элементы обоих типов.
  • head: T - возвращает первый элемент.
  • headOption: Option[T] - возвращает первый элемент в Option.
  • last: T - возвращает последний элемент.
  • lastOption: Option[T] - возвращает последний элемент в Option.
  • tail: Iterable - возвращает все, кроме первого элемента.
  • length: Int - возвращает количество элементов.
  • isEmpty: Boolean - возвращает true, если количество элементов равно нулю.
  • sum: Int - возвращает сумму элементов.
  • product: Int - возвращает произведение элементов.
  • max: Int - возвращает наибольший элемент.
  • min: Int - возвращает наименьший элемент.
  • count(f: T => Boolean): Int - возвращает количество элементов, для которых предикат f вернет true.
  • forall(f: T => Boolean): Boolean - возвращает true, если для всех элементов предикат f вернет true.
  • exists(f: T => Boolean): Boolean - возвращает true, если для хотя бы одного элемента предикат f вернет true.
  • filter(f: T => Boolean): Iterable[T] - возвращает коллекцию исходного типа, для элементов которых f вернет true.
  • filterNot(f: T => Boolean): Iterable[T] - метод, обратный filter.
  • partition(f: T => Boolean): (Iterable[T], Iterable[T]) - вернет пару из результатов filter и filterNot.
  • takeWhile(f: T => Boolean): Iterable[T] - вернет первые элементы, соответствующие предикату.
  • dropWhile(f: T => Boolean): Iterable[T] - вернет все, кроме первых элементов, соответствующих предикату.
  • span(f: T => Boolean): (Iterable[T], Iterable[T]) - вернет пару из результатов takeWhile и dropWhile.
  • take(n: Int): Iterable[T] - вернет коллекцию из первых n элементов.
  • drop(n: Int): Iterable[T] - вернет коллекцию из всех, кроме первых n элементов.
  • splitAt(n: Int): Iterable[T] - вернет пару из результатов take и drop.
  • takeRight(n: Int): Iterable[T] - вернет коллекцию из последних n элементов.
  • dropRight(n: Int): Iterable[T] - вернет коллекцию из всех, кроме последних n элементов.
  • slice(from: Int, to: Int): Iterable[T] - вернет коллекцию из всех элементов с позиции from по to.
  • grouped(n: Int): Iterator[Iterable[T]] - вернет итератор фрагментов исходных коллекций с индексами 0 until n, n until n * 2 и так далее.
  • sliding(n: Int): Iterator[Iterable[T]] - вернет итератор фрагментов исходных коллекций с индексами 0 until n, 1 until n + 1 и так далее.
  • mkString(start: String, separator: String, end: String): String - объединяет все элементы в строку, в начале строки ставит start, между элементами separator, в конце end.
  • addString(b: StringBuilder): StringBuilder - добавляет строку к построителю строк.
scala> Map(1 -> "Hello").iterator
res0: Iterator[(Int, String)] = non-empty iterator

Iterator позволяет обходить его элементы в цикле, при этом имеет методы hasNext: Boolean, который проверяет, есть ли следующий элемент, а также метод next(): A, который возвращает следующий элемент и передвигает счетчик.

scala> for (i <- Map(1 -> "Hello").iterator) println(i)
(1,Hello)

scala> val it = Set(1, 2, 3, 4).iterator
it: Iterator[Int] = non-empty iterator

scala> while (it.hasNext) println(it.next())
1
2
3
4

Трейт Iterable имеет три основных дочерних трейта.

  • Seq - трейт, родитель коллекций, имеющих определенный порядок.
  • Set - трейт, родитель коллекций, не имеющих определенный порядок, но содержащие уникальные элементы.
  • Map - трейт, родитель коллекций, которые состоят из пар ключ-значение.

Каждый трейт имеет метод apply, который создает экземпляр коллекции. Помимо этого существуют изменяемые и неизменяемые коллекции. Неизменяемые являются предпочтителными и находятся в пакете scala.collection.immutable, в то время как изменяемые в scala.collection.mutable.

Seq

Трейт Seq содержит методы:

  • :+(v: T) - добавляет элемент в конец.
  • +:(v: T) - добавляет элемент в начало.
  • contains(e: T): Boolean - вернет true, если последовательность содержит элемент.
  • containsSlice(s: Seq[T]): Boolean - вернет true, если последовательность содержит последовательность.
  • startsWith(s: Seq[T]): Boolean - вернет true, если последовательность начинается с последовательности.
  • endsWith(s: Seq[T]): Boolean - вернет true, если последовательность заканчивается на последовательности.
  • indexWhere(f: T => Boolean): Int - вернет индекс первого элемента, для которого предикат вернет true.
  • prefixLength(f: T => Boolean): Int - вернет длину последовательности, начиная с начала, для которой f вернет true.
  • segmentLength(f: T => Boolean, n: Int): Int - вернет длину последовательности, начиная с n, для которой f вернет true.
  • padTo(n: Int, fill: T): Seq[T] - вернет копию последовательности, заполеную до длины n элементами fill.
  • intersect(seq: Seq[T]): Seq[T] - вернет пересечение двух последовательностей.
  • diff(seq: Seq[T]): Seq[T] - вернет разность двух последовательностей.
  • reverse: Seq[T] - вернет развернутую последовательность.
  • sorted: Seq[T] - вернет отсортированную последовательность.
  • sortWith(f: (T, T) => Boolean): Seq[T] - вернет отсортированную по функции-предикату последовательность.
  • sortBy(f: T => Int): Seq[T] - вернет отсортированную по функции-предикату, формирующей из элемента порядковый номер, последовательность.
  • permutations: Iterator[Seq[T]] - вернет итератор всех перестановок последовательности.
  • combinations(n: Int): Iterator[Seq[T]] - вернет итератор всех комбинаций последовательности, длиной n.

Прямыми наследниками трейта Seq являются трейты IndexedSeq, Buffer и LinearSeq. IndexedSeq представляет последовательности с индексами, по которым можно получить доступ к элементам с помощью метода apply. Неизменяемые наследники IndexedSeq:

  • Vector[T] - представляет последовательность, представленную в виде дерева, где каждый узел может иметь до 32 потомков
    scala> Vector("Hello", "Bye")
    res18: scala.collection.immutable.Vector[String] = Vector(Hello, Bye)
  • Range - представляет последовательность целых чисел, создается с помощью начального значения, конечного и шага (по-умолчанию 1). Методы, используемые для создания.
    • (start: Int) to (end: Int) by (step: Int) - от и до с шагом включительно.
    • until - аналог to, но с исключением конечного значения.
scala> 1 to 10 by 2
res19: scala.collection.immutable.Range = Range(1, 3, 5, 7, 9)
  • NumericRange[T] - аналог Range для действительных чисел.
scala> 1.0 until 2.3 by 0.4
res21: scala.collection.immutable.NumericRange[Double] = NumericRange(1.0, 1.4, 1.7999999999999998, 2.1999999999999997)
  • Array[T] и String - не являются наследниками IndexedSeq, поскольку берутся из Java, при этом неявно преобразуются в IndexedSeq.
scala> Array(1, 3, 10)
res22: Array[Int] = Array(1, 3, 10)

scala> "Scala"(2)
res24: Char = a

scala> "Scala".toArray
res25: Array[Char] = Array(S, c, a, l, a)

Наиболее важным изменяемым наследником IndexedSeq является ArraySeq[T] - массив, элементы которого можно изменить с помощью update.

scala> val as = ArraySeq(10, 20)
as: scala.collection.mutable.ArraySeq[Int] = ArraySeq(10, 20)

scala> as(0) = 15

scala> as
res69: scala.collection.mutable.ArraySeq[Int] = ArraySeq(15, 20)

LinearSeq предоставляет быстрый доступ к первому элементу при доступе к голове последовательности, а также быстрые операции с хвостом. Неизменяемые наследники LinearSeq:

  • List[T] - представлен значением Nil, который представляет пустой список, или объектом List. Методы List:
    • head: T - возвращает головной элемент списка.
    • tail: List[T] - возвращает хвост списка
    • Список можно создать с помощью оператора ::, который также может быть использован при сопоставлении с образцом.
scala> 10 :: 12 :: 14 :: Nil
res1: List[Int] = List(10, 12, 14)

scala> res1.head
res2: Int = 10

scala> res1.tail
res3: List[Int] = List(12, 14)
  • Stream[T] - список, хвост которого вычисляется только по-требованию. Подобно List имеет методы head и tail. Методы Stream:
    • take(n: Int): Stream[T] - возвращает поток с заданным количеством элементов.
    • force: Stream[T] - возвращает поток со всеми элементами.
    • Список можно создать с помощью оператора #::, который также может быть использован при сопоставлении с образцом.
scala> (for (i <- 1 to 10000) yield i).toStream
res5: scala.collection.immutable.Stream[Int] = Stream(1, ?)

scala> res5.take(14).force
res6: scala.collection.immutable.Stream[Int] = Stream(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)

scala> def foo(n: Int): Stream[Int] = n #:: foo(n + 1)
foo: (n: Int)Stream[Int]

scala> foo(1).take(6).force
res10: scala.collection.immutable.Stream[Int] = Stream(1, 2, 3, 4, 5, 6)

scala> foo(10)
res11: Stream[Int] = Stream(10, ?)
  • Queue[T] - передставляет коллекцию, представленую в виде очереди (“первый пришел - первый вышел”). Методы Queue:
    • enqueue(e: T): Queue[T] - создает новую очередь с заданным элементом в хвосте.
    • dequeue: (T, Queue[T]) - возвращает пару элемент-очередь, где очередь без первого элемента.
scala> import scala.collection.immutable.Queue
import scala.collection.immutable.Queue

scala> Queue("msg1").enqueue("msg2").dequeue
res15: (String, scala.collection.immutable.Queue[String]) = (msg1,Queue(msg2))

- Stack[T] - передставляет коллекцию, представленую в виде стэка (“последний пришел - первый вышел”). Методы Stack: - pop: Stack[T] - возвращает коллекцию без головного элемента. - push(e: T): Stack[T] - возвращает коллекцию с новым головным элементом.

scala> import scala.collection.immutable.Stack
import scala.collection.immutable.Stack

scala> Stack("msg1", "msg2")
res16: scala.collection.immutable.Stack[String] = Stack(msg1, msg2)

scala> Stack("msg1", "msg2").head
res17: String = msg1

scala> Stack("msg1", "msg2").pop
res18: scala.collection.immutable.Stack[String] = Stack(msg2)

scala> Stack("msg1", "msg2").push("msg3")
res19: scala.collection.immutable.Stack[String] = Stack(msg3, msg1, msg2)

scala> Stack("msg1", "msg2").push("msg3").pop
res20: scala.collection.immutable.Stack[String] = Stack(msg1, msg2)

К основным изменяемым класса, наследникам LinearSeq следует отнести два класса.

  • MutableList - изменяемый список. Помимо методов списка, имеет собственные методы:
    • +=(v: T) - добавляет элемент в конец списка.
    • update(key: Int, v: T) - заменяет элемент по индексу, как у массива.
    • apply(key: Int) - возвращает элемент по индексу, как у массива.
scala> val ml = scala.collection.mutable.MutableList(10)
ml: scala.collection.mutable.MutableList[Int] = MutableList(10)

scala> ml += 20
res15: ml.type = MutableList(10, 20)

scala> ml(1) = 25

scala> ml(1)
res17: Int = 25

scala> ml
res18: scala.collection.mutable.MutableList[Int] = MutableList(10, 25)
  • LinkedList - изменяемый список. Помимо методов списка, имеет собственные методы:
    • elem(v: T): T - заменяет голову списка.
    • next(v: LinkedList[T]) - заменяет хвост списка.
scala> val ll = scala.collection.mutable.LinkedList
LinkedList   LinkedListLike

scala> val ll = scala.collection.mutable.LinkedList(10, 20)
warning: there was one deprecation warning; re-run with -deprecation for details
ll: scala.collection.mutable.LinkedList[Int] = LinkedList(10, 20)

scala> ll.elem = 15
ll.elem: Int = 15

scala> ll.next = scala.collection.mutable.LinkedList(10, 20)
warning: there was one deprecation warning; re-run with -deprecation for details
ll.next: scala.collection.mutable.LinkedList[Int] = LinkedList(10, 20)

scala> ll
res19: scala.collection.mutable.LinkedList[Int] = LinkedList(15, 10, 20)

Buffer представлен двумя основными классами.

  • ArrayBuffer[T] - изменяемый массив. Методы ArrayBuffer:
    • +=(v: T) - добавляет элемент в конец.
    • -=(v: T) - удаляет элемент.
    • -(elem: A): ArrayBuffer[A] - возвращает ArrayBuffer с удаленным элементом.
    • --(elem: ArrayBuffer[A]): ArrayBuffer[A] - возвращает ArrayBuffer с удаленными элементами, содержащимися в переданном.
    • update(key: Int, v: T) - заменяет элемент по индексу.
    • +=:(v: T) - добавляет элемент в начало.
    • ++=(v: Traversable[T]) - добавляет элементы коллекции в начало.
scala> val ab = scala.collection.mutable.ArrayBuffer(10, 20)
ab: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(10, 20)

scala> ab += 30
res20: ab.type = ArrayBuffer(10, 20, 30)

scala> ab -= 20
res21: ab.type = ArrayBuffer(10, 30)

scala> ab(1) = 15

scala> ab
res23: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(10, 15)
  • ListBuffer[T] - изменяемый список. Методы такие же, как и у ArrayBuffer.
scala> val lb = scala.collection.mutable.ListBuffer(10, 20)
lb: scala.collection.mutable.ListBuffer[Int] = ListBuffer(10, 20)

scala> lb += 30
res24: lb.type = ListBuffer(10, 20, 30)

scala> lb -= 20
res25: lb.type = ListBuffer(10, 30)

scala> lb(1)
res26: Int = 30

Set

Set это коллекции, не содержающие повторяющихся элементов. Общие методы для Set это:

  • contains(key: A): Boolean - проверка, есть ли уже искомый элемент.
  • +(elem: A): Set[A] - возвращает Set с добавленным элементом.
  • -(elem: A): Set[A] - возвращает Set с удаленным элементом.
  • --(elem: Set[A]): Set[A] - возвращает Set с удаленными элементами, содержащимися в переданном.
  • |(elem: Set[A]): Set[A] - объединение коллекций (как ++).
  • &amp;(elem: Set[A]): Set[A] - пересечение коллекций.
  • &amp;~(elem: Set[A]): Set[A] - разность коллекций (как –).
scala> val s = Set(1, 2, 1, 3)
s: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> s.contains(4)
res27: Boolean = false

scala> s + 4
res28: scala.collection.immutable.Set[Int] = Set(1, 2, 3, 4)

Set имеет наследников:

  • HashSet[T] - набор, представленный в виде хэш-дерева.
  • SortedSet[T] - сортированный набор, представлен в виде красно-черного дерева.
  • BitSet - множество целых чисел, где i-бит равен единице, если число присутствует в множестве.
  • ListSet[T] - набор, представленный в виде списка.
scala> scala.collection.SortedSet(1, 2, 1, 4, 3)
res54: scala.collection.SortedSet[Int] = TreeSet(1, 2, 3, 4)

Помимо изменяемых аналогов вышеуказанных классов Set имеет наследника LinkedHashSet[T] из пакета scala.collection.mutable, который сохраняет порядок добавления элементов.

scala> val s = scala.collection.mutable.Set(2, 4, 1, 2, 3)
s: scala.collection.mutable.Set[Int] = Set(1, 2, 3, 4)

scala> s += 7
res35: s.type = Set(1, 2, 3, 7, 4)

scala> val lhs = scala.collection.mutable.LinkedHashSet(2, 4, 1, 2, 3)
lhs: scala.collection.mutable.LinkedHashSet[Int] = Set(2, 4, 1, 3)

scala> lhs += 8
res38: lhs.type = Set(2, 4, 1, 3, 7, 8)

scala> lhs += 5
res39: lhs.type = Set(2, 4, 1, 3, 7, 8, 5)

Дополнительные методы

Методы изменяемых коллекций:

  • +=(e: T) - добавляет элемент (может принимать кортеж).
  • ++=(e: Iterable[T]) - добавляет элементы (может принимать кортеж).
  • -=(e: T) - удаляет элемент (может принимать кортеж).
  • --=(e: Iterable[T]) - удаляет элементы (может принимать кортеж).

У трейта Iterable существует еще несколько методов. Методы, принимающие одноместную функцию:

  • map(f: T => T2): Travesable[T2] - возвращает коллекцию с измененными элементами.
scala> val l = List(10, 20, 30)
l: List[Int] = List(10, 20, 30)

scala> l map { _ / 20 }
res0: List[Int] = List(0, 1, 1)

scala> l map { _ / 10 toString }
warning: there was one feature warning; re-run with -feature for details
res1: List[String] = List(1, 2, 3)
  • flatMap(f: T => Traversable[T2]): Travesable[T2] - принимает функцию, которая работает с вложенными списками и объединяет результаты в единую коллекцию.
scala> val l = List(10, 20, 30)
l: List[Int] = List(10, 20, 30)

scala> l map { (i: Int) => Vector(i - 1, i + 1) }
res2: List[scala.collection.immutable.Vector[Int]] = List(Vector(9, 11), Vector(19, 21), Vector(29, 31))

scala> l flatMap { (i: Int) => Vector(i - 1, i + 1) }
res3: List[Int] = List(9, 11, 19, 21, 29, 31)
  • collect(pf: PartialFunction[T, T2]): Travesable[T2] - принимает частично определенную функцию, которая коллекцию, в которую включены только значения, для которых она определена.
scala> val l = List(10, 20, 30)
l: List[Int] = List(10, 20, 30)

scala> l collect {
     | case 10 => 1
     | case 20 => 2
     | }
res4: List[Int] = List(1, 2)
  • foreach(f: T => T2): Unit - применяет переданую функцию поочередно ко всем элементам коллекции.
scala> val l = List(10, 20, 30)
l: List[Int] = List(10, 20, 30)

scala> l foreach println
10
20
30

Методы, принимающие двуместную функцию:

  • reduceLeft(f: (T, T) => T2): T2 - поочередно применяет операцию к элементам, обходя слева направо, подставляя в качестве левого элемента результат предыдущего вычисления операции.
scala> val l = List(10, 20, 30)
l: List[Int] = List(10, 20, 30)

scala> l reduceLeft { _ + _ }
res6: Int = 60
  • reduceRight(f: (T, T) => T2): T2 - как reduceLeft, но справа налево.
  • foldLeft(init: T)(f: (T, T) => T2): T2 - подобна reduceLeft, но устанавливается начальное значение. Также может быть записана в форме /:.
scala> val l = List(10, 20, 30)
l: List[Int] = List(10, 20, 30)

scala> l.foldLeft(5)(_ + _)
res11: Int = 65

scala> (5 /: l)(_ + _)
res12: Int = 65
  • foldRight(init: T)(f: (T, T) => T2): T2 - как foldLeft, но справа налево. Также может быть записана в форме :\.
  • scanLeft(init: T)(f: (T, T) => T2): Traversable[T2] - подобна foldLeft, но возвращает коллекцию всех промежуточных результатов.
scala> val l = List(10, 20, 30)
l: List[Int] = List(10, 20, 30)

scala> l.scanLeft(5)(_ + _)
res16: List[Int] = List(5, 15, 35, 65)
  • scanRight(init: T)(f: (T, T) => T2): Traversable[T2] - как scanLeft, но справа налево.

Методы трейта Iterable:

  • zip(a: Iterable[T2]): Iterable[(T, T2)] - объединяет две коллекции в коллекцию пар, длину равна длине меньшей из коллекций.
  • zipAll(a: Iterable[T2], d1: T, d22: T2): Iterable[(T, T2)] - объединяет две коллекции в коллекцию пар, заполняя меньшую коллекцию значениями по-умолчанию.
  • zipWithIndex: Iterable[(T, Int)] - возвращает список пар, где вторым компонентом является индекс исходной коллекции.
  • view: SeqView[T] - возвращает ленивую коллекцию, каждый элемент которого вычисляется непосредственно при получении. Похоже на Stream, но не имеет даже первого элемента.
scala> val l = List(10, 20, 30, 40)
l: List[Int] = List(10, 20, 30, 40)

scala> val lazyl = l.view.map(_ * 2)
lazyl: scala.collection.SeqView[Int,Seq[_]] = SeqViewM(...)

scala> lazyl
res18: scala.collection.SeqView[Int,Seq[_]] = SeqViewM(...)

scala> lazyl(2)
res19: Int = 60

scala> lazyl.take(2).force
res20: Seq[Int] = List(20, 40)

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

Пакет scala.collection.JavaConversions содержит множество методов для преобразований между коллекциями Scala и Java. Из Scala в Java:

  • asJavaCollection(i: Iterable): java.util.Collection
  • asJavaIterable(i: Iterable): java.util.Iterable
  • asJavaIterator(i: Iterator): java.util.Iterator
  • asJavaEnumeration(i: Iterator): java.util.Enumeration
  • seqAsJavaList(i: Seq): java.util.List
  • mutableSeqAsJavaList(i: mutable.Seq): java.util.List
  • bufferAsJavaList(i: mutable.Buffer): java.util.List
  • setAsJavaSet(i: Set): java.util.Set
  • mutableSetAsJavaSet(i: mutable.Set): java.util.Set
  • mutableSetAsJavaSet(i: mutable.Set): java.util.Set
  • mapAsJavaMap(i: Map): java.util.Map
  • mutableMapAsJavaMap(i: mutable.Map): java.util.Map
  • asJavaDictionary(i: Map): java.util.Dictionary
  • asJavaConcurrentMap(i: mutable.ConcurrentMap): java.util.concurrent.Map

Из Java в Scala:

  • collectionAsScalaIterable(ju: java.util.Collection): Iterable
  • iterableAsScalaIterable(ju: java.util.Iterable): Iterable
  • asScalaIterator(ju: java.util.Iterator): Iterator
  • enumerationAsScalaIterator(ju: java.util.Enumeration): Iterator
  • asScalaBuffer(ju: java.util.List): mutable.Buffer
  • mapAsScalaMap(ju: java.util.Map): mutable.Map
  • dictionaryAsScalaMap(ju: java.util.Dictionary): mutable.Map
  • propertioesAsScalaMap(ju: java.util.Properties): mutable.Map
  • asScalaConcurrentMap(i: java.util.concurrent.Map): mutable.ConcurrentMap

Многопоточные коллекции

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

  • SynchronizedBuffer
  • SynchronizedMap
  • SynchronizedPriorityQueue
  • SynchronizedQueue
  • SynchronizedSet
  • SynchronizedStack

Выполняя различные операции над коллекциями, можно запустить вычисления в нескольких потоков. Для этого существует метод par.

Сопоставление с образцом

Сопоставление с образцом

В Scala нет кострукции switch, вместо неё используется механизм, именуемый pattern matching. Инструкция match возвращает значение, которое является результатом первого совпадения выражения case =>.

val num: Int = 6

val what = num match {
    case 6 => "Number"
}

Для совпавдения с любым значением используется _, иначе вызывается исключение MatchError.

0 match {
    case _ => "Any"
}

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

one match {
    case i: Int => {
        "Тип Int"
        "Можно использовать переменную i"
    }
    case _: Int => "Тип Int"
    case 10 => "Конкретное значение"
    case head :: tail => {
        "Тип List"
        "В head лежит голова списка"
        "В tail лежит хвост списка"
    }
}

В сопоставлении с образцом можно использовать ограничители. Конструкция if b: Boolean ставится перед =>, тогда совпадение, помимо прочего, произойдет, если в ограничителе истина.

 val i: Int = 10

i match {
    case int: Int if int < 10 => "Too small"
    case int: Int if int >= 10 => "Good!"
}

При сопоставлении с массивами имеется возможность сравнить только определенную часть с помощью _ и _. При использовании _ элемент найдется и никуда не запишется, а _ может использоваться только в конце, все оставшиеся значения отпавятся в эту переменную при совпадении.

scala> val a = Array(0, 1, 2, 3, 4, 5)
a: Array[Int] = Array(0, 1, 2, 3, 4, 5)

scala> a match {
    | case Array(0, _, 2, a, _*) => "0, some, 2, " + a + " and other..."
    | }
res4: String = 0, some, 2, 3 and other...

В одном case можно указать несколько вариантов с помощью оператора |.

scala> 1 match {
    | case 1 | 2 | 3 => print("Hello")
    | }
Hello

Можно создавать псевдонимы, при сопоставлениями в case-классами с помощью аннотации @.

case class Name(firstName: String)

case class Person(name: Name)
scala> john match {
  | case p @ Person(n @ Name(firstName)) => {
  | println(firstName)
  | println(n)
  | println(p)
  | }
  | case _ => println("Oops")
  | }
John
Name(John)
Person(Name(John))

Совпадение с образцом можно использовать в циклах for.

scala> for ((key, value) <- Map(1 -> "one", 2 -> "two", 3 -> "three") if key < 3) yield value
res5: scala.collection.immutable.Iterable[String] = List(one, two)

Case классы

Для сопоставления с образцом используется метод unapply. Case классы объявляются с помощью ключевого слова case. Они имеют следующие свойства:

  • Каждый параметр конструктора автоматически становится val, если только явно не обозначен, как var.
  • Создается объект-компаньон с методом apply.
  • Создается метод unapply.
  • Генерируются методы toString, equals, hashCode и copy.

Можно пометить объект словом case.

abstract class Currency

case class USD(precision: Int) extends Currency
case class RUB(precision: Int) extends Currency
case object Unknown extends Currency
cur match {
    case USD(pr) => "is UDS"
    case RUB(pr) if pr > 2 => "RUB with precision more than 2"
}

Case класс можно копировать с помощью метода copy, при этом метод позволяет изменять его значения полей, если указать в параметре valName = newValue.

scala> val currency = USD(2)
currency: USD = USD(2)

scala> currency.copy(precision = 4)
res7: USD = USD(4)

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

scala> case class Person(fname: String, lname: String)
defined class Person

scala> val john = Person("John", "Smith")
john: Person = Person(John,Smith)

scala> john match {
  | case fname Person lname => (lname, fname)
  | }
res8: (String, String) = (Smith,John)

Запечатанный класс

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

Option

На case классах основан тип Option. Он представлен запечатанным абстрактным классом Option, наследниками case классом Some и case объектом None. Методы Option:

  • isEmpty: Boolean - false для Some, true для None.
  • get: T - значение для Some, бросает исключение для None.

Например, Map.get возвращает Option.

Частично определенные функции

Частично определенные функции, PartialFunction[A, B], где A - входящее значение, а B - возвращаемое, состоят из множества предложений case. Такая функция имеет два метода:

  • apply(v: A): B - возвращает значение из сопоставления с образцом.
  • isDefinedAt(v: A): Boolean - возвращает true, если найдено совпадение в сопоставлениях с образцом.
scala> val pf: PartialFunction[Any, Any] = {
      | case _: Int => 1
      | case _: String => " "
      | case _: Float => 1.toFloat
      | }
pf: PartialFunction[Any,Any] = <function1>

scala> pf(4)
res11: Any = 1

scala> pf.isDefinedAt('a')
res13: Boolean = false

Можно объединять частично определенные функции с помощью методов OrElse и andThen.

Аннотации

Аннотации

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

@deprecated def foo = 10

При аннотировании главного конструктора, аннотации указываются перед ним, с указанием пустых строк, если она не имеет аргументов.

class A @const() (name: String, b: Int)

Выражение можно аннотировать, добавляя двоеточие и аннотацию.

(Map(1 -> "one").get(1): @annot)

Параметры типов.

Option[@annot T]

Аннотация может принимать именнованные аргументы. Если имя value, то его можно опустить.

@anno(name = "Name", "hello") def foo = 10

Для реализации аннотаций, её следует отнаследовать от annotation.Annotation

Аннотации для оптимизации

Если рекурсивный метод заканчивается рекурсивным методом, то он может быть оптимизирован компилятором в цикл. Это свойство называется хвостовой рекурсией. Если перед рекурсивным методом поместить аннотацию @tailrec, то компилятор сообщит об ошибки, если не получится выполнить оптимизацию.

@tailrec def foo(n: Int): Int = if (n < 0) 0 else foo(n - 1)

Аннотация @switch оптимизирует инструкцию сопоставления с образцом в таблицу переходов.

(n: @switch) match {
    case _ => "ololo"
}

Аннотация @elidable помечает методы, которые следует удалить из окончательной версии. Подобный код следует компилировать определенныс образом. Аннотация @specialized(TypeName) автоматически генерирует методы, для каждых из перечисленных типов.

def foo[@specialized(Int, Float) T](a: T, b: T): Boolean = a != b

Аннотация предупреждений и ошибок

Существует ряд аннотаций для работы с предупреждениями и ошибками:

  • @depricated - помечает метод, как устаревший.
  • @depricatedName(value) - позволяет использовать устаревшее имя параметра.
  • @unchecked - подавляет предупреждения о неполном match.
  • @implicitNotFound(message) - сообщает, если не может найти неявное преобразование.

Параметризированные типы

Обобщенные классы и методы

В Scala, как в C++ или Java класс или трейт может иметь параметризированные типы. Они обозначаются в квадратных скобках после имени класса.

case class Person[T, U](name: T, age: U)

При этом Scala умеет автоматически определять тип в момент создания объекта.

Person('A', 1.2) // Person[Char, Double]

Person[Char, Double]('A', 1.2) // Person[Char, Double]

Методы тоже могут быть параметризированными.

def foo[T](v: T) = v.asInstanceOf[String]

val f = foo[String] _

Можно указать границы изменения типов.

  • T <: M - T должен быть подтипом M (верхняя граница).
scala> class Person
defined class Person
warning: previously defined object Person is not a companion to class Person.
Companions must be defined together; you may wish to use :paste mode for this.

scala> class Worker extends Person
defined class Worker

scala> def foo[T <: Person](p: T) = p
foo: [T <: Person](p: T)T

scala> foo(10)
<console>:14: error: inferred type arguments [Int] do not conform to method foo's type parameter bounds [T <: Person]
       foo(10)
       ^
<console>:14: error: type mismatch;
 found   : Int(10)
 required: T
       foo(10)
           ^

scala> foo(new Worker)
res16: Worker = Worker@70b1028d

scala> foo(new Person)
res17: Person = Person@50caeb4b
  • T >: M - T должен быть супертипом для M (нижняя граница).
trait Name {
    def getFirstName: Option[String]
    def getLastName: Option[String]
}

case class FirstName(value: String) extends Name {
    def getFirstName: Option[String] = Some(value)
    def getLastName: Option[String] = None
}

case class LastName(value: String) extends Name {
    def getFirstName: Option[String] = None
    def getLastName: Option[String] = Some(value)
}

case class Person[T](name: T) {
    def replaceNameWith[U >: T](name: U) = Person[U](name)
}
scala> val smith = Person(LastName("Smith"))
smith: Person[LastName] = Person(LastName(Smith))

scala> smith.replaceNameWith(FirstName("John"))
res26: Person[Product with Serializable with Name] = Person(FirstName(John))
  • T <% M - T может быть неявно преобразован в M (см. неявные преобразования) (граница представления, view bound). То есть есть тип T, но при этом он может использовать методы типа M. Устарело.
case class Person[T <% Name](name: T)

implicit def stringToName(str: String): Name = FirstName(str)
scala> Person("Ann")
res27: Person[String] = Person(Ann)
  • T : M - существует неявное значение типа M[T] (см. неявные преобразования) (граница контекста, context bound).
case class Name[T](value: T) {
    def get: T = value
}

case class Person[T : Name](name: T) {
    def getFullName(implicit lastName: Name[T]): String = name + " " + lastName.get
}
scala> implicit val lastName = Name("Ivanov")
lastName: Name[String] = Name(Ivanov)

scala> val ivan = Person("Ivan")
ivan: Person[String] = Person(Ivan)

scala> ivan.getFullName
res28: String = Ivan Ivanov

Для одного типа может использоваться несколько границ. Можно определить по-одной верхней и нижней границе, но можно, чтобы тип реализовывал несколько трейтов. Можно использовать несколько границ представления и контекста.

class A[T : M : R] // T[M] и T[R]
scala> case class A[T](a: T)
defined class A

scala> case class B[T](b: T)
defined class B

scala> case class C[T : A : B](c: T)
defined class C

scala> C(5)
<console>:17: error: could not find implicit value for evidence parameter of type A[Int]
    C(5)
     ^

scala> implicit val a: A[Int] = A(10)
a: A[Int] = A(10)

scala> C(5)
<console>:18: error: could not find implicit value for evidence parameter of type B[Int]
    C(5)
     ^

scala> implicit val b: B[Int] = B(10)
b: B[Int] = B(10)

scala> C(5)
res2: C[Int] = C(5)

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

  • T =:= U - тип T равен U.
  • T <:< U - тип T является подтипом U.
trait Move {
    def step: Unit
}

case class Name[T](value: T)

case class Person[T <% Name](name: T) {
    def move(implicit ev: T <:< Move) = name.step
}

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

Вариантность

Вариантность — перенос наследования исходных типов на производные от них типы. Под производными типами понимаются контейнеры. Существует три вида вариантости:

  • Инвариантность — ситуация, когда наследование исходных типов не переносится на производные. Все, что рассматривалось до этого.
  • Ковариантность — перенос наследования исходных типов на производные от них типы в прямом порядке. Обозначается знаком + в исходном типе. Здесь Group[Person] является родителем Group[Worker]. Отношение типа “если я могу что-то сделать из базового класса, то я могу сделать это и с подклассом”.
class Person
class Worker extends Person

case class Group[+T](person: T)

def foo[T <: Group[Person]](gr: T) = true
scala> foo(Group(new Worker))
res4: Boolean = true
  • Контравариантность — перенос наследования исходных типов на производные от них типы в обратном порядке. Обозначается знаком - в исходном типе. Может быть использовано при контрактном программировании. Здесь Group[Worker] является родителем Group[Person]. Отношение типа “если я могу что-то сделать в подклассе из базового класса, то я могу сделать это и с базовым классом”.
trait Smile[-T] {
    def doSmile(p: T) = println("smile4T")
}

class Person extends Smile[Person]
class Worker extends Person

def foo(from: Smile[Worker], to: Person) = s.doSmile
scala> foo(Group(new Person))
res4: Boolean = true

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

class Smile[+T] {
    def stop(p: T) = println("stop smiling") // Ошибка. T коварианта, но подставляется в контрвариантную позицию
}
class Smile[-T](val max: T) {
    def stop(p: T) = {
        println("stop smiling")
        max // Ошибка. T контрварианта, но подставляется в ковариантную позицию
    }
}

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

class Smile[+T] {
    def stop[R > T](p: R) = println("stop smiling") // Ошибки нет
}

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

class Person[T](name: T)

def addPerson(person: Person[_ <: ConcreteType])

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

Неявные параметры и преобразования

Неявные преобразования

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

case class MyInt(n: Int) {
    def mult10 = n * 10
}

implicit def int2MyInt(n: Int): MyInt = MyInt(n)
scala> 10.mult10
res0: Int = 100

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

  • Если тип выражения отличается от ожидаемого.
scala> def foo(i: Int) :Float = i + 10
foo: (i: Int)Float

scala> foo(1)
res0: Float = 11.0
  • Обращение к несуществующему полю или методу объекта.
  • При вызове метода с аргументами, не соответствующими ожиданию.

Неявные параметры

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

  • Среди объявлений def и val в текущей области видимости.
  • В объекте-компаньоне типа, связанного с требуемым, то есть требуемого типе, а также, если тип параметризирован, то в объектах компаньонах параметров типа.
scala> def merge(i: Iterable[Any])(implicit delimiter: Char): String = i.map(_.toString).reduceLeft(_ + delimiter.toString + _)
merge: (i: Iterable[Any])(implicit delimiter: Char)String

scala> implicit def delimiter: Char = '"'
delimiter: Char

scala> merge(Array(1, 2, 3, 4, 5))
res0: String = 1"2"3"4"5

При этом остается возможность передавать параметр явно.

scala> merge(Array(1, 2, 3, 4, 5))('.')
res1: String = 1.2.3.4.5

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

def multiply5[T](a: T)(implicit int: T => Int) = a * 5
scala> implicit def str2Int(s: String) = s.size
warning: there was one feature warning; re-run with -feature for details
str2Int: (s: String)Int

scala> multiply5("")
res4: Int = 0

scala> multiply5("ololo")
res5: Int = 25

Вытащить текущее неявное значение можно с помощью метода implicitly[TypeOfImplicit].

scala> val f = implicitly[String => Int]
f: String => Int = <function1>

Дополнительно

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

implicit class MyInt(i: Int) {
    def mult10 = i * 10
}

Пример выше будет преобразован в:

class MyInt(i: Int) {
    def mult10 = i * 10
}

implicit def int2MyInt(i: Int): MyInt = new MyInt(i)

Дополнительные типы

Типы-одиночки

При постоении fluent-методов this будет тип класса, в котором объявлен метод, а поэтому не получится использовать методы конктретного типа, если предыдущий fluent-метод вернет тип родителя. Для этого можно указать тип как T.type.

class Person {
    def walk: this.type = {
        prinln("I'm walk")
        this
    }
}

class Worker extends Person {
    def work: this.type = this
}
scala> val w = new Worker
w: Worker = Worker@74a9c4b0

scala> w.doSomething.work
res1: w.type = Worker@74a9c4b0

Если потребуется передать объект-одиночку в качестве параметра, то в методе следует объявить как SingletonName.type.

def foo(s: Singleton.type) = s.method

Проекция типов

Об этом уже говорилось. При использовании вложенных классов, какждый вложенный класс будет принадлежать к конкретному внешнему, чтобы обойти это необходимо использовать конструкцию Outer#Inner.

class Group(val name: String) {
    private var members = scala.collection.mutable.ArrayBuffer[Group#Member]()

    def +=:(member: Group#Member) =
        {
            members += member
            this
        }

    def -:(member: Group#Member) =
        {
            members -= member
            this
        }

    def getMembers: ArrayBuffer[Group#Member] = members

    class Member(val firstName: String, val lastName: String)
}
scala> val alcoholics = new Group("Alcoholics")
alcoholics: Group = Group@17f2dd85

scala> val vasya = new alcoholics.Member("Vasya", "Semenov")
vasya: alcoholics.Member = Group$Member@6981f8f3

scala> val gays = new Group("Gays")
gays: Group = Group@524a076e

scala> vasya +=: gays
res4: Group = Group@524a076e

scala> vasya +=: alcoholics
res6: Group = Group@17f2dd85

scala> gays.getMembers
res7: scala.collection.mutable.ArrayBuffer[Group#Member] = ArrayBuffer(Group$Member@6981f8f3)

scala> alcoholics.getMembers
res8: scala.collection.mutable.ArrayBuffer[Group#Member] = ArrayBuffer(Group$Member@6981f8f3)

scala> vasya
res9: alcoholics.Member = Group$Member@6981f8f3

Цепочки

Выражение типа scala.collection.mutable.ArrayBuffer называют цепочкой. Все компоненты цепочки, кроме последнего обязаны быть стабильными, к ним относят:

  • Пакет.
  • Объект.
  • Значения val.
  • this и super.

Псевдонимы типов

С помощью ключевого слова type можно создавать псевдонимы сложных имен типов. Все объявления псевдонимов типа должны вкладываться в класс или объект.

class A {
    type MutableMap = scala.collection.mutable.Map[String, String]

    def foo(mm: MutableMap) = mm(0) = ("a" -> "b")
}

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

trait A {
    type In
    def foo(in: In): String
}

class A1 extends A {
    type In = String
    def foo(in: In) = in
}

Структурный и составной типы

Структурный тип - это описание абстрактных полей и методов, которыми должен обладать соответствующий тип.

def foo(
    n: { def f(a: Int): String },
    n2: { val stat: Int}
): String = n.f(n2.stat)

Чтобы принадлежать составному типу, значение должно принадлежать каждому из типов. Составной тип определяетяс как T with T2 with T3.

def foo[T](
    a: Ordered[T] with Serializable
)(
    implicit int2T: Int => T
): String = if (a > 10) a.toString else ""

Технически структурный и составной типы являются сокращенной формой одного вида, для структурного AnyRef { def methodName: Type }, а для составного Type with Type2 {}.

Инфиксный тип

Если тип параметризирован двумя параметрами типа, то можно записывать его в инфиксной форме T1 T T2 для типа T[T1, T2].

class Person(val age: Int, val name: String)

object Person {
    type ^[Int, String] = (Int, String)
    def apply(desc: Int ^ String) = new Person(desc._1, desc._2)
}

Экзистенциальный тип

Экзистенциальные типы данных названы так из-за квантора существования ∃. То есть позволяет ограничить подтип конструкцией forSome { statement }. В выражении могут быть объявления val и type.

def foo(
    a: Array[T] forSome { type T <: Numeric }
): Numeric = a.sum

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

class Outer {
    class Inner
}

def foo[I <: o.Inner forSome { val o: Outer } ](a1: I, a2: I) = println("YES")
scala> val o1 = new Outer
o1: Outer = Outer@7db5b890

scala> val o2 = new Outer
o2: Outer = Outer@b506ed0

scala> foo(new o1.Inner, new o1.Inner)
YES

scala> foo(new o1.Inner, new o2.Inner)
<console>:16: error: inferred type arguments [Outer#Inner] do not conform to method foo's type parameter bounds [I <: o.Inner forSome { val o: Outer }]
    foo(new o1.Inner, new o2.Inner)
    ^
<console>:16: error: type mismatch;
found   : o1.Inner
required: I
    foo(new o1.Inner, new o2.Inner)
        ^
<console>:16: error: type mismatch;
found   : o2.Inner
required: I
    foo(new o1.Inner, new o2.Inner)

Собственные типы

Имеется возможность задать псевдоним и собственный тип в начале класса, это позволит внутри класса использовать новое имя или гарантировать, что класс будет наследовать определенный тип. Для этого в начале класса или трейта пишется инструкция alias: Type =>. Если псевдоним не нужен, то вместо alias пишется this, а если не нужен тип, то Type не указывается.

trait Fatal { π: Exception =>
    val level: String = "MAX"
}

В примере выше внутри this доступна по π, также гарантируется, что трейт будет подмешан в наследника Exception.

XML

Узлы и аттрибуты

В Scala имеется встроенная поддердка xml. Можно определять литералы xml, используя разметку xml.

val doc =
    <ul>
        <li class="first">one</li>
        <li>two</li>
        <?php echo("Scala"); ?>
        <!-- COMMENT -->
    </ul>

Scala предоставляет целую иерархию для работы с xml. Все инструменты для работы хранятся в пакете scala.xml. Верхним классом иерархии является NodeSeq, который является наследником Seq[Node] и обладает всеми свойствами Seq. Прямым наследником NodeSeq является Node. Объект типа Node имеет методы:

  • child: Seq[Node] - возвращает набор дочерних элементов.
scala> doc.child
res12: Seq[scala.xml.Node] =
ArrayBuffer(
  , <li class="first">one</li>,
  , <li>two</li>,
  , <?php echo("Scala"); ?>,
  , <!-- COMMENT -->,
)
  • label - возвращает имя элемента.
scala> doc.label
res13: String = ul
  • attributes - возвращает объект типа MetaData, который похож на ассоциативный массив.
scala> doc.child(1).attributes("class")
res14: Seq[scala.xml.Node] = first
  • text - строку без элемнтов xml.
scala> doc.text
res15: String =
"
  one
  two
"

Для получения пространства имен используется метод scope.

Встроенные выражения

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

scala> <ul>{for (i <- 1 to 5) yield <li>{i}</li>}</ul>
res0: scala.xml.Elem = <ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>

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

scala> def href = "http://ya.ru"
href: String

scala> <a href={href} />
res25: scala.xml.Elem = <a href="http://ya.ru"/>

XPath подобные выражения

NodeSeq имеет два метода, которые применяются для поиска элементов.

  • \(s: String): NodeSeq - поиск дочернего элемента.
  • \\(s: String): NodeSeq - поиск на всех уровнях вложенности.

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

scala> doc \ "li"
res40: scala.xml.NodeSeq = NodeSeq(<li class="first">one</li>, <li>two</li>)

scala> (doc \ "li")(0)
res41: scala.xml.Node = <li class="first">one</li>

scala> (doc \ "li")(0) \ "@class"
res42: scala.xml.NodeSeq = first

Сопоставление с образцом

При сопоставлении с образцом используются следующие правила.

  • Совпадение с любым количеством аттрибутов.
elem match {
    case <a /> =>
}
  • Совпадение с единственным дочерним с помощью _.
elem match {
    case <li>{_}</li> =>
}
  • Совпадение с любыми дочерними с помощью _*.
elem match {
    case <li>{_*}</li> =>
}
  • Совпадение с единственным дочерним с присвоением в переменную.
elem match {
    case <li>{child}</li> =>
}
  • Совпадение с любым количеством дочерних элементов с присвоением в переменную.
elem match {
    case <li>{children @ _*}</li> =>
}
  • Совпадение с текстовым узлом с присвоением в переменную.

elem match {
    case <li>{Text(child)}</li> =>
}

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

Модификация

Для модификации элемнтов следует пользоваться методом copy класса Elem, который имеет аргументы label, attributes и child.

scala> doc.copy(child = <li>another one</li>)
res43: scala.xml.Elem = <ul><li>another one</li></ul>

Добавить или изменить аттрибут можно с помощью метода %(attr: Attribute). Создать аттрибут можно используя конструкцию Attribute(namespace: String, name: String, value: String, MetaData).

scala> val a = <a />
a: scala.xml.Elem = <a/>

scala> a % scala.xml.Attribute(null, "href", "http://ya.ru", scala.xml.Null)
res46: scala.xml.Elem = <a href="http://ya.ru"/>

scala> a % scala.xml.Attribute(null, "href", "http://ya.ru", scala.xml.Attribute(null, "class", "external", scala.xml.Null))
res47: scala.xml.Elem = <a class="external" href="http://ya.ru"/>

Трансформация XML

Для трансформации используется класс RuleTransformer, конструктор которого принимает один или несколько объектов типа RuleRewrite и применяет их. Для трансформации следует переопределить метод transform, который может принимать Node или Seq[Node].

scala> val doc = <ul><li>One</li><li>Two</li><li>Three</li></ul>
doc: scala.xml.Elem = <ul><li>One</li><li>Two</li><li>Three</li></ul>

scala> import scala.xml._
import scala.xml._

scala> import scala.xml.transform._
import scala.xml.transform._

scala> :paste
// Entering paste mode (ctrl-D to finish)

val ol_ul_rule = new RewriteRule {
    override def transform(node: Node) = node match {
        case <ul>{_*}</ul> => node.asInstanceOf[Elem].copy(label = "ol")
        case <li>{scala.xml.Text(li)}</li> => <a />
        case _ => node
    }
}

// Exiting paste mode, now interpreting.

ol_ul_rule: scala.xml.transform.RewriteRule = <function1>

scala> new RuleTransformer(ol_ul_rule).transform(doc)
res5: Seq[scala.xml.Node] = List(<ol><a/><a/><a/></ol>)

Загрузка и сохранение

Для загрузки можно воспользоваться объектом XML.

  • loadFile(path: String) - загрузка xml из файла.
  • load(url: URL): Elem - загрузка из url
  • load(source: InputSource): Elem - загрузка различными объектами из java.io объектов
XML.load(new java.net.URL("http://example.com"))

Монады

Монады и функторы

Моноид — это термин из абстрактной алгебры. Моноид определяется следующими вещами:

  • Множество M.
  • Бинарная операция ⊕ на этом множестве, от которой требуется ассоциативность.
  • Нейтральный элемент ε этой операции, входящий в множество (т.е. такой, что (∀a ∈ M) ε⊕a = a⊕ε = a).

Монада — это моноид в категории эндофункторов, параметрический тип данных, контейнер, который обязательно реализует две операции: создание монады (в литературе функция unit) — и функцию flatMap() (в литературе иногда имеет название bind) и подчиняется некоторым правилам. Функция unit отвечает за создание монады и для каждой монады она отличается. Функция flatMap принимает на вход функцию, которая принимает на данные что размещены в монаде и возвращает новую монаду, причем возможно монаду другого типа (U вместо T).

trait Monad[A] {
    def apply(a: A): Monad[A]
    def flatMap[B](f: A => Monad[B]): Monad[B]
}

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

  • Left unit law - если применить функцию flatMap для типа с позитивным значением и передать туда некоторую функцию то результат будет такой же, как простое применение этой функции к переменной. unit(x) flatMap f ≡ f(x)
def foo(x: Int) => Some(x + 1)

val n = 10

Some(n).flatMap(foo)
    //equal to
foo(n)
  • Right unit law - если передадим в flatMap функцию которая просто создает монаду из данных (тех что находятся в монаде) — то на выходе мы получаем такую же монаду. monad flatMap unit ≡ monad
def foo(x: Int) => Some(x)

val n = 10

Some(n).flatMap(foo)
    //equal to
Some(n)
  • Associativity law - если передадим в flatMap функцию которая создает монаду и применяет flatMap к ней внутри функции из данных (тех что находятся в монаде) — то это тоже, что и поочередно применять flatMap к верхнеуровневой монаде с внешней и внутренней функцией, соответственно. (monad flatMap f) flatMap g ≡ monad flatMap(x => f(x) flatMap g)
def foo1(x: Int) => Some(x + 1)
def foo2(x: Int) => Some(x * 2)

val n = 10

Some(n) flatMap { foo1(_).flatMap(foo2) }
    //equal to
Some(n) flatMap { foo1 } flatMap { foo2 }

Существует два вида функторов: ковариантные и контрвариантные. Здесь рассмотрены только ковариантные. Функтором является любой тип данных-контейнер A[T], для которого определен метод map[U](f: T => U): A[R] и выполяются два закона.

  • Identity law - map(identity) ничего не должно менять внутри функтора, где identity — это полиморфная тождественная функция (единичная функция) из Predef.scala.
Some(1).map(identity)
    //equal to
Some(1)
  • Composition law - произвольный функтор-контейнер, который последовательно отображают функцией ‘f’ и потом функцией ‘g’ эквивалентен тому, что мы строим новую функцию-композицию функций f и g (f andThen g) и отображаем один раз.
def foo = (_: Int) + 1

Some(1).map(foo1).map(foo2)
    //equal to
Some(1).map(foo2(foo1))

Для монады и функтора должен выполнятся нулевой закон (the zeroth law) - m map f ≡ m flatMap { x => unit(f(x)) }.

Some(1).map(foo)
//equal to
Some(1).flatMap(i => Some(foo(i)))

Некоторые монады могут иметь нулевое значение, то есть содержать отсутствие значение, такие монады называются монадическими нулями (monadic zeros). Некоторые монады Scala могут иметь два конкретных типа контейнера, один - контейнер позитивного значения, а второй - негативного. Такие монады называют Result - интерфейс, определяющий тип монады и два конкретных класса-наследника трейта. Для монадических нулей должны выполнятся два закона:

  • Left unit law - результатом применения функции flatMap будет такой же монадический нуль. mzero flatMap f ≡ mzero.
None flatMap { Some(_) }
    //equal to
None
  • Plus - результатом сложения позитивной монады и монадического нуля будет позитивная монада. mzero plus m ≡ m.

None orElse Some(1)
    //equal to
Some(1)

Option

Option[T] - монада типа Result, имеющая два представления.

  • Some[T] - контейнер положительного значения.
  • None - монадический нуль.

Тип представляет из себя монаду, в которой значение или есть, или нет. Получить элесент можно с помощью методов get и getOrElse. Операцией плюс называют метод OrElse(f: => Option[T]): Option[T].

Try

Try[T] - монада типа Result из пакета scala.util, имеющая два представления.

  • Success[T] - контейнер положительного значения.
  • Failure[T < Throwable] - контейнер негативного значения.

Тип представляет возможность безопасно обрабатывать исключения. Метод apply объекта-компаньона Try принимает фукнцию типа => T, случае успешного завершения которой вернется объект-контейнер типа Success[T], содержащий результат выполнения функции, а в случае возникновения ошибки Failure[T <: Throwable], содержащий ошибку, которая произошла. Можно воспользоваться сопоставлением с образцом. Операцие плюс называют метод recover[U :> T](f: PartialFunction[Throwable, U]: Try[U]. Получить элемент можно с помощью метода get и getOrElse.

def foo(m: Array[Int]): Try[Int] = {
    import scala.util.Try

    Try {
        m(3)
    } recover {
        case e: ArrayIndexOutOfBoundsException => 0
        case e => throw e
    }
}

def foo2(m: Array[Int]): Option[Int] = {
    import scala.util.Try

    Try {
        m(3)
    } match {
        case Success(i) => Some(i)
        case Failure(e) => None
    }
}
scala> foo(Array(1,2,3,4,5,6,7,8,9))
res6: scala.util.Try[Int] = Success(4)

scala> foo(Array(1,2,3))
res7: scala.util.Try[Int] = Success(0)

scala> foo2(Array(0))
res9: Option[Int] = None

scala> foo2(Array(1,2,3,4))
res10: Option[Int] = Some(4)

Either

Either[A, B] - монада типа Result из пакета scala.util, имеющая два представления.

  • Left[A] - контейнер положительного значения.
  • Right[B] - контейнер негативного значения.

Методы проверки это isRight и isLeft. Для того, чтобы работать с контейнером необходимо получить проекцию контейнера, это делается методами left и right, которые возвращают объекты монадического типа.

def connect(url: String): Either[Int, String] = {
    import scala.io.Source
    import java.net.URL

    Try {
        Source.fromURL(new URL(url))
    } match {
        case Success(source) => Right(source.mkString)
        case Failure(e) => Left(10)
    }
}
scala>connect("https://www.ya.ru")
res94: scala.util.Either[Int,String] = Right("https://www.ya.ru content")
scala> connect("https://ww.ya.ru")
res95: scala.util.Either[Int,String] = Left(10)

scala> connect("https://ww.ya.ru").left.map(_ / 10).left.get
res101: Int = 1

Future

Тип Future[T], определённый в scala.concurrent пакете — это коллекция, представляющий вычисление, которое когда-нибудь закончится и вернёт значение типа T. Вычисление может закончиться с ошибкой или не буть вычисленным в поставленные временные рамки. Если что-то пойдёт не так, то результат будет содержать исключение. Метод apply объекта-компаньона принимает два аргумента apply[T](body: => T)(implicit execctx: ExecutionContext): Future[T]. В параметр body по имени передаётся вычисление, которое будет выполняться асинхронно. Второй параметр — неявный параметр контекста вычисления, это означает, что мы можем не передавать его явно, если значение с таким типом определено в той же области видимости переменных. Заботиться о ExecutionContext нужно только в случае блокирующих вызовов, что не приветствуются в Scala в общем. В общем случае для этого мы импортируем глобальный контекст вычисления import scala.concurrent.ExecutionContext.Implicits.global. Future является монадой, поэтому определены все монадические методы:

  • flatMap - удобно использовать, если одно асинхронное вычисление зависит от другого асинхронного вычисления, можно заменить циклом for.
  • onFailure[U](pf: PartialFunction[Throwable, U]): Unit - выполняет некоторые действия при возникновении ошибки.
  • onSuccess[U](pf: PartialFunction[T, U]): Unit - выполняет некоторые действия при успешном выполнении, в качестве аргумента частично определенной функции принимает результат вычисления Future.
  • onComplete[U](f: Try[T] => U): Unit - комбинирует onSuccess и onFailure.
  • value: Option[Try[T]] - возвращает результат вычисления.
val f = Future {
    10 + 20
}

f.onFailure {
    case e: NullPointerException => println(e.toString)
}

f.onSuccess {
    case v: Int => println(v + 10)
    case v => print(v)
}

f.onComplete {
    case Success(v) => println(v)
    case Failure(e) => println(e.toString)
}

В то время как Future предоставляет методы только для чтения, тип Promise[T] позволяет завершить вычисление Future записью значения. Значение может быть записано только один раз. Тип Promise обещает, что тут будет значение типа T, как только обещание (promise) было исполнено мы не можем его изменить. Значение типа Promise всегда связано лишь с одним значением типа Future.

  • success(v: T): Promise[T] - запишет значение как успешное.
  • failure(e: Throwable): Promise[T] - запишет значение-исключение.
  • future: Future[T] - запишет Future с результатом из Promise.
val amount = Promise[Float]() // (1)

Future { // (2)
    println("get amount...")
    amount.success(10.55.toFloat) // (3)
}

amount.future.onSuccess { // (4)
    case v => println(v)
}

Что происходит в примере выше:

  • 1) Дается обещание, что у amount будет значение Float.
  • 2) Происходит некая асинхранная операция.
  • 3) Внутри операции записывается некоторое значение в Promise.
  • 4) Используется связный с amount Future, onSuccess выполниться, когда у amount появится значение.

Чтобы блокировать вычисление (дождаться его результата) применяется объект Await. У него есть два метода:

  • ready(f: Awaitable[T], duration: Duration): Awaitable[T] - блокирует вычисление и возвращает контейнер при вычислении (Future или Promise).
  • result(f: Awaitable[T], duration: Duration): T - блокирует вычисление и возвращает ео результат.

Duration - объект, передающий время ожидания, после чего бросается TimeoutException.

Акторы

Модель акторов

Модель акторов представляет собой математическую модель параллельных вычислений, которая трактует понятие «актор» как универсальный примитив параллельного численного расчёта: в ответ на сообщения, которые он получает, актор может принимать локальные решения, создавать новые акторы, посылать свои сообщения, а также устанавливать, как следует реагировать на последующие сообщения. Основная идея в том, что приложение построено из многих легковесных процессов, называемых акторами. Каждый актор отвечает за одну очень маленькую задачу, поэтому нам легко понять, что он делает. Более сложная логика возникает из взаимодействия нескольких акторов, мы решаем задачи с помощью одних акторов, в это время посылаем сообщения совокупности других. Стандартная библиотека акторов в Scala считается устаревшей, рекомендуется использовать акторы akka.

Создание акторов

Актор - это класс, наследующий трейт Actor из пакета scala.actors. Этот трейт имеет один абстрактный метод act: Unit, который необходимо реализовать. Запустить актор можно с помощью метода start(): Unit, после этого метод act запустится в отдельном потоке. У объекта-компаньона Actor существует метод actor, который принимает функцию =>Unit, и сразу же запускает актор, внутри себя он зранит ссылку в виде self.

object ActorTest extends App {
    println("Start")

    val actor = new Printer
    actor.start()

    import scala.actors.Actor

    class Printer extends Actor {
        def act(): Unit = {
            while (true) {
                receive {
                    case _ => println("Hello")
                }
            }
        }
    }
}
import scala.actors.Actor._

val actor2 = actor {
    while (true) {
        receive {
            case _ => println("Hello")
        }
    }
}

Актор - это объект, обрабатывающий асинхронные сообщения. Отправить сообщение актору можно с помощью метода !(msg: Any). После отправки сообщения актору поток продолжает свою работу. В качестве сообщений зачастую используются case-классы. Помимо этого, можно передать ссылку на другой актор, чтобы отправить ему результаты обработки, например.

actor ! StdIn.readLine()

Сообщения, посылаемые актору попадают в его почтовый ящик, а при вызове метода receive(PartialFunction[Any, T]) сообщение передается частично определенной функции, которая обрабатывает пришедшее сообщение. Актор выполняется в одном потоке, поэтому сообщения обрабатываются по одному, причем вызов метода receive может быть заблокирован до следующего подходящего сообщения, поэтому можно использовать конструкцию case _ для обработки произвольных сообщений.

Каналы

Вместо ссылок на акторы можно использовать каналы. Это дает два преимущества:

  • Безопасность на уровне типов. Каналы обрабатывают только сообщения определенного типа.
  • Исключают возможность случайного вызова актора.

Каналы определены двумя трейтами OutputChannel[T] с методом ! и InputChannel[T] с методом receive. Класс Channel реализует оба эти трейта. В коструктор можно передать актор, с которым связан канал. Если ничего не перать, то будет использован текущий актор. Акторам, которые должны возвращать результат, как правило передают OutputChannel.

Синхронные сообщения

Можно отправить сообщение актору и ждать ответа от него. Это деалется с помощью метода !?. Актор в методе receive должен возвращать сообщение с помощью конструкций sender ! answer, где sender - имя метода, или же с помощью метода reply(answer: T). Чтобы указать максимальное время выполнения необходимо вместо метода receive использовать receiveWithin С помощью метода !! вернет объект типа scala.actors.FutureActor, значение которого можно получить с помощью метода apply.

import scala.actors.Actor._
import scala.io.StdIn

object ActorTest extends App {
    val reader = actor {
        while (true) {
            receiveWithin(1000) { //Ждет секунду и принимает scala.actors.TIMEOUT
                case (msg: String) => sender ! "* " + msg
                case scala.actors.TIMEOUT => reply("timeout")
                case _ => println("What?")
            }
        }
    }

    reader.start()

    while (true) {
        val % = reader !? (StdIn.readLine())
        println(%)
        val @% = reader !! (StdIn.readLine())
        println(@%())
    }
}

Метод react(PartialFunction) связывает почтовый ящик актора и частично определенную функцию. При совпадении может быть вызвана и еще одна функция react. react нижнего уровня должен вызывать act, либо необходимо начать act с объявления loop(f: =>Unit) или loopWhile(p: Boolean)(f: =>Unit). Метод eventloop(pf: PartialFunction) является сочетанием loop и react.

import scala.actors.Actor._

object ActorTest extends App {
    val reader = new scala.actors.Actor {
        override def act() = {
            loop {
                react {
                    case _ => println("hello")
                }
            }
        }
    }

    reader.start()

    while (true) {
        reader ! "a"
    }
}

Для завершения работы актора можно вызвать метод exit(cause: String).

Связывание акторов

Можно связать два актора, тогда каждый из них получит оповещение, если другой завершит свою работу. На основе этого можно построить систему, где главный актор следит за работой своих подчиненных. По-умолчанию актор завершает свою работу, если связный закончил свою работу по причине, отличной от normal. Избежать этого можно, указав trapExit = true.

import scala.actors.Actor._
import scala.actors.Exit

object ActorTest extends Application {
    def main = {
        val reader = new scala.actors.Actor {
            override def act() = {
                trapExit = true
                loop {
                    react {
                        case Exit(linkedActor, reason) =>  println(reason.toString)
                        case _ => println("hello")
                    }
                }
            }
        }

        reader.start()

        while (true) {
            reader ! "a"
        }
    }
}