Справочник по Scala (Handbook)

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

Коротко для всех: Scala совмещает в себе мощь JVM, лаконичность синтаксиса и строгую типизацию. Здесь мы разбираем Scala 3 (совместимо с 2.x), делая акцент на читаемости, безопасности и реальной практике.

1. Переменные, иммутабельность и типы: `val` против `var`

В Scala фундаментальное правило гласит: **состояние должно быть неизменяемым (immutable) по умолчанию**. Это избавляет от 90% багов при параллельной работе кода.

Вывод типов (Type Inference)

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

// Компилятор сам понимает, что это String и Int
val siteName = "FeXcharged"
val version = 3

// Но тип можно указать и явно:
val limit: Double = 100.50

// Попытка переопределить val вызовет ошибку еще до запуска:
// siteName = "Other" -- Ошибка компиляции!

2. Функции и выражения — в Scala всё возвращает значение

В Scala почти любой блок кода — это **выражение (expression)**, то есть он всегда вычисляет и возвращает итоговое значение. Даже обычный if/else работает как тернарный оператор.

Вам не обязательно писать ключевое слово return — функция автоматически возвращает результат последней строчки:

// Определение функции: параметры, тип ответа и тело
def getMax(a: Int, b: Int): Int = 
    if (a > b) a else b

// Присваиваем результат работы условия сразу в переменную
val status = if (version >= 3) "Modern" else "Legacy"
println(status) // "Modern"

3. Case-классы и Сопоставление с образцом (Pattern Matching)

Case-классы — это супер-заряженные структуры для хранения данных. В них автоматически встроен сгенерированный конструктор, готовые методы equals, toString и быстрый доступ к полям.

В связке с Pattern Matching (продвинутый switch на стереоидах) они позволяют читать структуру объектов за пару строк кода:

// Создаем модель данных за 1 строчку
case class User(name: String, role: String)

val currentUser = User("Мадалбеков", "admin")

// Pattern Matching по ролям и содержимому
val accessMessage = currentUser match {
    case User(name, "admin") => s"Полный доступ предоставлен, $name"
    case User(name, "guest") => s"Ограниченный просмотр, $name"
    case _                   => "Неизвестный тип пользователя"
}

println(accessMessage) // "Полный доступ предоставлен, Мадалбеков"

4. ООП в Scala: Трейты (Traits) и Объекты-компаньоны

В Scala гибкая система классов. Вместо стандартных интерфейсов используются **Трейты (Traits)**, а вместо статических методов — **Объекты (object)**.

Конструкция Что делает На что похоже
trait Задает поведение. Может содержать и абстрактные, и готовые методы. Интерфейс в Java/C# + миксины
object Единственный экземпляр на всё приложение (Singleton). Статический класс / Модуль
Компаньон object и class с одинаковым именем в одном файле. Класс со статическими методами
trait Logger {
    def log(msg: String): Unit = println(s"[LOG]: $msg")
}

// Объект-одиночка (Singleton), расширяющий трейт
object AppServer extends Logger {
    def start(): Unit = log("Сервер запущен на порту 8080")
}

AppServer.start()

5. Безопасная работа с данными: `Option`, `Try` вместо `null`

В правильном коде на Scala вы почти никогда не увидите NullPointerException. Если значение может отсутствовать, его заворачивают в контейнер Option.

Как работает Option:
  • Some(value) — если внутри контейнера реально есть значение.
  • None — если значения нет (вместо использования опасного null).
// Функция возвращает контейнер: данные либо есть, либо нет
def findUser(id: Int): Option[String] = {
    if (id == 1) Some("Мурас") else None
}

// Безопасный разбор через getOrElse (задаем дефолт на случай None)
val userName = findUser(2).getOrElse("Гость")
println(userName) // "Гость"

6. Коллекции и их суперсилы: `map`, `filter`, `flatMap`

Коллекции в Scala по умолчанию неизменяемые. Любая операция не меняет старый массив, а возвращает новый с нужными преобразованиями.

val numbers = List(1, 2, 3, 4, 5)

// 1. Фильтрация: оставить только четные
val evens = numbers.filter(n => n % 2 == 0)

// 2. Преобразование: умножить каждое число на 10 (короткая запись с _)
val multiplied = numbers.map(_ * 10)

// 3. Цепочка вызовов
val result = numbers
    .filter(_ > 2)
    .map(_ * 2)

println(result) // List(6, 8, 10)