Scala появилась как язык для JVM, который объединяет объектно-ориентированный и функциональный подходы. Спустя годы она остаётся выбором для команд, которым нужны строгая типизация, выразительные абстракции и доступ ко всей Java-экосистеме. Scala 3 заметно упростила синтаксис: вместо громоздких конструкций появились enum, given/using и extension-методы. Эта статья — практический маршрут от установки до первого осмысленного сервиса. Примеры рассчитаны на Scala 3.3 LTS и JDK 21: этого набора достаточно для большинства задач.
Зачем Scala в 2026 году
Язык работает на JVM и бесшовно вызывает Java-библиотеки. Это значит, что JDBC, Apache Kafka, Akka-подобные рантаймы и корпоративные SDK доступны без обёрток. Функциональный стиль помогает описывать преобразования данных как композицию чистых функций, а система типов ловит часть ошибок на этапе компиляции. Плата за выразительность — время компиляции и порог входа; для небольших скриптов проще взять Python или Go.
- Подходит для потоковой обработки, аналитики и серверных сервисов.
- Хорошо сочетается с Spark и экосистемой больших данных.
- Требует дисциплины: легко написать нечитаемые цепочки абстракций.
Установка инструментов
Самый короткий путь — coursier. Он поставит JVM, sbt и Scala CLI в пользовательский каталог. После установки перезапустите терминал, чтобы обновился PATH.
curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs
chmod +x cs
./cs setup
cs version
Для экспериментов удобен scala-cli: он компилирует одиночные файлы, умеет запускать REPL и собирать исполняемые пакеты без проекта. Для командной работы используйте sbt или Mill — они дают воспроизводимую сборку и управление зависимостями.
scala-cli version
scala-cli repl
scala-cli run Main.scala
sbt new scala/scala3.g8
Первый проект и сборка
Минимальный build.sbt фиксирует версию Scala и тестовую библиотеку. Не полагайтесь на плавающие версии: обновление компилятора может изменить вывод типов и сломать неочевидные места.
ThisBuild / scalaVersion := "3.3.4"
ThisBuild / organization := "ru.example"
libraryDependencies += "org.scalameta" %% "munit" % "1.0.0" % Test
scalacOptions ++= Seq("-deprecation", "-feature", "-Wunused:all")
Неизменяемость и типы
val создаёт неизменяемую ссылку, var — изменяемую переменную, def — метод, lazy val вычисляется при первом обращении. По умолчанию выбирайте val и неизменяемые коллекции: так проще рассуждать о коде и тестировать его. Вывод типов не отменяет явных аннотаций на публичных границах.
case class User(id: Long, name: String, active: Boolean)
val users = List(User(1, "Анна", true), User(2, "Олег", false))
val activeNames = users.filter(_.active).map(_.name)
val byId = users.map(u => u.id -> u).toMap
lazy val report = activeNames.mkString(", ")
Сопоставление с образцом
Pattern matching — не просто switch. Он разбирает структуру значения, проверяет типы и извлекает поля. Компилятор предупреждает о неполном совпадении, если включён соответствующий флаг. Для sealed-иерархий это даёт исчерпывающую проверку веток.
enum Event:
case Created(id: Long)
case Updated(id: Long, fields: List[String])
case Deleted(id: Long, reason: Option[String])
def describe(e: Event): String = e match
case Event.Created(id) => s"создан $id"
case Event.Updated(id, fields) => s"обновлён $id: ${fields.mkString(",")}"
case Event.Deleted(id, Some(reason)) => s"удалён $id: $reason"
case Event.Deleted(id, None) => s"удалён $id"
Option, Either и for-выражения
Вместо null в Scala принято использовать Option, а для ошибок с причиной — Either. For-выражение разворачивает цепочку вычислений и останавливается на первом None или Left. Это читается лучше вложенных проверок и не требует исключений для ожидаемых ситуаций.
def parseAge(raw: String): Either[String, Int] =
raw.toIntOption.toRight(s"не число: $raw").flatMap { age =>
if age >= 0 && age < 150 then Right(age) else Left("возраст вне диапазона")
}
val result = for
a <- parseAge("34")
b <- parseAge("41")
yield a + b
println(result.getOrElse(-1))
Given, using и extension
Scala 3 заменила неявные параметры на given/using. Это тот же механизм передачи зависимостей, но с явным синтаксисом. Extension-методы добавляют поведение к существующим типам без наследования и без правки исходников.
trait Clock:
def nowMillis: Long
given systemClock: Clock with
def nowMillis: Long = System.currentTimeMillis()
def elapsed(start: Long)(using clock: Clock): Long =
clock.nowMillis - start
extension (s: String)
def truncate(n: Int): String = if s.length <= n then s else s.take(n) + "..."
println("готово".truncate(3))
Коллекции и тесты
Коллекции в Scala ленивы там, где это нужно: View откладывает вычисления, Iterator экономит память. Не смешивайте ленивые и строгие преобразования без причины. Тесты на munit пишутся компактно и запускаются через sbt.
val rows = (1 to 1000).view.filter(_ % 2 == 0).map(_ * 2).take(5).toList
class AgeSuite extends munit.FunSuite:
test("отрицательный возраст отклоняется") {
assertEquals(parseAge("-1"), Left("возраст вне диапазона"))
}
Интероп с Java и JVM
Scala не изолирована от Java: коллекции, даты, JDBC и любые библиотеки подключаются напрямую. Для преобразования java.util.List в scala.List используйте CollectionConverters. Помните, что Java-коллекции изменяемы, поэтому не передавайте их внутрь чистых функций без копирования. На границе с Java полезно сразу приводить данные к неизменяемым типам Scala.
import java.time.Instant
import scala.jdk.CollectionConverters.*
val javaList = java.util.List.of("a", "b")
val scalaList = javaList.asScala.toList
val started = Instant.now()
println(s"старт: $started")
JVM-диагностика и сборка
Долгая компиляция — обычная жалоба. Проверьте, хватает ли памяти sbt, и не запускайте тяжёлые плагины без необходимости. Для сервиса соберите fat-jar и запускайте его на той же версии JVM, что и в production.
sbt -mem 2048 compile
sbt test
scala-cli --power package . --assembly -o app.jar
jcmd $(pgrep -f app.jar) GC.heap_info
Чек-лист внедрения
- Версия Scala и JDK зафиксированы в build-файле и CI.
- Публичные API имеют явные типы, внутренние — по необходимости.
- Ожидаемые ошибки возвращаются через Either, а не через исключения.
- Включены предупреждения компилятора и они не подавляются массово.
- Есть тесты на разбор входных данных и граничные случаи.
- CI использует ту же версию JDK и те же флаги компилятора, что и локальная сборка.
Итог
Scala стоит выбирать не ради модности, а когда нужны и JVM-совместимость, и сильная функциональная модель. Начните с scala-cli, затем перенесите код в sbt-проект, добавьте тесты и зафиксируйте версии. Функциональный стиль приходит через практику: маленькие чистые функции, неизменяемые данные и явные типы на границах дают больше пользы, чем экзотические абстракции.