[Конспект] Scala
Основы
Интерпретатор
Scala имеет интерпретатор командной строки REPL. По сути он не является интерпретатором, а представляет из себя программу, которая считывает введенные данные, компилирует их в байт-код JVM, выполняет и возвращяет результат. REPL - read-eval-print loop. REPL включается в себя некоторые способности командной строки, например автозавершение команд по нажатию TAB. Чтобы воспользоваться программой небходимо ввести программу scala с командной строки.
Объявление значений и переменных
Если попробовать ввести в REPL число, например, то увидим нечто похожее.
REPL говорит нам, что записал 12 в значение (val) res0. Теперь можно обратиться к данному значению.
Вместо имен “resN” можно использовать свои имена переменных. Если переменную объявить как val
, то переменная будет значением, то есть недоступна для изменения или перезаписи. А вот если объявить как var
, то это будет самая настоящая переменная с возможностью перезаписи и изменения. Если переменная была объявлена как ленивая lazy
, то вычислена она будет при первом обращении к ней.
Как можно обнаружить REPL отвечает нам как varName: varType
, в Scala тип всегда указывается через через двоеточие. Можно всегда указывать явно, хотя в большинстве случаев Scala может вычислить самостоятельно.
Как можно было заметить, точка с запятой в конце строки не указывается. Это обязательно только в случах, когда на одной строке находятся несколько инструкций. Можно объявить несколько переменных за раз через запятую.
Если возникает необходимость использовать в качестве имени переменной зарезервированное слово, то необходимо обернуть его в обратные кавычки.
Часто используемые типы.
Все типы в Scala являются классами, поэтому не никакой разницы между простым типом и классом. Можно вызывать метод непостредственно у числа, например.
Можно выделить 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 или \tString
- строка, простейшими литералами являются двойные кавычки. Использование:"
- можно использовать специальные символы переноса строки и прочих."""
- многострочная строка.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 позволяет использовать в качестве имен любые символы, поэтому “+” - это имя метода. Основные правила вызова функций и методов:
- Если аргумент один, то () можно заменить на {}.
- Если аргумент один и используется “operator notation”, то () не использовать.
- Если функция не принимает аргументов, то () можно не использовать. Общие правила здесь таковы: если метод изменяет объект, то скобки все-таки необходимо использовать, иначе не стоит.
- Если имя метода заканчивается на “:”, то метод правоассоциативен, то есть в “operator notation” параметр будет слева.
Еще один важный момент. В Scala не операторов ++ и –, вместо них используются +=1 и -=1.
Метод apply
Scala, являясь функциональным языком программирования, подталкивает к использованию синтаксиса, напоминающего вызов функций.
В действительности это является неявным вызовом метода apply
. В частности, определения метода является типичной идиомой при конструировании объекта-компаньона.
Управляющие конструкции и функции
Условные выражения
Синтаксис конструкций if/else if/else аналогичен Java/C++, за одним приятным исключением. Все управляющие конструкции в Scala возвращают значение. Отсюда вытекает, то что выражения имеют тип и могут быть присвоены в переменную.
В иерархии типов Scala общим родительским является тип Any, общим дочерним Nothing, а за тип void отвечает Unit, который имеет значение ().
При присвоении результата в переменную используется значение последнего выражения, именно поэтому может возникнуть ситуация, когда вернется значение типа Unit.
Ввод и вывод
Для вывода значения используется несколько функций.
print
- обычный вывод.println
- вывод строки.printf
- функция из C.
Для чтения введенных данных используется группа функций read. readLine(str: String)
- чтение строки, а readType
, где “Type” - имя типа, для чтения параметров определенного типа.
Циклы
Самое первое, что необходимо узнать о циклах в Scala, здесь нет инструкций break и continue. Есть альтернативная функция break из пакета scala.util.control.Breaks
, но его не рекомендуется использовать из-за скорости выполнения.
В Scala есть стандартные циклы while
и do/while
.
В Scala нет цикла типа for(init;check;update), но есть конструкция for (variable <- expr)
, это обеспечивает последовательное присвоение в variable итерируемого значения. У объекта RichInt есть два метода, которые возвращают объекты типа Range, являющиеся итерируемыми. Метод n to N
вернет в числа от n до N вклюичтельно, а метод until
невключительно.
Стоит определить термины отностительно циклов for.
- for (
variable <- iterator
) - генератор. - for (variable <- iterator;
variable2 = variable + 10
) - определение. - for (variable <- iterator
if booleanExpression
) - ограничитель.
Допускается использовать несколько генераторов, определений и ограничителей, разделенных между собой точкой с запятой или определенных с новой строки. При этом генераторы будут выполнятся как вложенные циклы слева-направо.
В определениях переменные являются var. Ограничители выглядят как if booleanExpression, итерация запуститься только в случае выполнения условий. Каждый ограничитель следует за генератором, которому он принадлежит, и не должен содержать переменные, находящиеся в объяалении цикла после него.
Если тело цикла начинается с ключевого yield
, то цикл вернет коллекцию.
Генераторы, определения и ограничители можно поместить в фигурные скобки и использовать перенос строк вместо точек с запятой.
Функции
В отличии от Java/C++ в Scala есть функции. Функция определяется с помощью ключевого слова def
. Функция состоит из имени, параметров и тела, можно указать тип параметра и возвращаемого значения.
Несколько свойств функций.
- Возвращаемым значением является последнее выражение.
- Возвращаемое значение можно опустить, но обязательно указывать при рекурсии.
- Использовать return необходимо только в случаях немедленного выхода из функции.
- Если функция возвращает значение типа Unit, то функцию называют
процедурой
, и знак = можно опустить.
Аргументам можно задать значение по-умолчанию через =.
Также при вызове функции можно указывать какие именно параметры передаются.
При одновременном использовании именованных и неименованных аргументов сначала указывают неименованные.
Функция может принимать переменное число аргументов, для этого следует указать тип аргумента как Type*
.
Внутри функции переменная args будет доступна как последовательность Seq. Можно напрямую в качестве передать коллекцию, но тогда необходимо сообщить компилятору, что это именно последовательность через уточнение типа: _*
.
Исключения
Бросить исключение в Scala можно также как и в других языках, то есть throw new Exception
. А вот перехват отличается, помимо конструкции try/catch/finally
необходимо знать, что catch-блок должен состоять из инструкций pattern matching, о котором будет говориться в следующих главах. Пока достаточно рассмотреть пример и запомнить его синтаксис.
Первый блок case перехватывает все MalformedException и выполняет блок за =>
. Второй case перехватывает все WrongArgumentException и присваивает в переменную ex. А последний case выполняется для всех остальный исключений.
Массивы
Индексные массивы
Массивы бывают фиксированной и переменной длины, они типизированы типом, который их наполняет, тип указывается в квадратных скобках. Массивом фиксированой длины является тип Array. Для инициализации пустого массива определенной длины используется конструкция new Array[T](length: Int)
. Для инициализации наполненного массива конструкция Array(value1, value2, ... valueN)
.
Для доступа к элементам массива используются круглые скобки.
Массивом переменной длины является тип 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 значения.
Для конвертации этих массивов используются методы toArray/toBuffer
.
Обойти массивы можно несколькими способами.
Можно использовать yield
для создания новых коллекций. Например, удвоить элементы.
Для подобных типичный операций были предусмотрены методы.
sum
- вернет сумму элементов.size/length
- вернет количество элементов.max
- вернет больший элемент.min
- вернет меньший элемент.mkString(delimiter: String)
- вернет строку с элементами и разделителем.
Создать матрицу можно с помощью метода Array.ofDim[T](row: Int, col: Int)
.
Ассоциативные массивы
Ассоциативные массивы в Scala представлены классами Map[KeyType, ValueType]
для неизменяемых массивов, и scala.collection.mutable.Map[KeyType, ValueType]
для изменяемых. Ассоциативные массивы представляют из себя коллекцию пар. Пару можно создать через (Value1, Value2)
или Value1 -> Value2
.
Неизменяемые массивы можно создать только одним способом, через вызов apply объекта-компаньона.
По-аналогии можно создать наполненный изменяемый массив, а чтобы создать пустой необходимо воспользоваться классом scala.collection.mutable.HashMap
.
Доступ к элементам осуществляется также, как и в индексных массивах.
Проверить существование ключа можно с помощью метода contains(key: T)
. И чтобы избавиться от постоянных проверок на существование был добавлен метод getOrElse(key: T, defaultValue: T2)
У ассоциативных массивов есть метод get(key: T)
, который возвращает объект типа Option, который представлен типами Some(Type) и None. Так, если ключ не будет найден, то метод get вернет None, иначе Some(value).
Изменять значения можно только в изменяемых ассоциативных массивах.
Несколько методов для изменяемых массивов.
+=
- добавить одну или несколько пар к массиву.-=
- удалить пару из массива.
Чтобы обойти ассоциативные массивы необходимо использовать цикл for и pattern matching.
По-умолчанию внутреняя реализация массива представляет из себя хеш-таблицу, чтобы создать сортированный массив необходимо создать его на основе сбалансированного дерева с помощью класса SortedMap.
Кортежи
Пара - это простейший случай кортежа. Кортеж (Tuple) создается с помощью круглых скобок, а его типом будет TupleN[Type1, Type2 ... TypeN]
.
Обратиться к элементам кортежа можно с помощью методов _n
, где n - номер элемента в кортеже начиная с 1.
Также удобно использовать сопоставление с образцом.
Например, у индексного массива, есть метод zip(another: Array[T])
, который вернет массив пар.
Классы
Объявление классов
Классы в Scala объявляются подобно классам в других языках, с помощью ключевого слова class
. Свойства и методы объявляются внутри класса также как и переменые и функции.
Для создания экземпляра класса используется ключевое слово new
. По-умолчанию все свойства и методы являются публичными, а ключевого слова public нет в принципе. Все поля должны быть инициализированы. Обратиться к свойству или методу можно через точку, а к своим методам изнутри как с использованием this, как и напрямую как функции или переменной.
Если метод не принимает параметров, то скобки вызова можно опустить. Есть негласное правило: “Если метод изменяет внутреннее состояние объекта (метод-мутатор), то скобки указывать необходимо, а для методов-акцессоров необходимо опустить”. Можно обязать вызывать метод без скобок, если не указать их при объявлении.
Методы доступа
Свойства и методы можно сделать недоступными из вне, для этого их необходимо сделать приватными с помощью ключевого слова private
или защищенными (доступными в наследниках класса) protected
. При этом можно оставить доступ через методы доступа. При объявлении свойства публичным Scala неявно создает два метода valName
- getter и valName_=(value: Type)
- setter. В этом можно убедиться, если скомпилировать класс через scalac и посмотреть байт код через javap -private.
Эти методы можно переопределить.
Методы доступаются генерируются по некоторым правилам.
- Если поле private, то и методы private.
- Если поле val, то генерируется только getter.
- Если поле private[this], то методы не генерируются вообще.
Когда поле объявляется как private, то поле будет доступно только внутри класса. Именно класса, то есть экземпляры одного класса будет иметь доступ к приватным методам и свойствам друг друга. Чтобы ограничить видимость внутри экземпляра, следует объявить метод или свойство как private[this]
.
Последний метод name_? становится не валидным в данном случае. В квадратных скобках также можно указать на внешний класс, если речь идет о вложенных классах.
Классическим методом именования методов доступа являются имена setXxx и getXxx, чтобы добавить поддержку необходимо перед именем свойства объявить аннотацию @BeanProperty
из пакета scala.beans
. Правила генерации аналогичны вышесказанным.
Конструкторы
Классы в Scala имеют один главный конструктор
и множество дополнительных. Дополнительные конструкторы имеют имя this, и каждый следующий должен вызывать вышестоящий. Причем главный конструктор есть всегда, по-умолчанию просто не принимает аргументов.
Главый конструктор вплетается в определение класса. Его параметры следуют сразу за именем класса, причем если аргумент объявлен как (private) val/var, то он автоматически становиться полем класса. Также при конструировании выполняются все инструкции внутри класса.
Если аргумент используется хотя бы в одном методе, то он становится private[this] свойством. Можно объявить главный конструктор как private, тогда клиент будет обязан использовать дополнительные конструкторы.
Вложенные классы
Scala позволяет вкладывать один класс в другой.
Создать внутренний класс можно через new variable.InnerClass
. Для каждого объекта вложенный объект будет иметь свой тип (varName.InnerClass).
Чтобы обойти это можно использовать проекцию типов, определяя тип как Outer#Inner
. Тогда внутренний тип будет определен, как конкретный тип любого внешнего типа.
Чтобы обратиться к вышестоящему классу необходимо воспользоваться определением собственного типа.
Объекты
Singleton
В Scala нет статических методов и свойств. На замену им приходят объекты. Объекты объявляются с помощью ключевого слова object
. У объекта не может быть экземпляра, соответственно конструктора, объект является инициализированной сущностью.
Объект является реализацией шаблона проектирования Singleton, он может наследовать другие классы и трейты, но не может иметь наследников.
Когда объект имеет одноименный класс, то он является объектом-компаньоном
. Такие сущности должны определяться в одном файле, они имеют доступ к приватным методам и свойствам друг друга.
Очень часто объект компаньон содержит в себе метод apply, создающий экземпляр класса-компаньона.
Объект, представляющий приложение
Выполнение каждой программы начинается с вызова метода main(args: Array[String])
на объекте представляющего приложение.
Можно подмешать трейт App
и поместить весь код программы в тело объекта, при этом к аргументам можно будет обратиться через свойство args.
Enumeration
Чтобы создать перечисление необходимо подмешать трейт Enumeration к объекту. Каждое значение объявляется как val Name = Value
. Каждое значение константы представляет из себя тип EnumName.Value
. Константы создаются с помощью метода Value также.
Это вызывает путаницу, но все-таки имя Value имеет значения:
- Метод трейта Enumeration, которы принимает id и/или имя значения enum’а.
- Внутренний класс Enumeration.
Как было упомянуто метод Value может принимать Int как идентификатор (если не передано, то инкрементируется), или имя (если не передано, то совпадает с именем свойства), или вообще ничего.
В вышестоящем примере типом цвета будет Color.Value
. При передаче в качестве параметра следует использовать именно его.
Можно импортировать значения Enum
и использовать их явно.
Также можно указать псевдоним типа на Enumerator
как Value
и упростить именование. В примере ниже тип каждого значения совпадает с именем объекта.
У класса Value есть два метода. Метод id
вернет числовое значение, а метод toString
имя. У объекта-наследника Enumeration есть методы values
- вернет множество всех значений, apply(id: Int)
- вернет Value по id, withName(name: String)
- вернет Value по имени.
Пакеты
Пакеты
Пакеты применяются для управления именами, так существует класс Map из пакетов scala.collection.immutable
и scala.collection.mutable
. Чтобы добавить элемент в пакет, необходимо объявить его внутри пакета. Объявить пакет можно с помощью ключевого слова package
.
В этом случае класс будет доступен под именем com.google.Android
. В отличии от классов и объектов пакеты можно определять в нескольких файлах, при этом связи между каталогом класса и пакетом нет. В одном файле также может быть определено несколько пакетов.
Правило видимости гласит, что дочерние пакеты могут обращаться к родительским без использования имени пакетов.
Любой пакет неявно имеет корень _root_
, можно начинать создание экземплара класса с _root_. Объявление package может содержать цепочку пакетов.
Если в одном файле используется только один пакет, то его можно определить в начале файла без использования фигурных скобок.
Пакет не может содержать функций и переменных, для реализации этой возможности в Scala были введены объекты пакетов
. Объект пакета имеет тоже имя, что и пакет к которому он относится, объявляется с помощью package object
.
Приватные и защищенные методы можно ограничить областью видимости внутри пакета с помощью квалификатора private[packageName]
. В этом случае методы и свойства будут доступны везде внутри пакета.
Импортирование
С помощью импортирования можно использовать короткие имена вместо длинных.
Можно испортировать все члены пакета, класса или объекта с помощью символа _
.
Импортирую пакет можно обращаться к сущностям из его подпакетов по коротким именам. Импортировать можно в любом месте, при этом область видимости распространяется до конца вмещающего блока. Можно импортировать определенные члены из одного пакета группировкой с помощью фигурных скобок.
Можно дать псевдоним с помощью конструкции {originalName => aliasName}
.
Оставить сущность неимпортированной можно, если задать в качестве имени символ _
.
При инициализации программы происходит неявный импорт.
Пакету scala даны особые права на переопределение имен из пакета java.lang. Predef содержит много полезных функций, хотя их можно было поместить в пакет scala, но пакет Predef появился раньше исторически.
Наследование
Классы
Отнаследовать класс можно с помощью ключевого слова extends. Класс-наследник может обратиться к родительским методам, не помеченным как private.
При переопределении методов обязательно исользовать ключевое слово override
.
Если метод помечен как final
, то его нельзя переопределить. Обратиться к родительскому методу можно с помощью слова super
.
Проверка и приведение типов
Чтобы проверить экземпляр класса на принадлежность к типу необходимо использовать метод isInstanceOf[Type]: Boolean
. Класс наследник является типом класса-родителя.
Чтобы привести класс к типу необходимо воспользоваться методом asInstanceOf[Type]: Type
.
Чтобы проверить на принадлежность к определенному классу без учета родителей необходимо воспользоваться методом getClass
.
И методом объекта Predef classOf[Type]
.
Конструкторы и переопределение полей
Главный конструктор суперкласса может вызвать только главный конструктор подкласса. Параметры передаются напрямую в круглых скобках после имени суперкласса. Конструтор не обязан дополнять родительский, но параметры должны быть переданы. Следует помнить, что конструктор суперкласса вызывается первым.
В подклассе можно переопределить поля и методы суперкласса.
- def может переопределить def.
- val может переопределить val и def без параметров.
- var может переопределить абстрактное var.
Scala позволяет определить анонимные подклассы, которые не имеют имени, но являются наследниками суперкласса. Это делается с помощью заключения в фигурные скобки дополнительных свойств и методов сразу после вызова конструктора супер класса.
В данном случае его тип будет Person{def speak(phrase: String): Unit; def shutUp: Unit}. Это будет объект структурного типа и его можно передать в качестве параметра.
При этом тип совпадет при наличии указанных методов, хотя другие методы по прежнему могут присутствовать, как здесь метод shutUp. Иногда может возникнуть неудобная ситуация при использовании методов в родительском конструкторе.
При вызове родительского конструктора в строке “val seq = for (i <- 1 to bonusCount) yield getBonusId” вызывается метод чтения “bonusCount”, но он переопределен в наследнике, при этом конструктор подкласса еще не был вызван, чтобы выполнить “override val bonusCount: Int = 1” поэтому поле было определено начальным значением, что для Int равно 0.
Есть несколько способов обойти это ограничение. Сделать val финальным, ленивым или использовать опережающее определение
. Его смысл в том, что когда будет обращение к переопределенному методу, то он уже будет реализован.
Абстрактные классы
Абстрактный класс определяется с помощью ключевого слова abstract
, он не может иметь экземпляров, но может содержать свойства и методы без реализации.
При наследовании абстрактного класса override для абстрактного метода или поля писать не нужно.
Чтобы определить абстрактное поле достаточно не задать ему начального значения. При этом будут сгенерированы абстрактные методы доступа согласно правилам. При реализации конкретных полей нет необходимости указывать его тип.
Иерархия классов
Базовым классом, то есть общим суперклассом, для всех классов в 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 не имеет встроенной поддержки работы с бинарными данными, для этого следует исследовать Java инструменты.
Запись
Для записи в файл используется класс java.io.PrintWriter(filename: String)
.
print(str: String)
- запишет строку в файл без переноса строки.println(str: String)
- запишет строку в файл c переноса строки.printf(pattern: String, values: AnyRef*)
- запишет строку в файл подобно функции printf, нотребует именно типа AnyRef
.close
- заканчивает работу с ресурсом (закрывает файл).
Взаимодействие с файлами системы
Встроенных средств для работы с каталогами в scala нет, поэтому следует пользоваться средствами Java.
Для сериализации объектов в Scala необходимо подмешать к классу трейт Serializable
. Коллекции Scala имеют встроенную поддержку, поэтому их можно запросто использовать в качестве свойств. Сериализация и десериализация выполняется стандартными средствами Java.
Взаимодействие с командной оболочкой
Для взаимодействия с командной оболочкой следует использовать пакет scala.sys.process
. Он содержит неявное преобразование строк в ProcessBuilder
.
!: Int
- вернет код завершения команды.!!: String
- вернет результат завершения команды.#
[bash operator] - использовать операраторы консоли (>, >>, && и прочие).
Регулярные выражения
За регулярные выражения отвечает класс 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 могут использоваться как интерфейс Java, при этом они могут иметь как абстрактные методы и свойства, так и конкретные реализации. Объявляется трейт с помощью ключевого слова trait
.
Можно использовать трейт как интерфейс необходимо задать все необходимые абстрактные методы, а также имеется возможность определить свойства как абстрактные. Когда трейт подмешивается
к классу, то для первого трейта это делается с помощью ключевого слова extends
, и with
для всех последующих трейтов. При реализации абстрактных свойств и методов override писать нельзя.
Все интерфейсы Java можно использовать как трейты.
Трейты с конкретными реализациями
Трейты могут содержать конкретную реализацию.
Трейты можно подмешивать к классу на этапе создания объекта, при этом используется ключевое слово with
.
Если добавить несколько трейтов с общим родителем, то имеется возможность в трейтах вызывать методы друг друга, начиная с последнего, с помощью ключевого слова super
. При этом порядок вызова будет зависеть от порядка подмешивания трейтов.
При этом если вызов super ссылается на абстрактный метод, то компилятор вызовет ошибку, что логично, абстрактный метод не имеет реализации.
При этом известно, что метод имеет смысл при определенных условиях подмешивания трейтов, как в примере выше. Чтобы компилятор правильно обрабатывал подобную ситуацию метод следует дополнительно пометить ключевым словом abstract
.
Трейт может содержать одновременно и абстрактные методы и конкретные реализации.
Поля в трейтах
Как уже было сказано, в отличие от Java трейты Scala могут иметь поля, причем как абстрактные, так и конкретные
. Абстрактным полем является поле без начального значения.
Как и везде, при реализации абстрактного поля ключевое слово override не требуется, а при переопределении конкретного поля оно нужно.
Конструирование трейтов
Как и классы, трейты имеют конструктор. Но при этом в трейтах конструктор только один - главный конструктор, не имеющий конструктор. Порядок конструирования трейтов.
- Конструктор суперкласса.
- Конструкторы трейтов слева направо.
- Внутри трейтов - родительские конструкторы выполняются первыми.
- Внутри трейтов - для общего родителя нескольких трейтов, его конструирование выполняется только один раз.
- Конструктор класса.
Здесь поджидает одна ловушка. Если трейт содержит абстрактное поле, и в классе, в который подмешивается трейт, оно не реализовано, то прямое решение - определение при конструировании не будет работать.
Это происходит потому что фактически создается анонимный подкласс, а класс Person with Singer становится супер классом. При инициализации Singer.max в поле octave еще нет значения. Можно использовать опережающее определение для этого случая.
Наследование классов
Трейты могут наследовать классы. При этом суперкласс трейта становится суперклассом класса, подмешающего трейт. При этом класс может иметь свой суперкласс, но это должен быть тот же суперкласс, что и у трейта или же его наследник. Трейт гарантирует, что класс, куда он будет подмешан будет определенного типа. Существует иной способ гарантировать это. Использовать в трейте определение собственного типа
. Для этого следует начать объявление трейта с конструкции this: Type =>
.
Здесь удобно будет использовать структурные типы, которые просто описывают методы, которые должны быть у класса.
Операторы
Инфиксные операторы
Идентификаторами в Scala могут быть любый символы Unicode. Если необходимо использовать ключевое слово в качестве идентификатора, то его необходимо поместить в обратные кавычки.
Выражение называется инфиксным, если оператор находится между двумя аргументами. Методы, которые принимают один аргументы можно записывать в инфиксной форме. То есть использовать вместо a.op(b)
- a op b
Унарные операторы
Оператор называют унарным, если он принимает один параметр. Все методы, которые не принимают параметров являются постфиксными унарными операторами
. Префиксных унарных операторов всего 4: +, -, !, ~
, а именами методов для них является unary_OPERATOR
.
Приоритет операторов
Инфиксные операторы более приоритены, чем постфиксные. Приоритет операторов, начинающихся с символов таков.
- Все символы, отличные от менее приоритетных.
-
- / %
-
- -
- :
-
<
- ! =
- &
-
- Операторы присвоения. a op= b (a = a op b)
Приоритет операторов левоассоциативен
, то есть при равенстве операторов они будут выполнятся слева-направо. Кроме операторов, которые заканчиваются на двоеточие и операторов присвоения, для них объект, на котором вызывается метод находится справа.
Apply, update и unapply
При попытки вызвать не функцию вызывается метод apply
, а при попытке приравнять такое выражение к чему-либо вызывается метод update
.
Можно определить сколько угодно различных методов apply с различными сигнатурами.
Объект с методом unapply
называется экстракторами
. Unapply является противоположностью метода apply, принимает в качестве аргумента объект некоторого типа, а возвращает набор значений. Используется при объявлении переменных и при сопоставлении с образцом. Резутат следует обернуть в тип Option, тогда в случае успеха (тип Some) сработает совпадение с образцом. При этом есть возможность воспользоваться wildcard для извлечения только конкретных значений.
Метод unapply може принимать любой тип аргументов и возвращать кортеж любой допустимой длины. Также возможно объявить несколько методов в различной сигнатурой.
- unapply(object: S): Option[T] - при совпадении с образцом S(t).
- unapply(object: S): Option[(T1, T2 .. Tn)] - при совпадении с образцом S(t1, t2 .. tn). - unapply(object: S): Boolean - при совпадении с образцом S().
Экстракторы могут извлекать и произвольное число значений. Если необходимо определить экстрактор, который принимает объекты определённого типа и возвращает коллекцию значений, длина которой неизвестна на этапе компиляции, то следует воспользоваться методом unapplySeq(object: S): Option[Seq[T]]
.
Функции высшего порядка
Функции
В Scala функции являются объектами, их можно передавать в качестве параметра, вызывать в качестве переменной. Для того, чтобы присвоить уже существующую функцию в переменную, после имени необходимо указать _
.
Функции необязательно давать имена, тогда функция называется анонимной
. Она объявляется по форме.
(arg1: Type1, arg2: Type2): ReturnType => Body
- в общем виде.(arg1, arg2): ReturnType => Body
- если не требуется уточнение типа, например функция принимается в качестве параметра.(arg1: Type1) => Body
- если возвращаемый тип не требует уточнения.arg: Type => Body
- с одним аргументом.Body operation (_: Type)
-_
единственный аргумент, который используется в одном месте.Body operation _
-_
единственный аргумент, который используется в одном месте и не требует уточнения типа.() => Body
- без параметров.
В функции нет необходимость пользоваться return
, тем не менее эта возможность остается.
Для композиции функций в Scala определены две функции. Если есть две функции f и g, выражение f.compose(g)
вернёт новую функцию, которая при вызове сначала выполнит функцию g и затем применит f к результату. Аналогично f.andThen(g)
вернёт функцию, которая сначала вызовет f и затем g, на результате, который был получен из f.
Функции высшего порядка
При передаче функции в качестве параметра необходимо правильно указать её тип. Тип указывается как (parameterType) => returnType
. Функции, принимающие в качестве аргумента или возвращающие другую функцию называются функциями высшего порядка
.
Функция может передать параметр из одной область видимости в другую, используя замыкания
.
В частности можно не присваивать возвращенную функцию в переменную, а использовать сразу, воспользовавшись каррированием
.
Зачастую такие функции обозначаются как function(agr: Type)(arg: Type): ReturnType
. лбую функцию можно привратить в каррированную с помощью метода curried
.
Абстракция управляющих конструкций
При использовании функций без параметров и возвращаемого значения можно использовать две нотации.
Здесь при передаче аргумента придется использовать конструкцию типа () => block
, из-за указаных скобок в объявлении высшей функции.
Здесь достаточно будет просто передать сам блок функции. Так можно создавать абстракции управляющих конструкций.
Коллекции
Основные трейты коллекций
Верхним трейтом иерархии коллекций является 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
- добавляет строку к построителю строк.
Iterator позволяет обходить его элементы в цикле, при этом имеет методы hasNext: Boolean
, который проверяет, есть ли следующий элемент, а также метод next(): A
, который возвращает следующий элемент и передвигает счетчик.
Трейт 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 потомков
Range
- представляет последовательность целых чисел, создается с помощью начального значения, конечного и шага (по-умолчанию 1). Методы, используемые для создания.(start: Int) to (end: Int) by (step: Int)
- от и до с шагом включительно.until
- аналог to, но с исключением конечного значения.
NumericRange[T]
- аналог Range для действительных чисел.
Array[T]
иString
- не являются наследниками IndexedSeq, поскольку берутся из Java, при этом неявно преобразуются в IndexedSeq.
Наиболее важным изменяемым наследником IndexedSeq является ArraySeq[T]
- массив, элементы которого можно изменить с помощью update.
LinearSeq предоставляет быстрый доступ к первому элементу при доступе к голове последовательности, а также быстрые операции с хвостом. Неизменяемые наследники LinearSeq:
List[T]
- представлен значениемNil
, который представляет пустой список, или объектом List. Методы List:head: T
- возвращает головной элемент списка.tail: List[T]
- возвращает хвост списка- Список можно создать с помощью оператора
::
, который также может быть использован при сопоставлении с образцом.
Stream[T]
- список, хвост которого вычисляется только по-требованию. Подобно List имеет методы head и tail. Методы Stream:take(n: Int): Stream[T]
- возвращает поток с заданным количеством элементов.force: Stream[T]
- возвращает поток со всеми элементами.- Список можно создать с помощью оператора
#::
, который также может быть использован при сопоставлении с образцом.
Queue[T]
- передставляет коллекцию, представленую в виде очереди (“первый пришел - первый вышел”). Методы Queue:enqueue(e: T): Queue[T]
- создает новую очередь с заданным элементом в хвосте.dequeue: (T, Queue[T])
- возвращает пару элемент-очередь, где очередь без первого элемента.
-
Stack[T]
- передставляет коллекцию, представленую в виде стэка (“последний пришел - первый вышел”). Методы Stack:
- pop: Stack[T]
- возвращает коллекцию без головного элемента.
- push(e: T): Stack[T]
- возвращает коллекцию с новым головным элементом.
К основным изменяемым класса, наследникам LinearSeq следует отнести два класса.
MutableList
- изменяемый список. Помимо методов списка, имеет собственные методы:+=(v: T)
- добавляет элемент в конец списка.update(key: Int, v: T)
- заменяет элемент по индексу, как у массива.apply(key: Int)
- возвращает элемент по индексу, как у массива.
LinkedList
- изменяемый список. Помимо методов списка, имеет собственные методы:elem(v: T): T
- заменяет голову списка.next(v: LinkedList[T])
- заменяет хвост списка.
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])
- добавляет элементы коллекции в начало.
ListBuffer[T]
- изменяемый список. Методы такие же, как и у ArrayBuffer.
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]
- объединение коллекций (как ++).&(elem: Set[A]): Set[A]
- пересечение коллекций.&~(elem: Set[A]): Set[A]
- разность коллекций (как –).
Set имеет наследников:
HashSet[T]
- набор, представленный в виде хэш-дерева.SortedSet[T]
- сортированный набор, представлен в виде красно-черного дерева.BitSet
- множество целых чисел, где i-бит равен единице, если число присутствует в множестве.ListSet[T]
- набор, представленный в виде списка.
Помимо изменяемых аналогов вышеуказанных классов Set имеет наследника LinkedHashSet[T] из пакета scala.collection.mutable, который сохраняет порядок добавления элементов.
Дополнительные методы
Методы изменяемых коллекций:
+=(e: T)
- добавляет элемент (может принимать кортеж).++=(e: Iterable[T])
- добавляет элементы (может принимать кортеж).-=(e: T)
- удаляет элемент (может принимать кортеж).--=(e: Iterable[T])
- удаляет элементы (может принимать кортеж).
У трейта Iterable существует еще несколько методов. Методы, принимающие одноместную функцию:
map(f: T => T2): Travesable[T2]
- возвращает коллекцию с измененными элементами.
flatMap(f: T => Traversable[T2]): Travesable[T2]
- принимает функцию, которая работает с вложенными списками и объединяет результаты в единую коллекцию.
collect(pf: PartialFunction[T, T2]): Travesable[T2]
- принимает частично определенную функцию, которая коллекцию, в которую включены только значения, для которых она определена.
foreach(f: T => T2): Unit
- применяет переданую функцию поочередно ко всем элементам коллекции.
Методы, принимающие двуместную функцию:
reduceLeft(f: (T, T) => T2): T2
- поочередно применяет операцию к элементам, обходя слева направо, подставляя в качестве левого элемента результат предыдущего вычисления операции.
reduceRight(f: (T, T) => T2): T2
- как reduceLeft, но справа налево.foldLeft(init: T)(f: (T, T) => T2): T2
- подобна reduceLeft, но устанавливается начальное значение. Также может быть записана в форме/:
.
foldRight(init: T)(f: (T, T) => T2): T2
- как foldLeft, но справа налево. Также может быть записана в форме:\
.scanLeft(init: T)(f: (T, T) => T2): Traversable[T2]
- подобна foldLeft, но возвращает коллекцию всех промежуточных результатов.
-
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, но не имеет даже первого элемента.
Взаимодействие с 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 =>
.
Для совпавдения с любым значением используется _
, иначе вызывается исключение MatchError.
В выражении case могут использоваться конкретные значения, типы, шаблоны. Если за ключевым словом case следует имя переменной, то при совпадении результат присвоиться этой переменной.
В сопоставлении с образцом можно использовать ограничители
. Конструкция if b: Boolean
ставится перед =>, тогда совпадение, помимо прочего, произойдет, если в ограничителе истина.
При сопоставлении с массивами имеется возможность сравнить только определенную часть с помощью _
и _
. При использовании _ элемент найдется и никуда не запишется, а _ может использоваться только в конце, все оставшиеся значения отпавятся в эту переменную при совпадении.
В одном case можно указать несколько вариантов с помощью оператора |
.
Можно создавать псевдонимы, при сопоставлениями в case-классами с помощью аннотации @
.
Совпадение с образцом можно использовать в циклах for.
Case классы
Для сопоставления с образцом используется метод unapply. Case классы объявляются с помощью ключевого слова case
. Они имеют следующие свойства:
- Каждый параметр конструктора автоматически становится val, если только явно не обозначен, как var.
- Создается объект-компаньон с методом apply.
- Создается метод unapply.
- Генерируются методы toString, equals, hashCode и copy.
Можно пометить объект словом case.
Case класс можно копировать с помощью метода copy, при этом метод позволяет изменять его значения полей, если указать в параметре valName = newValue
.
Если метод unapply возвращает пару значений, то можно использовать инфиксную форму записи при совпадении с образцом.
Запечатанный класс
Если пометить суперкласс ключевым словом 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, если найдено совпадение в сопоставлениях с образцом.
Можно объединять частично определенные функции с помощью методов OrElse
и andThen
.
Аннотации
Аннотации
Аннотации
- это теги, добавляемые в исходный код программыс целью обработки дополнительными инструментами. Обычно они обрабатываются компилятором или его расширениями. В Scala можно аннотировать классы, методы, поля, локальные переменные и параметры.
Обычно аннотации указываются перед аннотируемым элементом, при этом можно указывать несколько аннотаций.
При аннотировании главного конструктора, аннотации указываются перед ним, с указанием пустых строк, если она не имеет аргументов.
Выражение можно аннотировать, добавляя двоеточие и аннотацию.
Параметры типов.
Аннотация может принимать именнованные аргументы. Если имя value
, то его можно опустить.
Для реализации аннотаций, её следует отнаследовать от annotation.Annotation
Аннотации для оптимизации
Если рекурсивный метод заканчивается рекурсивным методом, то он может быть оптимизирован компилятором в цикл. Это свойство называется хвостовой рекурсией
. Если перед рекурсивным методом поместить аннотацию @tailrec
, то компилятор сообщит об ошибки, если не получится выполнить оптимизацию.
Аннотация @switch
оптимизирует инструкцию сопоставления с образцом в таблицу переходов
.
Аннотация @elidable
помечает методы, которые следует удалить из окончательной версии. Подобный код следует компилировать определенныс образом.
Аннотация @specialized(TypeName)
автоматически генерирует методы, для каждых из перечисленных типов.
Аннотация предупреждений и ошибок
Существует ряд аннотаций для работы с предупреждениями и ошибками:
@depricated
- помечает метод, как устаревший.@depricatedName(value)
- позволяет использовать устаревшее имя параметра.@unchecked
- подавляет предупреждения о неполном match.@implicitNotFound(message)
- сообщает, если не может найти неявное преобразование.
Параметризированные типы
Обобщенные классы и методы
В Scala, как в C++ или Java класс или трейт может иметь параметризированные типы. Они обозначаются в квадратных скобках после имени класса.
При этом Scala умеет автоматически определять тип в момент создания объекта.
Методы тоже могут быть параметризированными.
Можно указать границы изменения типов
.
T <: M
- T должен быть подтипом M (верхняя граница).
T >: M
- T должен быть супертипом для M (нижняя граница).
T <% M
- T может быть неявно преобразован в M (см. неявные преобразования) (граница представления, view bound). То есть есть тип T, но при этом он может использовать методы типа M. Устарело.
T : M
- существуетнеявное значение
типа M[T] (см. неявные преобразования) (граница контекста, context bound).
Для одного типа может использоваться несколько границ. Можно определить по-одной верхней и нижней границе, но можно, чтобы тип реализовывал несколько трейтов. Можно использовать несколько границ представления и контекста.
Еще один из механизмов ограничения типов - это использовать неявный параметр подтверждения. Так можно ограничить применение конкретных методов в типизированном классе. То есть будет позволено создавать класс, параметризированный типом T, но вызвать определенный метод будет можно только при определенных условиях, для этого необходимо передавать неявный параметр подтверждения в конкретный метод:
T =:= U
- тип T равен U.T <:< U
- тип T является подтипом U.
Объекты не могут иметь обобщенные типы. При наследовании от типизированного класса или трейта, его тип должен быть указан явно.
Вариантность
Вариантность — перенос наследования исходных типов на производные от них типы. Под производными типами понимаются контейнеры. Существует три вида вариантости:
Инвариантность
— ситуация, когда наследование исходных типов не переносится на производные. Все, что рассматривалось до этого.Ковариантность
— перенос наследования исходных типов на производные от них типы в прямом порядке. Обозначается знаком+
в исходном типе. Здесь Group[Person] является родителем Group[Worker]. Отношение типа “если я могу что-то сделать из базового класса, то я могу сделать это и с подклассом”.
Контравариантность
— перенос наследования исходных типов на производные от них типы в обратном порядке. Обозначается знаком-
в исходном типе. Может быть использовано при контрактном программировании. Здесь Group[Worker] является родителем Group[Person]. Отношение типа “если я могу что-то сделать в подклассе из базового класса, то я могу сделать это и с базовым классом”.
Для методов объекта важно правило - аргументы должны быть контрвариантными, а возвращаемое значение ковариантное.
В случае, если передать ковариантное значение необходимо передать в метод, следует использовать нижнию границу при определении метода.
Если тип объявлен, как инвариантный, остается возможность изменить его можно там, где он используется, с помощью подстановочного символа _
.
Можно использовать как ковариантные, как и ковариантные объявления, это является синтаксическим сахаром для экзистенциальных типов.
Неявные параметры и преобразования
Неявные преобразования
Функция неявного преобразования
- это функция, помеченная ключевым словом implicit
, которая принимает единственный параметр. Такая функция автоматически преобразовывает аргумент из одного типа в другой.
При обнаружении неявного преобразования объект исходного типа может использовать методы целевого типа. Компилятор Scala ищет неявные преобразования в текущей области видимости, а также в объекте-компаньоне исходного и целевого типов. Неявные преобразования применяются в следующих ситуациях:
- Если тип выражения отличается от ожидаемого.
- Обращение к несуществующему полю или методу объекта.
- При вызове метода с аргументами, не соответствующими ожиданию.
Неявные параметры
Функции и методы могут иметь параметры, помеченные ключевым словом implicit
. В таком случает компилятор будет подыскивать значения по-умолчанию требуемого типа и помеченное ключевым словом implicit. Поиск неявных значений будет произведен в:
- Среди объявлений def и val в текущей области видимости.
- В объекте-компаньоне типа, связанного с требуемым, то есть требуемого типе, а также, если тип параметризирован, то в объектах компаньонах параметров типа.
При этом остается возможность передавать параметр явно.
Неявные параметры могут использоваться в сочетании с неявными преобразованиями. Если функция типизирована, то может потребоваться потверждение, что переданный тип обладает определенными методами. Помимо неявного параметра подтверждения, можно использовать неявный параметр - функцию неявного преобразования.
Вытащить текущее неявное значение можно с помощью метода implicitly[TypeOfImplicit]
.
Дополнительно
Вместо использования функций неявного преобразования для использования методов целевого типа можно использовать неявные классы. Неявный класс использует только один явный аргумент конструктора, который расширяется, а все методы класса становятся методами расширяемого типа.
Пример выше будет преобразован в:
Дополнительные типы
Типы-одиночки
При постоении fluent-методов this будет тип класса, в котором объявлен метод, а поэтому не получится использовать методы конктретного типа, если предыдущий fluent-метод вернет тип родителя. Для этого можно указать тип как T.type
.
Если потребуется передать объект-одиночку в качестве параметра, то в методе следует объявить как SingletonName.type.
Проекция типов
Об этом уже говорилось. При использовании вложенных классов, какждый вложенный класс будет принадлежать к конкретному внешнему, чтобы обойти это необходимо использовать конструкцию Outer#Inner
.
Цепочки
Выражение типа scala.collection.mutable.ArrayBuffer называют цепочкой
. Все компоненты цепочки, кроме последнего обязаны быть стабильными, к ним относят:
- Пакет.
- Объект.
- Значения val.
- this и super.
Псевдонимы типов
С помощью ключевого слова type
можно создавать псевдонимы сложных имен типов. Все объявления псевдонимов типа должны вкладываться в класс или объект.
В абстрактных классах и трейтах имеется возможность задать абстрактный псевдоним, с конкретной реализацией в конкретных классах. Абстрактный тип поддерживает границы типов.
Структурный и составной типы
Структурный тип
- это описание абстрактных полей и методов, которыми должен обладать соответствующий тип.
Чтобы принадлежать составному типу, значение должно принадлежать каждому из типов. Составной тип определяетяс как T with T2 with T3
.
Технически структурный и составной типы являются сокращенной формой одного вида, для структурного AnyRef { def methodName: Type }
, а для составного Type with Type2 {}
.
Инфиксный тип
Если тип параметризирован двумя параметрами типа, то можно записывать его в инфиксной форме T1 T T2
для типа T[T1, T2].
Экзистенциальный тип
Экзистенциальные типы данных названы так из-за квантора существования ∃. То есть позволяет ограничить подтип конструкцией forSome { statement }
. В выражении могут быть объявления val и type.
Выражения val как правило можно заместить проекцией типов, но может понадобиться, если требуется принимать типы одного подтипа.
Собственные типы
Имеется возможность задать псевдоним и собственный тип в начале класса, это позволит внутри класса использовать новое имя или гарантировать, что класс будет наследовать определенный тип. Для этого в начале класса или трейта пишется инструкция alias: Type =>
. Если псевдоним не нужен, то вместо alias пишется this, а если не нужен тип, то Type не указывается.
В примере выше внутри this доступна по π, также гарантируется, что трейт будет подмешан в наследника Exception.
XML
Узлы и аттрибуты
В Scala имеется встроенная поддердка xml. Можно определять литералы xml, используя разметку xml.
Scala предоставляет целую иерархию для работы с xml. Все инструменты для работы хранятся в пакете scala.xml
. Верхним классом иерархии является NodeSeq
, который является наследником Seq[Node] и обладает всеми свойствами Seq.
Прямым наследником NodeSeq является Node. Объект типа Node имеет методы:
child: Seq[Node]
- возвращает набор дочерних элементов.
label
- возвращает имя элемента.
attributes
- возвращает объект типаMetaData
, который похож на ассоциативный массив.
text
- строку без элемнтов xml.
Для получения пространства имен используется метод scope.
Встроенные выражения
В литералы xml можно встраивать блоки программного кода, заключив его в фигурные скобки. Чтобы экранировать блок требуется указать две фигурные скобки.
Выражения можно использовать и для встраивания в аттрибутах, при этом если их поместить в двойные кавычки, то выражение вычеслено не будет.
XPath подобные выражения
NodeSeq имеет два метода, которые применяются для поиска элементов.
\(s: String): NodeSeq
- поиск дочернего элемента.\\(s: String): NodeSeq
- поиск на всех уровнях вложенности.
Можно производить поиск элементов, используя имя тега, поиск аттрибутов, начиная значение с @. Можно использовать _ для указания любого элемента.
Сопоставление с образцом
При сопоставлении с образцом используются следующие правила.
- Совпадение с любым количеством аттрибутов.
- Совпадение с единственным дочерним с помощью _.
- Совпадение с любыми дочерними с помощью _*.
- Совпадение с единственным дочерним с присвоением в переменную.
- Совпадение с любым количеством дочерних элементов с присвоением в переменную.
-
Совпадение с текстовым узлом с присвоением в переменную.
Можно использовать только один элемент, аттрибуты использовать нельзя, для совпадения с аттрибутами следует использовать ограничитель.
Модификация
Для модификации элемнтов следует пользоваться методом copy класса Elem
, который имеет аргументы label, attributes и child.
Добавить или изменить аттрибут можно с помощью метода %(attr: Attribute)
. Создать аттрибут можно используя конструкцию Attribute(namespace: String, name: String, value: String, MetaData).
Трансформация XML
Для трансформации используется класс RuleTransformer
, конструктор которого принимает один или несколько объектов типа RuleRewrite
и применяет их. Для трансформации следует переопределить метод transform, который может принимать Node или Seq[Node].
Загрузка и сохранение
Для загрузки можно воспользоваться объектом XML.
loadFile(path: String)
- загрузка xml из файла.load(url: URL): Elem
- загрузка из urlload(source: InputSource): Elem
- загрузка различными объектами из java.io объектов
Монады
Монады и функторы
Моноид — это термин из абстрактной алгебры. Моноид определяется следующими вещами:
- Множество M.
- Бинарная операция ⊕ на этом множестве, от которой требуется ассоциативность.
- Нейтральный элемент ε этой операции, входящий в множество (т.е. такой, что (∀a ∈ M) ε⊕a = a⊕ε = a).
Монада
— это моноид в категории эндофункторов, параметрический тип данных, контейнер, который обязательно реализует две операции: создание монады (в литературе функция unit) — и функцию flatMap() (в литературе иногда имеет название bind) и подчиняется некоторым правилам. Функция unit отвечает за создание монады и для каждой монады она отличается. Функция flatMap принимает на вход функцию, которая принимает на данные что размещены в монаде и возвращает новую монаду, причем возможно монаду другого типа (U вместо T).
Каждая монада должна подчинятся 3 законам, и они должны гарантировать, что монадическая композиция будет работать предсказуемым образом.
Left unit law
- если применить функцию flatMap для типа с позитивным значением и передать туда некоторую функцию то результат будет такой же, как простое применение этой функции к переменной.unit(x) flatMap f ≡ f(x)
Right unit law
- если передадим в flatMap функцию которая просто создает монаду из данных (тех что находятся в монаде) — то на выходе мы получаем такую же монаду.monad flatMap unit ≡ monad
Associativity law
- если передадим в flatMap функцию которая создает монаду и применяет flatMap к ней внутри функции из данных (тех что находятся в монаде) — то это тоже, что и поочередно применять flatMap к верхнеуровневой монаде с внешней и внутренней функцией, соответственно.(monad flatMap f) flatMap g ≡ monad flatMap(x => f(x) flatMap g)
Существует два вида функторов: ковариантные и контрвариантные. Здесь рассмотрены только ковариантные. Функтором
является любой тип данных-контейнер A[T]
, для которого определен метод map[U](f: T => U): A[R]
и выполяются два закона.
Identity law
- map(identity) ничего не должно менять внутри функтора, где identity — это полиморфная тождественная функция (единичная функция) из Predef.scala.
Composition law
- произвольный функтор-контейнер, который последовательно отображают функцией ‘f’ и потом функцией ‘g’ эквивалентен тому, что мы строим новую функцию-композицию функций f и g (f andThen g) и отображаем один раз.
Для монады и функтора должен выполнятся нулевой закон (the zeroth law)
- m map f ≡ m flatMap { x => unit(f(x)) }
.
Некоторые монады могут иметь нулевое значение, то есть содержать отсутствие значение, такие монады называются монадическими нулями (monadic zeros). Некоторые монады Scala могут иметь два конкретных типа контейнера, один - контейнер позитивного значения, а второй - негативного. Такие монады называют Result
- интерфейс, определяющий тип монады и два конкретных класса-наследника трейта. Для монадических нулей должны выполнятся два закона:
Left unit law
- результатом применения функции flatMap будет такой же монадический нуль.mzero flatMap f ≡ mzero
.
-
Plus
- результатом сложения позитивной монады и монадического нуля будет позитивная монада.mzero plus m ≡ m
.
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.
Either
Either[A, B] - монада типа Result из пакета scala.util, имеющая два представления.
- Left[A] - контейнер положительного значения.
- Right[B] - контейнер негативного значения.
Методы проверки это isRight и isLeft. Для того, чтобы работать с контейнером необходимо получить проекцию контейнера, это делается методами left и right, которые возвращают объекты монадического типа.
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]]
- возвращает результат вычисления.
В то время как Future предоставляет методы только для чтения, тип Promise[T]
позволяет завершить вычисление Future записью значения. Значение может быть записано только один раз. Тип Promise обещает, что тут будет значение типа T, как только обещание (promise) было исполнено мы не можем его изменить. Значение типа Promise всегда связано лишь с одним значением типа Future.
success(v: T): Promise[T]
- запишет значение как успешное.failure(e: Throwable): Promise[T]
- запишет значение-исключение.future: Future[T]
- запишет Future с результатом из Promise.
Что происходит в примере выше:
- 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
.
Актор - это объект, обрабатывающий асинхронные сообщения. Отправить сообщение актору можно с помощью метода !(msg: Any)
. После отправки сообщения актору поток продолжает свою работу. В качестве сообщений зачастую используются case-классы. Помимо этого, можно передать ссылку на другой актор, чтобы отправить ему результаты обработки, например.
Сообщения, посылаемые актору попадают в его почтовый ящик, а при вызове метода 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.
Метод react(PartialFunction)
связывает почтовый ящик актора и частично определенную функцию. При совпадении может быть вызвана и еще одна функция react. react нижнего уровня должен вызывать act, либо необходимо начать act с объявления loop(f: =>Unit)
или loopWhile(p: Boolean)(f: =>Unit)
. Метод eventloop(pf: PartialFunction)
является сочетанием loop и react.
Для завершения работы актора можно вызвать метод exit(cause: String)
.
Связывание акторов
Можно связать два актора, тогда каждый из них получит оповещение, если другой завершит свою работу. На основе этого можно построить систему, где главный актор следит за работой своих подчиненных. По-умолчанию актор завершает свою работу, если связный закончил свою работу по причине, отличной от normal. Избежать этого можно, указав trapExit = true
.