Scala предоставляет множество различных конструкций для моделирования предметной области:
- Классы
- Объекты
- Сопутствующие объекты
- Трейты
- Абстрактные классы
- Перечисления только в Scala 3
- Case классы
- Case объекты
В этом разделе кратко представлена каждая из этих языковых конструкций.
Классы
Как и в других языках, класс в Scala — это шаблон для создания экземпляров объекта. Вот несколько примеров классов:
class Person(var name: String, var vocation: String)
class Book(var title: String, var author: String, var year: Int)
class Movie(var name: String, var director: String, var year: Int)
Эти примеры показывают, что в Scala есть очень легкий способ объявления классов.
Все параметры в примерах наших классов определены как var
поля, а значит, они изменяемы: их можно читать, а также изменять.
Если вы хотите, чтобы они были неизменяемыми — только для чтения — создайте их как val
поля или используйте case класс.
До Scala 3 для создания нового экземпляра класса использовалось ключевое слово new
:
val p = new Person("Robert Allen Zimmerman", "Harmonica Player")
// ---
Однако с универсальными apply методами в Scala 3 этого больше не требуется: только в Scala 3.
val p = Person("Robert Allen Zimmerman", "Harmonica Player")
Если у вас есть экземпляр класса, такой как p
, то вы можете получить доступ к полям экземпляра,
которые в этом примере являются параметрами конструктора:
p.name // "Robert Allen Zimmerman"
p.vocation // "Harmonica Player"
Как уже упоминалось, все эти параметры были созданы как var
поля, поэтому они изменяемые:
p.name = "Bob Dylan"
p.vocation = "Musician"
Поля и методы
Классы также могут содержать методы и дополнительные поля, не являющиеся частью конструкторов. Они определены в теле класса. Тело инициализируется как часть конструктора по умолчанию:
class Person(var firstName: String, var lastName: String) {
println("initialization begins")
val fullName = firstName + " " + lastName
// метод класса
def printFullName: Unit =
// обращение к полю `fullName`, определенному выше
println(fullName)
printFullName
println("initialization ends")
}
class Person(var firstName: String, var lastName: String):
println("initialization begins")
val fullName = firstName + " " + lastName
// метод класса
def printFullName: Unit =
// обращение к полю `fullName`, определенному выше
println(fullName)
printFullName
println("initialization ends")
Следующая сессия REPL показывает, как создать новый экземпляр Person
с этим классом:
scala> val john = new Person("John", "Doe")
initialization begins
John Doe
initialization ends
val john: Person = Person@55d8f6bb
scala> john.printFullName
John Doe
scala> val john = Person("John", "Doe")
initialization begins
John Doe
initialization ends
val john: Person = Person@55d8f6bb
scala> john.printFullName
John Doe
Классы также могут расширять трейты и абстрактные классы, которые мы рассмотрим в специальных разделах ниже.
Значения параметров по умолчанию
В качестве беглого взгляда на некоторые другие функции, параметры конструктора класса также могут иметь значения по умолчанию:
class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) {
override def toString = s"timeout: $timeout, linger: $linger"
}
class Socket(val timeout: Int = 5_000, val linger: Int = 5_000):
override def toString = s"timeout: $timeout, linger: $linger"
Отличительной особенностью этой функции является то, что она позволяет потребителям вашего кода создавать классы различными способами, как если бы у класса были альтернативные конструкторы:
val s = new Socket() // timeout: 5000, linger: 5000
val s = new Socket(2_500) // timeout: 2500, linger: 5000
val s = new Socket(10_000, 10_000) // timeout: 10000, linger: 10000
val s = new Socket(timeout = 10_000) // timeout: 10000, linger: 5000
val s = new Socket(linger = 10_000) // timeout: 5000, linger: 10000
val s = Socket() // timeout: 5000, linger: 5000
val s = Socket(2_500) // timeout: 2500, linger: 5000
val s = Socket(10_000, 10_000) // timeout: 10000, linger: 10000
val s = Socket(timeout = 10_000) // timeout: 10000, linger: 5000
val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000
При создании нового экземпляра класса вы также можете использовать именованные параметры. Это особенно полезно, когда несколько параметров имеют одинаковый тип, как показано в этом сравнении:
// пример 1
val s = new Socket(10_000, 10_000)
// пример 2
val s = new Socket(
timeout = 10_000,
linger = 10_000
)
// пример 1
val s = Socket(10_000, 10_000)
// пример 2
val s = Socket(
timeout = 10_000,
linger = 10_000
)
Вспомогательные конструкторы
Вы можете определить класс с несколькими конструкторами,
чтобы клиенты вашего класса могли создавать его различными способами.
Например, предположим, что вам нужно написать код для моделирования студентов в системе приема в колледж.
При анализе требований вы увидели, что необходимо создавать экземпляр Student
тремя способами:
- С именем и государственным удостоверением личности, когда они впервые начинают процесс приема
- С именем, государственным удостоверением личности и дополнительной датой подачи заявки, когда они подают заявку
- С именем, государственным удостоверением личности и студенческим билетом после того, как они будут приняты
Один из способов справиться с этой ситуацией в стиле ООП - с помощью нижеследующего кода:
import java.time._
// [1] основной конструктор
class Student(
var name: String,
var govtId: String
) {
private var _applicationDate: Option[LocalDate] = None
private var _studentId: Int = 0
// [2] конструктор для студента, подавшего заявку
def this(
name: String,
govtId: String,
applicationDate: LocalDate
) = {
this(name, govtId)
_applicationDate = Some(applicationDate)
}
// [3] конструктор, когда учащийся принят и теперь имеет студенческий билет
def this(
name: String,
govtId: String,
studentId: Int
) = {
this(name, govtId)
_studentId = studentId
}
}
import java.time.*
// [1] основной конструктор
class Student(
var name: String,
var govtId: String
):
private var _applicationDate: Option[LocalDate] = None
private var _studentId: Int = 0
// [2] конструктор для студента, подавшего заявку
def this(
name: String,
govtId: String,
applicationDate: LocalDate
) =
this(name, govtId)
_applicationDate = Some(applicationDate)
// [3] конструктор, когда учащийся принят и теперь имеет студенческий билет
def this(
name: String,
govtId: String,
studentId: Int
) =
this(name, govtId)
_studentId = studentId
Класс содержит три конструктора, обозначенных комментариями в коде:
- Первичный конструктор, заданный
name
иgovtId
в определении класса - Вспомогательный конструктор с параметрами
name
,govtId
иapplicationDate
- Другой вспомогательный конструктор с параметрами
name
,govtId
иstudentId
Эти конструкторы можно вызывать следующим образом:
val s1 = new Student("Mary", "123")
val s2 = new Student("Mary", "123", LocalDate.now)
val s3 = new Student("Mary", "123", 456)
val s1 = Student("Mary", "123")
val s2 = Student("Mary", "123", LocalDate.now)
val s3 = Student("Mary", "123", 456)
Хотя этот метод можно использовать, имейте в виду, что параметры конструктора также могут иметь значения по умолчанию,
из-за чего создается впечатление, что класс содержит несколько конструкторов.
Это показано в предыдущем примере Socket
.
Объекты
Объект — это класс, который имеет ровно один экземпляр.
Инициализируется он лениво, тогда, когда на его элементы ссылаются, подобно lazy val
.
Объекты в Scala позволяют группировать методы и поля в одном пространстве имен, аналогично тому,
как вы используете static
члены в классе в Java, Javascript (ES6) или @staticmethod
в Python.
Объявление object
аналогично объявлению class
.
Вот пример объекта “строковые утилиты”, который содержит набор методов для работы со строками:
object StringUtils {
def truncate(s: String, length: Int): String = s.take(length)
def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty
}
object StringUtils:
def truncate(s: String, length: Int): String = s.take(length)
def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty
Мы можем использовать объект следующим образом:
StringUtils.truncate("Chuck Bartowski", 5) // "Chuck"
Импорт в Scala очень гибкий и позволяет импортировать все члены объекта:
import StringUtils._
truncate("Chuck Bartowski", 5) // "Chuck"
containsWhitespace("Sarah Walker") // true
isNullOrEmpty("John Casey") // false
import StringUtils.*
truncate("Chuck Bartowski", 5) // "Chuck"
containsWhitespace("Sarah Walker") // true
isNullOrEmpty("John Casey") // false
или только некоторые:
import StringUtils.{truncate, containsWhitespace}
truncate("Charles Carmichael", 7) // "Charles"
containsWhitespace("Captain Awesome") // true
isNullOrEmpty("Morgan Grimes") // Not found: isNullOrEmpty (error)
Объекты также могут содержать поля, доступ к которым также осуществляется как к статическим элементам:
object MathConstants {
val PI = 3.14159
val E = 2.71828
}
println(MathConstants.PI) // 3.14159
object MathConstants:
val PI = 3.14159
val E = 2.71828
println(MathConstants.PI) // 3.14159
Сопутствующие объекты
Объект object
, имеющий то же имя, что и класс, и объявленный в том же файле, что и класс,
называется “сопутствующим объектом”. Точно так же соответствующий класс называется сопутствующим классом объекта.
Сопутствующие класс или объект могут получить доступ к закрытым членам своего “соседа”.
Сопутствующие объекты используются для методов и значений, не относящихся к экземплярам сопутствующего класса.
Например, в следующем примере у класса Circle
есть элемент с именем area
, специфичный для каждого экземпляра,
а у его сопутствующего объекта есть метод с именем calculateArea
,
который (а) не специфичен для экземпляра и (б) доступен для каждого экземпляра:
import scala.math._
class Circle(val radius: Double) {
def area: Double = Circle.calculateArea(radius)
}
object Circle {
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
}
val circle1 = new Circle(5.0)
circle1.area
import scala.math.*
class Circle(val radius: Double):
def area: Double = Circle.calculateArea(radius)
object Circle:
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
val circle1 = Circle(5.0)
circle1.area
В этом примере метод area
, доступный для каждого экземпляра Circle
,
использует метод calculateArea
, определенный в сопутствующем объекте.
Кроме того, поскольку calculateArea
является приватным, к нему нельзя получить доступ с помощью другого кода,
но, как показано, его могут видеть экземпляры класса Circle
.
Другие виды использования сопутствующих объектов
Сопутствующие объекты могут использоваться для нескольких целей:
- их можно использовать для группировки “статических” методов в пространстве имен, как в примере выше
- эти методы могут быть
public
илиprivate
- если бы
calculateArea
былpublic
, к нему можно было бы получить доступ из любого места какCircle.calculateArea
- эти методы могут быть
- они могут содержать методы
apply
, которые — благодаря некоторому синтаксическому сахару — работают как фабричные методы для создания новых экземпляров - они могут содержать методы
unapply
, которые используются для деконструкции объектов, например, с помощью сопоставления с шаблоном
Вот краткий обзор того, как методы apply
можно использовать в качестве фабричных методов для создания новых объектов:
class Person {
var name = ""
var age = 0
override def toString = s"$name is $age years old"
}
object Person {
// фабричный метод с одним аргументом
def apply(name: String): Person = {
var p = new Person
p.name = name
p
}
// фабричный метод с двумя аргументами
def apply(name: String, age: Int): Person = {
var p = new Person
p.name = name
p.age = age
p
}
}
val joe = Person("Joe")
val fred = Person("Fred", 29)
//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old
Метод unapply
здесь не рассматривается, но описан в Спецификации языка.
class Person:
var name = ""
var age = 0
override def toString = s"$name is $age years old"
object Person:
// фабричный метод с одним аргументом
def apply(name: String): Person =
var p = new Person
p.name = name
p
// фабричный метод с двумя аргументами
def apply(name: String, age: Int): Person =
var p = new Person
p.name = name
p.age = age
p
end Person
val joe = Person("Joe")
val fred = Person("Fred", 29)
//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old
Метод unapply
здесь не рассматривается, но описан в справочной документации.
Трейты
Если провести аналогию с Java, то Scala trait
похож на интерфейс в Java 8+.
Trait-ы могут содержать:
- абстрактные методы и поля
- конкретные методы и поля
В базовом использовании trait
может использоваться как интерфейс, определяющий только абстрактные члены,
которые будут реализованы другими классами:
trait Employee {
def id: Int
def firstName: String
def lastName: String
}
trait Employee:
def id: Int
def firstName: String
def lastName: String
Однако трейты также могут содержать конкретные члены.
Например, следующий трейт определяет два абстрактных члена — numLegs
и walk()
—
а также имеет конкретную реализацию метода stop()
:
trait HasLegs {
def numLegs: Int
def walk(): Unit
def stop() = println("Stopped walking")
}
trait HasLegs:
def numLegs: Int
def walk(): Unit
def stop() = println("Stopped walking")
Вот еще один трейт с абстрактным членом и двумя конкретными реализациями:
trait HasTail {
def tailColor: String
def wagTail() = println("Tail is wagging")
def stopTail() = println("Tail is stopped")
}
trait HasTail:
def tailColor: String
def wagTail() = println("Tail is wagging")
def stopTail() = println("Tail is stopped")
Обратите внимание, что каждый трейт обрабатывает только очень специфичные атрибуты и поведение:
HasLegs
имеет дело только с “лапами”, а HasTail
имеет дело только с функциональностью, связанной с хвостом.
Трейты позволяют создавать такие небольшие модули.
Позже в вашем коде классы могут смешивать несколько трейтов для создания более крупных компонентов:
class IrishSetter(name: String) extends HasLegs with HasTail {
val numLegs = 4
val tailColor = "Red"
def walk() = println("I’m walking")
override def toString = s"$name is a Dog"
}
class IrishSetter(name: String) extends HasLegs, HasTail:
val numLegs = 4
val tailColor = "Red"
def walk() = println("I’m walking")
override def toString = s"$name is a Dog"
Обратите внимание, что класс IrishSetter
реализует абстрактные члены, определенные в HasLegs
и HasTail
.
Теперь вы можете создавать новые экземпляры IrishSetter
:
val d = new IrishSetter("Big Red") // "Big Red is a Dog"
val d = IrishSetter("Big Red") // "Big Red is a Dog"
Это всего лишь пример того, чего можно добиться с помощью trait-ов. Дополнительные сведения см. в остальных уроках по моделированию.
Абстрактные классы
Когда необходимо написать класс, но известно, что в нем будут абстрактные члены, можно создать либо trait
, либо абстрактный класс.
В большинстве случаев желательно использовать trait
, но исторически сложилось так, что было две ситуации,
когда предпочтительнее использование абстрактного класса:
- необходимо создать базовый класс, который принимает аргументы конструктора
- код будет вызван из Java-кода
Базовый класс, который принимает аргументы конструктора
До Scala 3, когда базовому классу нужно было принимать аргументы конструктора, он объявлялся как abstract class
:
abstract class Pet(name: String) {
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
}
class Dog(name: String, var age: Int) extends Pet(name) {
val greeting = "Woof"
}
val d = new Dog("Fido", 1)
abstract class Pet(name: String):
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
class Dog(name: String, var age: Int) extends Pet(name):
val greeting = "Woof"
val d = Dog("Fido", 1)
Параметры в trait только в Scala 3
Однако в Scala 3 трейты теперь могут иметь параметры, так что теперь вы можете использовать трейты в той же ситуации:
trait Pet(name: String):
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
class Dog(name: String, var age: Int) extends Pet(name):
val greeting = "Woof"
val d = Dog("Fido", 1)
Trait-ы более гибки в составлении, потому что можно смешивать (наследовать) несколько trait-ов, но только один класс. В большинстве случаев trait-ы следует предпочитать классам и абстрактным классам. Правило выбора состоит в том, чтобы использовать классы всякий раз, когда необходимо создавать экземпляры определенного типа, и trait-ы, когда желательно разложить и повторно использовать поведение.
Перечисления только в Scala 3
Перечисление (an enumeration) может быть использовано для определения типа, состоящего из конечного набора именованных значений (в разделе, посвященном моделированию ФП, будут показаны дополнительные возможности перечислений). Базовые перечисления используются для определения наборов констант, таких как месяцы в году, дни в неделе, направления, такие как север/юг/восток/запад, и многое другое.
В качестве примера, рассмотрим перечисления, определяющие наборы атрибутов, связанных с пиццами:
enum CrustSize:
case Small, Medium, Large
enum CrustType:
case Thin, Thick, Regular
enum Topping:
case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions
Для использования в коде в первую очередь перечисление нужно импортировать, а затем - использовать:
import CrustSize.*
val currentCrustSize = Small
Значения перечислений можно сравнивать (==
) и использовать в сопоставлении:
// if/then
if currentCrustSize == Large then
println("You get a prize!")
// match
currentCrustSize match
case Small => println("small")
case Medium => println("medium")
case Large => println("large")
Дополнительные функции перечисления
Перечисления также могут быть параметризованы:
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
И они также могут содержать элементы (например, поля и методы):
enum Planet(mass: Double, radius: Double):
private final val G = 6.67300E-11
def surfaceGravity = G * mass / (radius * radius)
def surfaceWeight(otherMass: Double) =
otherMass * surfaceGravity
case Mercury extends Planet(3.303e+23, 2.4397e6)
case Earth extends Planet(5.976e+24, 6.37814e6)
// далее идут остальные планеты ...
Совместимость с перечислениями Java
Если вы хотите использовать перечисления, определенные в Scala, как перечисления Java,
то можете сделать это, расширив класс java.lang.Enum
(импортированный по умолчанию) следующим образом:
enum Color extends Enum[Color] { case Red, Green, Blue }
Параметр типа берется из определения Java enum
и должен совпадать с типом перечисления.
Нет необходимости предоставлять аргументы конструктора (как определено в документации Java API) для java.lang.Enum
при его расширении — компилятор генерирует их автоматически.
После такого определения Color
вы можете использовать его так же, как перечисление Java:
scala> Color.Red.compareTo(Color.Green)
val res0: Int = -1
В разделе об алгебраических типах данных и справочной документации перечисления рассматриваются более подробно.
Case class-ы
Case class используются для моделирования неизменяемых структур данных. Возьмем следующий пример:
case class Person(name: String, relation: String)
Поскольку мы объявляем Person
как case class
, поля name
и relation
по умолчанию общедоступны и неизменяемы.
Мы можем создавать экземпляры case классов следующим образом:
val christina = Person("Christina", "niece")
Обратите внимание, что поля не могут быть изменены:
christina.name = "Fred" // ошибка: reassignment to val
Поскольку предполагается, что поля case класса неизменяемы, компилятор Scala может сгенерировать для вас множество полезных методов:
- Генерируется метод
unapply
, позволяющий выполнять сопоставление с образцом case класса (то естьcase Person(n, r) => ...
). - В классе генерируется метод
copy
, полезный для создания модифицированных копий экземпляра. - Генерируются методы
equals
иhashCode
, использующие структурное равенство, что позволяет использовать экземпляры case классов вMap
-ах. - Генерируется дефолтный метод
toString
, полезный для отладки.
Эти дополнительные функции показаны в следующем примере:
// Case class-ы можно использовать в качестве шаблонов
christina match {
case Person(n, r) => println("name is " + n)
}
// для вас генерируются методы `equals` и `hashCode`
val hannah = Person("Hannah", "niece")
christina == hannah // false
// метод `toString`
println(christina) // Person(Christina,niece)
// встроенный метод `copy`
case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
// в результате:
// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
// Case class-ы можно использовать в качестве шаблонов
christina match
case Person(n, r) => println("name is " + n)
// для вас генерируются методы `equals` и `hashCode`
val hannah = Person("Hannah", "niece")
christina == hannah // false
// метод `toString`
println(christina) // Person(Christina,niece)
// встроенный метод `copy`
case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
// в результате:
// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
Поддержка функционального программирования
Как уже упоминалось ранее, case class-ы поддерживают функциональное программирование (ФП):
- ФП избегает изменения структур данных.
Поэтому поля конструктора по умолчанию имеют значение
val
. Поскольку экземпляры case class не могут быть изменены, ими можно легко делиться, не опасаясь мутаций или условий гонки. - вместо изменения экземпляра можно использовать метод
copy
в качестве шаблона для создания нового (потенциально измененного) экземпляра. Этот процесс можно назвать “обновлением по мере копирования”. - наличие автоматически сгенерированного метода
unapply
позволяет использовать case class в сопоставлении шаблонов.
Case object-ы
Case object-ы относятся к объектам так же, как case class-ы относятся к классам:
они предоставляют ряд автоматически генерируемых методов, чтобы сделать их более мощными.
Case object-ы особенно полезны тогда, когда необходим одноэлементный объект,
который нуждается в небольшой дополнительной функциональности,
например, для использования с сопоставлением шаблонов в выражениях match
.
Case object-ы полезны, когда необходимо передавать неизменяемые сообщения. Например, представим проект музыкального проигрывателя, и создадим набор команд или сообщений:
sealed trait Message
case class PlaySong(name: String) extends Message
case class IncreaseVolume(amount: Int) extends Message
case class DecreaseVolume(amount: Int) extends Message
case object StopPlaying extends Message
Затем в других частях кода можно написать методы, которые используют сопоставление с образцом
для обработки входящего сообщения
(при условии, что методы playSong
, changeVolume
и stopPlayingSong
определены где-то еще):
def handleMessages(message: Message): Unit = message match {
case PlaySong(name) => playSong(name)
case IncreaseVolume(amount) => changeVolume(amount)
case DecreaseVolume(amount) => changeVolume(-amount)
case StopPlaying => stopPlayingSong()
}
def handleMessages(message: Message): Unit = message match
case PlaySong(name) => playSong(name)
case IncreaseVolume(amount) => changeVolume(amount)
case DecreaseVolume(amount) => changeVolume(-amount)
case StopPlaying => stopPlayingSong()
Contributors to this page:
Contents
- Введение
- Возможности Scala
- Почему Scala 3?
- Почувствуй Scala
- Пример 'Hello, World!'
- REPL
- Переменные и типы данных
- Структуры управления
- Моделирование данных
- Методы
- Функции первого класса
- Одноэлементные объекты
- Коллекции
- Контекстные абстракции
- Верхнеуровневые определения
- Обзор
- Первый взгляд на типы
- Интерполяция строк
- Структуры управления
- Моделирование предметной области
- Инструменты
- Моделирование ООП
- Моделирование ФП
- Методы
- Особенности методов
- Main методы в Scala 3
- Обзор
- Функции
- Анонимные функции
- Параметры функции
- Eta расширение
- Функции высшего порядка
- Собственный map
- Создание метода, возвращающего функцию
- Обзор
- Пакеты и импорт
- Коллекции в Scala
- Типы коллекций
- Методы в коллекциях
- Обзор
- Функциональное программирование
- Что такое функциональное программирование?
- Неизменяемые значения
- Чистые функции
- Функции — это значения
- Функциональная обработка ошибок
- Обзор
- Типы и система типов
- Определение типов
- Параметризованные типы
- Пересечение типов
- Объединение типов
- Алгебраические типы данных
- Вариантность
- Непрозрачные типы
- Структурные типы
- Зависимые типы функций
- Другие типы
- Контекстные абстракции
- Методы расширения
- Параметры контекста
- Контекстные границы
- Given импорты
- Классы типов
- Многостороннее равенство
- Неявное преобразование типов
- Обзор
- Параллелизм
- Scala утилиты
- Сборка и тестирование проектов Scala с помощью Sbt
- Рабочие листы
- Взаимодействие с Java