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-проект, добавьте тесты и зафиксируйте версии. Функциональный стиль приходит через практику: маленькие чистые функции, неизменяемые данные и явные типы на границах дают больше пользы, чем экзотические абстракции.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте