Skip to content

Scala Шпаргалка

Гибридный функциональный/ООП язык на JVM.

01

Основы

Переменные и типы

Предпочитайте val (неизменяемое) var (изменяемое) для более безопасного, предсказуемого кода. Scala выводит типы, но явные аннотации помогают читаемости для публичных API. Всё — объект, нет примитивов (Int, Boolean — классы).

scala
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List[Int] = List(1, 2, 3)
println(name.getClass)  // class java.lang.String

Интерполяция строк

s"..." включает интерполяцию ${expr}. f"..." добавляет printf-подобное форматирование (%s, %.2f). raw"..." отключает escape-последовательности. Эти префиксы делают построение строк типобезопасным и читаемым.

scala
val name = "Alice"
val age = 30
println(s"Name: ${name}, Age: ${age}")
println(s"Next year: ${age + 1}")
println(s"Upper: ${name.toUpperCase}")
println(f"${name}%s weighs ${65.5}%.1f kg")  // formatted
println(raw"No \n escape")  // raw string

Кортежи

Кортежи группируют 2-22 гетерогенных значения. Доступ через _1, _2 (1-индексация). Деструктуризация через val (a, b) = tuple. Для более 2 элементов предпочитайте case-классы для именованных полей и лучшей читаемости.

scala
val pair = ("Alice", 30)
println(pair._1)  // Alice
println(pair._2)  // 30
val (name, age) = pair  // destructure
println(s"${name}: ${age}")
// Scala 3: val p = ("a", 1, 2.0)
val triple = ("a", 1, 2.0)
println(triple._3)  // 2.0

Вывод типа и приписывание

Scala выводит типы для локальных переменных и возвращаемых типов. Используйте явные типы для публичных API, рекурсивных функций и неоднозначных случаев. Приписывание типа (expr: Type) принуждает тип — полезно для upcast или устранения неоднозначности.

scala
val x = 42           // Int inferred
val y: Long = 42      // explicit Long
val z = 42: Long      // type ascription
val list = List(1, 2, 3)  // List[Int]
val mixed: List[Any] = List(1, "a", true)
def double(x: Int) = x * 2  // return type inferred

Unit и Nothing

Unit подобен void (одно значение: ()). Nothing — нижний тип без экземпляров, используется для функций, которые никогда не возвращаются (throw, бесконечный цикл). Nothing — подтип всех типов, обеспечивая гибкий вывод типа.

scala
def printIt(x: Int): Unit = println(x)  // like void
val u: Unit = ()    // Unit has one value: ()
// Nothing is the bottom type—no instances
def error(msg: String): Nothing =
  throw new RuntimeException(msg)
// Nothing is a subtype of everything
val n: Nothing = error("boom")
02

Строки

Распространённые строковые методы

Строки Scala — Java-строки с дополнительными методами через неявные преобразования (StringOps). Большинство методов возвращают новые строки (неизменяемые). Используйте их вместо ручных циклов для ясности и корректности.

scala
val s = "Hello, World"
println(s.length)        // 12
println(s.toUpperCase)   // HELLO, WORLD
println(s.toLowerCase)   // hello, world
println(s.split(", "))   // Array(Hello, World)
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reverse)       // dlroW ,olleH
println(s.contains("World"))  // true

Многострочные строки (тройные кавычки)

Строки в тройных кавычках сохраняют все пробелы и переносы. Используйте stripMargin с | для чистого выравнивания кода — сохраняется только текст после |. Идеально для SQL, JSON или шаблонов, встроенных в код.

scala
val sql = """
  SELECT * FROM users
  WHERE age > 18
  ORDER BY name
"""
println(sql.trim)
// StripMargin for clean indentation
val text = """|Hello
              |World""".stripMargin
println(text)  // Hello
World

Построение строк

mkString объединяет коллекции с опциональным префиксом/суффиксом — идиоматично и эффективно. Используйте StringBuilder для построения больших строк в циклах. Избегайте повторной конкатенации + в циклах (создаёт много промежуточных объектов).

scala
val parts = List("apple", "banana", "cherry")
println(parts.mkString(", "))     // apple, banana, cherry
println(parts.mkString("[", ", ", "]"))  // [apple, banana, cherry]
val sb = new StringBuilder
for (p <- parts) sb.append(p).append(" ")
println(sb.toString.trim)

Строка в число

toInt/toDouble вызывают исключение при некорректном вводе. Используйте toIntOption (Scala 2.13+) для безопасного разбора, возвращающего Option. Для массового разбора используйте Try или Either для функциональной обработки ошибок без исключений.

scala
val n = "42".toInt        // 42
val d = "3.14".toDouble    // 3.14
val b = "true".toBoolean   // true
val safe = "abc".toIntOption  // Some(42) or None
// Handling errors
val result = try "x".toInt catch { case _ => 0 }
println(result)  // 0

Регулярные выражения

.r преобразует строку в Regex. findFirstIn возвращает Option, findAllIn возвращает итератор. Используйте в сопоставлении шаблонов с case email(e) => для извлечения. Regex — это Pattern Java под капотом.

scala
import scala.util.matching.Regex
val email: Regex = "[\w.]+@[\w]+\.[a-z]+".r
val text = "Contact: [email protected]"
email.findFirstIn(text) match {
  case Some(e) => println(s"Found: ${e}")
  case None => println("No email")
}
val replaced = "[0-9]+".r.replaceAllIn("a1b2c3", "#")
println(replaced)  // a#b#c#
03

Структуры данных

List и Seq

List — неизменяемый односвязный список: O(1) head/prepend, O(n) случайный доступ. Используйте Vector для случайного доступа (O(1) эффективный). +: добавляет в начало, :+ добавляет в конец. Предпочитайте неизменяемые коллекции для потокобезопасности.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.head)      // 1
println(nums.tail)      // List(2,3,4,5)
println(nums.reverse)   // List(5,4,3,2,1)
println(nums.take(2))   // List(1, 2)
println(nums.drop(2))   // List(3, 4, 5)
println(nums.mkString)  // 12345
val combined = 0 +: nums :+ 6  // List(0,1,2,3,4,5,6)

Map

Map неизменяем — операции возвращают новые Map. Используйте get(key) для Option-доступа, getOrElse для значений по умолчанию. + добавляет/обновляет, - удаляет. Для изменяемых map используйте scala.collection.mutable.Map. Ключи должны быть Hashable.

scala
val ages = Map("Alice" -> 30, "Bob" -> 25)
println(ages("Alice"))            // 30 (throws if missing)
println(ages.getOrElse("Eve", 0)) // 0 (safe)
val updated = ages + ("Eve" -> 28)  // new Map
val removed = ages - "Bob"
ages.foreach { case (k, v) => println(s"${k}: ${v}") }
println(ages.keys)  // Set(Alice, Bob)

Set

Set неизменяем с O(1) contains. union (|), intersect (&), diff (~) для операций над множествами. + добавляет, - удаляет. Используйте для дедупликации и проверки принадлежности. Изменяемый вариант: scala.collection.mutable.Set.

scala
val a = Set(1, 2, 3)
val b = Set(3, 4, 5)
println(a union b)        // Set(1,2,3,4,5)
println(a intersect b)    // Set(3)
println(a diff b)         // Set(1,2)
println(a subsetOf(Set(1,2,3,4)))  // true
val added = a + 6  // Set(1,2,3,6)

Option (null-безопасность)

Option заменяет null — Some(value) или None. Используйте map/filter/flatMap для преобразований, getOrElse для значений по умолчанию. For-comprehensions работают с Option. Это устраняет NullPointerException, делая отсутствие явным в типе.

scala
def findUser(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
val name = findUser(1)
println(name.getOrElse("Unknown"))  // Alice
println(name.map(_.toUpperCase))    // Some(ALICE)
println(name.filter(_.startsWith("A")))  // Some(Alice)
val result = for {
  n <- findUser(1)
  if n.startsWith("A")
} yield n.toUpperCase  // Some(ALICE)

Array и Vector

Array — изменяемый Java-массив (самый быстрый, но без функциональных обновлений). Vector неизменяем с эффективным O(1) случайным доступом и обновлениями — предпочтителен для неизменяемых коллекций со случайным доступом. Используйте List для последовательного, Vector для индексного доступа.

scala
val arr = Array(1, 2, 3, 4)  // mutable, Java array
arr(0) = 10
println(arr(0))  // 10
val vec = Vector(1, 2, 3, 4)  // immutable, fast random access
println(vec(2))  // 3
val updated = vec.updated(0, 10)  // Vector(10,2,3,4)
// Vector: O(1) random access + immutable
// Array: mutable, Java interop, fastest
04

Поток управления

If / Else (выражение)

В Scala if/else — выражение, возвращающее значение. Это устраняет необходимость тернарного оператора. Обе ветви должны иметь совместимые типы. Используйте для лаконичного условного присваивания.

scala
val score = 85
val grade =
  if (score >= 90) "A"
  else if (score >= 80) "B"
  else if (score >= 70) "C"
  else "F"
println(grade)  // B
// if returns a value—no ternary operator needed

For-comprehension

for-comprehensions итерируют и могут фильтровать (if guards) и преобразовывать (yield). Без yield это цикл; с yield строит коллекцию. to включает конец, until исключает. Эквивалентно цепочкам flatMap/map/filter.

scala
for (i <- 1 to 5) println(i)  // 1 2 3 4 5
for (i <- 1 until 5) print(i)  // 1 2 3 4
// With yield (generates collection)
val doubled = for (n <- List(1,2,3)) yield n * 2
println(doubled)  // List(2, 4, 6)
// With filter (guard)
val evens = for (n <- 1 to 10 if n % 2 == 0) yield n
println(evens.toList)  // List(2,4,6,8,10)

Match (сопоставление шаблонов)

match — мощное сопоставление шаблонов Scala, как switch на стероидах. Поддерживает литералы, OR (|), guards (if), сопоставление типов и деструктуризацию. Должно быть исчерпывающим (компилятор предупреждает о пропущенных случаях). Это выражение, возвращающее значение.

scala
val n = 2
val label = n match {
  case 0 => "zero"
  case 1 | 2 | 3 => "small"
  case x if x < 10 => "medium"
  case _ => "large"
}
println(label)  // small
// Match on types
def describe(x: Any): String = x match {
  case i: Int => s"Int: ${i}"
  case s: String => s"String: ${s}"
  case _ => "unknown"
}

While и Do-While

while/do-while — императивные циклы, возвращающие Unit. Требуют изменяемого состояния (var). Предпочитайте for-comprehensions или рекурсию для идиоматичного функционального Scala. Используйте while только когда требует производительность или для побочных эффектов.

scala
var i = 0
while (i < 3) {
  println(i)
  i += 1
}
var j = 0
do {
  println(j)
  j += 1
} while (j < 3)
// Prefer recursion or for-comprehensions for immutability

Try / Catch / Finally

try/catch/finally как в Java, но использует сопоставление шаблонов в catch. Предпочитайте Try для функциональной обработки ошибок — оборачивает исключения как значения Success/Failure, обеспечивая цепочки map/flatMap без шаблонности try/catch.

scala
import scala.util.{Try, Success, Failure}
val result = try {
  "abc".toInt
} catch {
  case e: NumberFormatException => 0
} finally {
  println("cleanup")
}
println(result)  // 0
// Functional alternative
val r2 = Try("abc".toInt).getOrElse(0)
println(r2)  // 0
05

Функции

Определение метода

Методы используют def name(params): ReturnType = body. Поддерживаются параметры по умолчанию и именованные аргументы. Возврат Unit = только побочный эффект. Одно-выражительные тела опускают фигурные скобки. = обязателен (без него это процедура, возвращающая Unit).

scala
def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
  s"${greeting}, ${name}!"
def log(msg: String): Unit = println(msg)
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg

Лямбда (анонимная функция)

Лямбды: (params) => body. Используйте _ как сокращение для одного параметра (x => x * 2 становится _ * 2). Несколько _ ссылаются на разные параметры (_ + _). Лямбды — первого класса, передавайте их в map, filter, reduce и т.д.

scala
val square = (x: Int) => x * x
println(square(5))  // 25
val nums = List(1, 2, 3)
println(nums.map(_ * 2))      // List(2, 4, 6)
println(nums.filter(_ > 1))   // List(2, 3)
println(nums.reduce(_ + _))   // 6
// _ is shorthand for the parameter

Функции высшего порядка

Функции высшего порядка принимают или возвращают функции. Это обеспечивает мощные абстракции: map/filter/reduce, композиция, частичное применение. makeAdder возвращает замыкание, захватывающее n. Это сердце функционального программирования.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 5))  // 11
def makeAdder(n: Int): Int => Int = _ + n
val add5 = makeAdder(5)
println(add5(10))  // 15
// Functions returning functions = currying-like

Каррирование и частичное применение

Каррирование разделяет параметры на несколько списков: def f(a)(b). Частичное применение через _ создаёт специализированные функции. Несколько списков параметров улучшают вывод типа (компилятор может вывести типы f из списка). Распространено в API коллекций.

scala
def add(a: Int)(b: Int): Int = a + b  // curried
val add5 = add(5)_  // partially applied
println(add5(3))  // 8
// Multiple parameter lists
def foldLeft[A, B](list: List[A])(z: B)(f: (B, A) => B): B = ???
// Helps type inference
val sum = List(1,2,3).foldLeft(0)(_ + _)

By-Name и Lazy

By-name параметры (=> T) вычисляются лениво при каждом использовании — полезно для логирования (пропуск дорогого msg, если отключено) и пользовательских управляющих структур. lazy val откладывает инициализацию до первого доступа — для дорогих или опциональных значений.

scala
// By-name parameter: evaluated on each use
def debug(msg: => String): Unit =
  if (debugEnabled) println(msg)
// Lazy evaluation
lazy val expensive = computeHeavy()
println(expensive)  // computed now
def computeHeavy(): Int = { println("computing"); 42 }
06

Классы и ООП

Класс и конструктор

Параметры первичного конструктора в сигнатуре класса. Параметры val становятся неизменяемыми полями (публичный getter), var — изменяемыми. Тело класса ЕСТЬ конструктор. Вспомогательные конструкторы используют def this(...) и должны вызывать другой конструктор.

scala
class Person(val name: String, val age: Int) {
  // Constructor params with val/var become fields
  def greet: String = s"Hi, I'm ${name}"
  def isAdult: Boolean = age >= 18
}
val p = new Person("Alice", 30)
println(p.greet)   // Hi, I'm Alice
println(p.name)    // Alice (val field)
println(p.isAdult) // true

Case-класс

Case-классы — неизменяемые классы данных с автогенерацией equals, hashCode, toString, copy и companion-объектом с apply/unapply. Используйте для моделирования данных и сопоставления шаблонов. 'new' не нужен. Это основа ADT в Scala.

scala
case class Point(x: Int, y: Int)
val p1 = Point(3, 4)  // no 'new' needed
val p2 = Point(3, 4)
println(p1 == p2)  // true (value equality)
val moved = p1.copy(x = 5)  // Point(5, 4)
println(p1.x, p1.y)  // 3 4 (fields auto-visible)
// Auto: equals, hashCode, toString, copy, companion

Трейты (интерфейсы с реализацией)

Трейты подобны интерфейсам Java, но могут иметь реализацию. Класс может смешивать несколько трейтов (через extends/with). Трейты обеспечивают множественное наследование поведения. Используйте для разделяемых интерфейсов, mixins и стека-модификаций через линеаризацию.

scala
trait Greetable {
  def name: String  // abstract
  def greet: String = s"Hello, ${name}"  // concrete
}
trait Named {
  val name: String
}
class User(val name: String) extends Greetable
val u = new User("Alice")
println(u.greet)  // Hello, Alice
// Stackable traits via linearization

Object (singleton)

object объявляет singleton (один экземпляр). Companion-объекты (то же имя, что и класс) содержат static-подобные методы, фабрики (apply) и экстракторы (unapply). Используйте apply для фабричных методов, чтобы вызывающий опускал 'new'. Это идиоматичный Scala.

scala
object Config {
  val version = "1.0"
  def load(): Map[String, String] = Map("key" -> "value")
}
println(Config.version)  // 1.0
// Companion object (same name as class)
class Person(val name: String)
object Person {
  def apply(name: String): Person = new Person(name)
}
val p = Person("Alice")  // uses apply, no 'new'

Наследование и абстрактный класс

abstract class может иметь нереализованные члены. Используйте extends для наследования, override для переопределения. Предпочитайте трейты для mixins (множественное наследование). Используйте abstract class, когда нужны параметры конструктора или базовый тип. Одиночное наследование классов.

scala
abstract class Shape {
  def area: Double  // abstract
  def describe: String = s"Area: ${area}"
}
class Circle(r: Double) extends Shape {
  def area: Double = math.Pi * r * r
}
val c = new Circle(5)
println(c.describe)  // Area: 78.53...
// override required for concrete members
// abstract class vs trait: use abstract for base, trait for mixins
07

Коллекции и функциональное

Map / Filter / Fold

map преобразует, filter выбирает, reduce/foldLeft агрегирует. Это ядро функциональной трансформации данных. foldLeft принимает seed и ассоциативен; reduce требует непустую коллекцию. Используйте их вместо циклов для ясности и неизменяемости.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.map(_ * 2))           // List(2,4,6,8,10)
println(nums.filter(_ % 2 == 0))   // List(2,4)
println(nums.reduce(_ + _))        // 15
println(nums.foldLeft(0)(_ + _))   // 15
println(nums.sum)                  // 15
println(nums.mkString(", "))       // 1, 2, 3, 4, 5

FlatMap и For-Comprehension

flatMap отображает и выравнивает за один шаг — необходимо для вложенных коллекций и монадических операций. For-comprehensions — синтаксический сахар для цепочек flatMap/map/filter. Используйте для сложных вложенных преобразований — более читаемо.

scala
val nested = List(List(1, 2), List(3, 4))
println(nested.flatten)        // List(1,2,3,4)
println(nested.flatMap(_.map(_ * 2)))  // List(2,4,6,8)
// Equivalent for-comprehension:
val result = for {
  inner <- nested
  n <- inner
} yield n * 2
println(result)  // List(2,4,6,8)

Группировка и сортировка

groupBy разделяет по ключу в Map. sorted сортирует естественно, sortBy по ключевой функции, sortWith с компаратором. Возвращают новые коллекции (неизменяемые). Используйте для анализа данных, категоризации и упорядочивания.

scala
val words = List("apple", "bat", "cat", "ant")
val byFirst = words.groupBy(_.head)
// Map(a -> List(apple, ant), b -> List(bat), c -> List(cat))
println(byFirst)
val sorted = words.sorted  // List(ant, apple, bat, cat)
val byLen = words.sortBy(_.length)  // List(ant, bat, cat, apple)
val desc = words.sortWith(_ > _)  // descending
println(sorted, byLen)

Either и Try

Try оборачивает исключения как значения Success/Failure. Either представляет Left (ошибка) или Right (успех) — используйте для доменных ошибок, где нужно различать типы ошибок. Оба поддерживают map/flatMap для функционального распространения ошибок.

scala
import scala.util.{Try, Success, Failure}
def parse(s: String): Try[Int] = Try(s.toInt)
parse("42") match {
  case Success(n) => println(s"OK: ${n}")
  case Failure(e) => println(s"Err: ${e.getMessage}")
}
// Either for domain errors
def divide(a: Int, b: Int): Either[String, Int] =
  if (b == 0) Left("div by zero") else Right(a / b)
println(divide(10, 2))  // Right(5)
println(divide(10, 0))  // Left(div by zero)

Ленивые коллекции (View и Lazy)

.view делает коллекции ленивыми — операции откладываются до принуждения (toList, sum и т.д.). Избегает промежуточных коллекций для лучшей производительности на больших данных. LazyList (ранее Stream) позволяет бесконечные последовательности — элементы вычисляются по требованию.

scala
val nums = (1 to 1000000).view
val result = nums
  .filter(_ % 2 == 0)
  .map(_ * 2)
  .take(5)
  .toList  // forces evaluation
println(result)  // List(4, 8, 12, 16, 20)
// view = lazy, no intermediate collections
// LazyList (Stream) for infinite sequences
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
08

Сопоставление шаблонов

Сопоставление case-классов

Sealed-трейты + case-классы формируют алгебраические типы данных (ADT). Компилятор проверяет исчерпываемость — добавьте case, и он предупредит, где нужно обновить match. Сопоставление шаблонов деструктурирует case-классы напрямую. Это идиоматичное моделирование Scala.

scala
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
  case Circle(r) => math.Pi * r * r
  case Square(s) => s * s
  case Rect(w, h) => w * h
}
println(area(Circle(5)))  // 78.53...

Guards и условия

Guards (if condition) добавляют проверки времени выполнения к шаблонам. Они делают сопоставление более выразительным. Порядок важен — первое совпадение побеждает. Используйте guards для диапазонов, условий или сложной логики, которую простые шаблоны не могут выразить.

scala
val n = 15
val desc = n match {
  case x if x < 0 => "negative"
  case 0 => "zero"
  case x if x % 2 == 0 => "even"
  case _ => "odd"
}
println(desc)  // odd
// Guards add boolean conditions to patterns

Сопоставление коллекций

:: cons-шаблон деструктурирует списки на голову и хвост. _* совпадает с нулём или более элементов в массивах/списках. Эти шаблоны обеспечивают рекурсивную обработку списков и структурную декомпозицию. Мощно для разбора и обхода деревьев.

scala
val list = List(1, 2, 3, 4)
list match {
  case Nil => println("empty")
  case head :: Nil => println(s"one: ${head}")
  case head :: tail => println(s"head=${head}, rest=${tail}")
  case _ => println("other")
}
// head :: tail destructures a list
val arr = Array(1, 2, 3)
arr match {
  case Array(1, _*) => println("starts with 1")
  case _ => println("other")
}

Сопоставление Option и Either

Сопоставление шаблонов на Option/Either идиоматично — Some(x)/None, Right(x)/Left(e). For-comprehensions разворачиваются в flatMap с match. Это делает обработку ошибок естественной без явных if-else проверок.

scala
def find(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
find(1) match {
  case Some(name) => println(s"Found: ${name}")
  case None => println("Not found")
}
// In for-comprehensions
val result = for {
  name <- find(1)
  upper = name.toUpperCase
} yield upper
println(result)  // Some(ALICE)

Экстракторы (unapply)

Пользовательские экстракторы через unapply обеспечивают сопоставление шаблонов на любом типе. Метод unapply возвращает Option извлечённых значений. Это позволяет определять собственные шаблоны — мощно для DSL и разбора. Case-классы автогенерируют unapply.

scala
object Email {
  def unapply(s: String): Option[(String, String)] = {
    val parts = s.split("@")
    if (parts.length == 2) Some((parts(0), parts(1))) else None
  }
}
"[email protected]" match {
  case Email(user, domain) =>
    println(s"User: ${user}, Domain: ${domain}")
  case _ => println("Not an email")
}
09

Обобщённые типы и неявные

Обобщённые классы и методы

Обобщённые типы (параметры типа [A]) пишут типобезопасный, переиспользуемый код. Используйте [A] для одного типа, [A, B] для двух. Вывод типа обычно определяет тип. Обобщённые типы стираются во время выполнения (ограничение JVM), но проверяются во время компиляции.

scala
class Stack[A] {
  private var items: List[A] = Nil
  def push(x: A): Unit = { items = x :: items }
  def pop: Option[A] = items.headOption
}
val s = new Stack[Int]
s.push(1); s.push(2)
println(s.pop)  // Some(2)
def first[A](list: List[A]): Option[A] = list.headOption
println(first(List("a", "b")))  // Some(a)

Ограничения типов

<: верхняя граница (A — подтип), >: нижняя граница (A — супертип). Context bounds (A: Ordering) требуют неявное значение этого типа. View bounds (A <% B) устарели — используйте context bounds вместо. Эти ограничения сужают параметры типа.

scala
// Upper bound: A must be Animal or subclass
class Box[A <: Animal](val content: A)
// Lower bound: A must be Dog or superclass
class Kennel[A >: Dog](val occupant: A)
// Context bound: A must have an Ordering
def max[A: Ordering](a: A, b: A): A =
  if (implicitly[Ordering[A]].gt(a, b)) a else b
abstract class Animal { def name: String }
class Dog extends Animal { def name = "Rex" }

Неявные параметры

Неявные параметры внедряются компилятором из области видимости. Используйте для конфигурации, классов типов или зависимостей, которые не хотите передавать везде. Объявляйте через implicit val/def. Компилятор ищет в охватывающей области и companion-объектах.

scala
def greet(name: String)(implicit greeting: String): String =
  s"${greeting}, ${name}!"
implicit val defaultGreeting: String = "Hello"
println(greet("Alice"))  // Hello, Alice! (implicit injected)
println(greet("Bob")("Hi"))  // Hi, Bob! (explicit override)
// Compiler finds implicit in scope

Классы типов (неявные преобразования)

Классы типов (через implicits) добавляют поведение типам без их изменения — ad-hoc полиморфизм. Определите трейт, предоставьте неявные экземпляры и используйте неявные параметры. Так работают Ordering, Numeric и Show. Гибче наследования.

scala
trait Show[A] { def show(a: A): String }
object Show {
  implicit val intShow: Show[Int] = (a: Int) => a.toString
  implicit val strShow: Show[String] = identity
}
def printIt[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))
printIt(42)       // 42
printIt("hello")  // hello
// Type class: ad-hoc polymorphism

Методы расширения (Scala 2)

Implicit classes добавляют методы расширения к существующим типам. Определите implicit class с одним параметром, и его методы станут доступными на этом типе. Используйте для добавления утилитарных методов к Int, String и т.д. Scala 3 использует более чистый синтаксис 'extension'.

scala
implicit class IntOps(val n: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to n).foreach(_ => f)
  def squared: Int = n * n
}
5.times { print("hi") }  // hihihihihi
println(5.squared)  // 25
// Scala 3: extension (n: Int) def squared = n * n
10

Параллелизм и Future

Future и Async

Future представляет асинхронное вычисление. onComplete регистрирует обратный вызов. Никогда не блокируйте (Await.result) в продакшен-веб-серверах — занимает потоки. Используйте for-comprehensions для функциональной цепочки futures. Требуется ExecutionContext.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
  Thread.sleep(1000)
  42
}
f.onComplete {
  case scala.util.Success(v) => println(s"Got ${v}")
  case scala.util.Failure(e) => println(s"Err: ${e}")
}
// Don't block in production—use callbacks or for-comprehensions

Композиция Futures

For-comprehensions на Futures выполняют их последовательно (каждый ждёт предыдущий). Для параллельного выполнения запустите все Futures сначала, затем используйте Future.sequence для объединения. Future.traverse отображает и последовательно соединяет за один шаг. Это идиоматичный способ компоновать асинхронную работу.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f1 = Future { 10 }
val f2 = Future { 20 }
val sum = for {
  a <- f1
  b <- f2
} yield a + b  // Future(30)
// Parallel execution
val results = Future.sequence(List(
  Future { 1 }, Future { 2 }, Future { 3 }
))
results.map(_.sum)  // Future(6)

Параллельные коллекции

.par преобразует коллекцию в параллельную версию — операции используют несколько потоков автоматически. Хорошо для CPU-bound работы на больших коллекциях. Осторожно: не-ассоциативные операции (как вычитание) могут дать разные результаты. Не для I/O-bound работы.

scala
val nums = (1 to 1000000).toList
val sum = nums.par.sum  // parallel sum
println(sum)
val doubled = nums.par.map(_ * 2).toList
// .par converts to ParCollection
// Operations run on multiple threads
// Use for CPU-bound work on large collections

Promise (ручной Future)

Promise — записываемая сторона Future — завершаете его вручную через success/failure. Используйте при мостинге callback-based API к Futures или когда нужно завершить Future из нескольких мест. Future только для чтения; Promise одноразовой записи.

scala
import scala.concurrent.{Promise, Future, ExecutionContext}
import ExecutionContext.Implicits.global
val p = Promise[Int]()
val f = p.future
// Complete the promise from another thread
Future { Thread.sleep(100); p.success(42) }
f.foreach(println)  // 42 (when complete)
// p.failure(new Exception) for errors
// Promise = write side, Future = read side

Sync vs Async (Await)

Await.result блокирует текущий поток до завершения Future (с таймаутом). Используйте только в тестах или main-методах — блокировка в асинхронном коде нарушает цель. В продакшене используйте обратные вызовы (onComplete, map) или for-comprehensions для неблокирующего режима.

scala
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
val f = Future { Thread.sleep(500); 42 }
// Block and wait (use sparingly—mainly in tests)
val result = Await.result(f, 1.second)
println(result)  // 42
// Await.ready returns Try, Await.result returns value
// Avoid in production servers—use callbacks instead
11

Implicits: глубокое погружение

Неявные параметры

Неявные параметры передаются автоматически компилятором, когда неявное значение соответствующего типа в области видимости. Это уменьшает шаблонность для 'контекстных' параметров (ExecutionContext, логирование, конфигурация). Компилятор ищет: локальная область, companion-объекты, неявная область. Можно всегда передать явно для переопределения. Избыточное использование усложняет трассировку кода — используйте для подлинно контекстных зависимостей.

scala
import scala.concurrent.ExecutionContext

// Method with implicit parameter
def process[A](data: List[A])
    (implicit ec: ExecutionContext): Future[Unit] = {
  Future { data.foreach(println) }
}

// The compiler finds an implicit ExecutionContext in scope
implicit val ec: ExecutionContext = ExecutionContext.global
process(List(1, 2, 3))  // ec passed automatically

// Explicitly providing (overrides implicit)
process(List(1, 2, 3))(myCustomEC)

// Multiple implicit parameters
def log(msg: String)(implicit
    level: Level, logger: Logger): Unit = {
  logger.log(level, msg)
}

Неявные преобразования

Неявные преобразования автоматически конвертируют между типами при необходимости. implicit class (расширяющий AnyVal для нулевых накладных расходов) добавляет методы расширения к существующим типам — так Scala добавляет методы к Int, String и т.д. Будьте осторожны: неявные преобразования могут запутать код (что преобразуется?). Предпочитайте implicit classes для расширений сырых implicit defs. Включите через import scala.language.implicitConversions.

scala
import scala.language.implicitConversions

// Implicit conversion: one type to another
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // intToString(42) called implicitly

// Extension via implicit class (Scala 2.10+)
implicit class RichInt(val self: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
  def squared: Int = self * self
}

5.times { println("hi") }  // prints hi 5 times
3.squared  // 9

// Implicit conversion for type compatibility
implicit def javaToScalaList(jl: java.util.List[Int]): List[Int] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

val javaList: java.util.List[Int] = ???
val scalaList: List[Int] = javaList  // converted

Приоритет неявного разрешения

Компилятор разрешает implicits по приоритету: локальная область > companion-объекты > импортированные > унаследованные. Если два implicits одного типа одинаково в области, возникает ошибка 'ambiguous implicit'. Паттерн LowPriorityImplicits trait предоставляет значения по умолчанию, которые могут быть переопределены более конкретными implicits. Понимание порядка разрешения критично для дизайна библиотек — помещайте значения по умолчанию в низкоприоритетные трейты, чтобы пользователи могли переопределять.

scala
// Priority of implicit resolution (highest to lowest):
// 1. Local implicit (defined in current scope)
implicit val ec1: ExecutionContext = ec1

// 2. Implicit in companion object
object MyService {
  implicit val ec2: ExecutionContext = ec2  // lower priority
}

// 3. Implicit scope (imported)
import somePackage.Implicits._

// 4. Implicit parameter default (inherited trait)
trait DefaultEc {
  implicit val ec: ExecutionContext = ExecutionContext.global
}

// More specific type wins
implicit def ord1: Ordering[Int] = ???
implicit def ord2: Ordering[Int] = ???  // ambiguous error!

// LowPriorityImplicits trait pattern
object MyLib {
  implicit val high: Ordering[Int] = ???
}
object MyLib extends LowPriorityImplicits
trait LowPriorityImplicits {
  implicit val low: Ordering[Int] = ???  // fallback
}

Context Bounds и свидетельства

Context bounds [A: TypeClass] — синтаксический сахар для неявных параметров — утверждают, что существует неявный TypeClass[A]. Используйте implicitly[TypeClass[A]] (Scala 2) или summon[TypeClass[A]] (Scala 3) для извлечения. Context bounds делают ограничения класса типов читаемыми: def sort[A: Ordering]. Несколько bounds складываются: [A: Ordering: Numeric]. Это идиоматичный способ выражения требований класса типов.

scala
// Context bound: [A: Ordering] means there's an implicit Ordering[A]
def max[A: Ordering](a: A, b: A): A = {
  val ord = implicitly[Ordering[A]]  // retrieve the implicit
  if (ord.gt(a, b)) a else b
}

// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
  if (ord.gt(a, b)) a else b

// summon (Scala 3) instead of implicitly
def max3[A: Ordering](a: A, b: A): A = {
  val ord = summon[Ordering[A]]
  if (ord.gt(a, b)) a else b
}

// Type class evidence
def sort[A: Ordering](list: List[A]): List[A] =
  list.sorted  // uses the implicit Ordering

// Multiple context bounds
def process[A: Ordering: Numeric](x: A, y: A): A = ???

Неявная область и companion-объекты

Неявная область шире текущей — включает companion-объекты задействованных типов. Поэтому не нужно импортировать Ordering[Int]: он живёт в companion Int. Этот механизм делает классы типов эргономичными: определите экземпляр в companion типа, и он автоматически доступен. Package objects содержат разделяемые implicits для пакета. Этот дизайн обеспечивает использование классов типов 'без импорта'.

scala
// Implicit in companion object is found automatically
case class UserId(value: Long)
object UserId {
  implicit val ordering: Ordering[UserId] =
    Ordering.by(_.value)
}

// No import needed—companion object implicits are in scope
List(UserId(3), UserId(1), UserId(2)).sorted
// Works because Ordering[UserId] is in UserId's companion

// Implicit scope includes:
// 1. Companion object of the type (UserId)
// 2. Companion object of the type class (Ordering)
// 3. Companion objects of type parameters

// This is why Int has an Ordering:
// object Int { implicit val ord: Ordering[Int] = ... }

// Package object for shared implicits
package object myapp {
  implicit val ec: ExecutionContext = ExecutionContext.global
  type Id = Long
}
12

Классы типов

Определение класса типов

Класс типов — трейт, параметризованный типом, с экземплярами, предоставляющими поведение для конкретных типов. В отличие от наследования, можно добавлять экземпляры класса типов ретроактивно (для типов, которыми не владеете). Show[A] определяет, как отображать A. Экземпляры живут в companion-объекте (автоматическая неявная область). Это ad-hoc полиморфизм — разное поведение на тип без изменения типов. Классы типов — самая мощная абстракция Scala.

scala
// Type class: a trait parameterized by type
trait Show[A] {
  def show(a: A): String
}

// Instances for specific types
object Show {
  // Instance for Int
  implicit val showInt: Show[Int] = (a: Int) => a.toString

  // Instance for String
  implicit val showString: Show[String] = (s: String) => s""$s""

  // Instance for List (recursive)
  implicit def showList[A](implicit s: Show[A]): Show[List[A]] =
    (list: List[A]) => list.map(s.show).mkString("[", ", ", "]")
}

// Usage with implicit parameter
def print[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))

print(42)           // 42
print("hello")      // "hello"
print(List(1, 2, 3))  // [1, 2, 3]

Использование классов типов (синтаксический сахар)

Context bounds [A: Show] + summon извлекают экземпляры класса типов. Методы расширения (implicit class) добавляют методы вроде .show, использующие класс типов. Эта комбинация даёт чистый API: 42.show работает, если существует Show[Int]. Стандартная библиотека предоставляет множество классов типов: Numeric, Ordering, Eq, Monoid (Cats). Импорт синтаксиса (Numeric.Implicits._) добавляет операторы вроде + и sum, использующие класс типов.

scala
// Context bound syntax
def printAll[A: Show](items: List[A]): Unit =
  items.foreach(a => println(summon[Show[A]].show(a)))

// Extension methods via implicit class
implicit class ShowOps[A](val a: A) extends AnyVal {
  def show(implicit s: Show[A]): String = s.show(a)
}

42.show           // "42"
"hi".show         // ""hi""
List(1,2).show    // "[1, 2]"

// Combining: type class + extension methods
def format[A: Show](a: A): String = a.show

// Standard library type classes
def sum[A: Numeric](xs: List[A]): A =
  summon[Numeric[A]].plus(xs.head, xs.tail.foldLeft(
    summon[Numeric[A]].zero)(summon[Numeric[A]].plus))

// Or with syntax:
import Numeric.Implicits._
def sum2[A: Numeric](xs: List[A]): A = xs.sum

Распространённые классы типов (Cats/Scalaz)

Cats и Scalaz предоставляют стандартные классы типов. Monoid (empty + combine) обеспечивает общую агрегацию. Functor (map) и Monad (pure + flatMap) абстрагируют контейнеры (List, Option, Future, IO). Eq обеспечивает типобезопасное равенство (без случайных кросс-типовых сравнений). Они компонуются: Monad — Functor, Monoid — Semigroup. Классы типов позволяют писать обобщённый, переиспользуемый код, работающий с многими типами.

scala
// Monoid: combine values with empty
trait Monoid[A] {
  def empty: A
  def combine(a: A, b: A): A
}
object Monoid {
  implicit val intAdd: Monoid[Int] = new Monoid[Int] {
    def empty = 0
    def combine(a: Int, b: Int) = a + b
  }
  implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
    def empty = Nil
    def combine(a: List[A], b: List[A]) = a ++ b
  }
}

// Functor: map over structure
trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

// Monad: chain operations
trait Monad[F[_]] {
  def pure[A](a: A): F[A]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}

// Eq: type-safe equality
trait Eq[A] {
  def eqv(a: A, b: A): Boolean
}

// Semigroup: combine (no empty)
trait Semigroup[A] {
  def combine(a: A, b: A): A
}

Законы и тестирование классов типов

Законы класса типов — математические свойства, которым должны удовлетворять экземпляры. Monoid требует ассоциативности и идентичности. Functor требует сохранения идентичности и композиции. Библиотеки вроде Cats предоставляют определения законов; discipline + ScalaCheck авто-тестируют их. Законы — причина мощи классов типов: обобщённый код (как foldMap) работает корректно для любого закона-соблюдающего экземпляра. Всегда проверяйте, что ваши экземпляры удовлетворяют законам — ошибки в экземплярах ломают весь обобщённый код, их использующий.

scala
// Type class laws: properties that must hold
// Monoid laws:
// 1. Left identity:  combine(empty, a) == a
// 2. Right identity: combine(a, empty) == a
// 3. Associativity:  combine(a, combine(b, c)) == combine(combine(a, b), c)

// Functor laws:
// 1. Identity:    map(fa)(identity) == fa
// 2. Composition: map(fa)(f andThen g) == map(map(fa)(f))(g)

// Testing laws with ScalaCheck (discipline)
import org.scalacheck.Prop.forAll
import cats.kernel.laws.MonoidLaws

class MonoidSpec extends munit.FunSuite with Discipline {
  checkAll("Int Monoid", MonoidLaws[Int].monoid)
}

// Custom law test
def monoidLeftIdentity[A](implicit m: Monoid[A], arb: Arbitrary[A]) =
  forAll { (a: A) =>
    m.combine(m.empty, a) == a
  }

// Laws make type classes trustworthy:
// if an instance satisfies laws, generic code works correctly

Вывод класса типов (Scala 3)

Scala 3 упрощает вывод класса типов с ключевым словом 'derives' и Mirror. Компилятор может автогенерировать экземпляры для case-классов и перечислений, компонуя экземпляры элементов. Это устраняет шаблонность написания экземпляров для каждого case-класса (часто в Scala 2 с shapeless). Библиотеки вроде Cats и Circe поддерживают вывод Scala 3. Тип Mirror даёт доступ на этапе компиляции к структуре типа (типы полей, метки) для обобщённого программирования.

scala
// Scala 3: derive type class instances automatically
import scala.deriving.Mirror

trait Show[A] {
  def show(a: A): String
}

object Show {
  // Inline given for derivation
  given showInt: Show[Int] with
    def show(a: Int) = a.toString

  given showString: Show[String] with
    def show(s: String) = s""$s""

  // Derive for products (case classes)
  given showProduct[A](using m: Mirror.ProductOf[A])
      (using ev: Show[m.MirroredElemTypes]): Show[A] with
    def show(a: A): String = ???

  // Or use Scala 3's derivation
  inline given derive[A](using m: Mirror.Of[A]): Show[A] = ???
}

// Auto-derive for case classes
case class Person(name: String, age: Int) derives Show
// Show[Person] is generated automatically
13

For-Comprehensions: глубокое погружение

Базовые For-Comprehensions

For-comprehensions — синтаксический сахар для flatMap/map/withFilter. Каждый <- это flatMap (кроме последнего, который map). if guards становятся withFilter. yield возвращает коллекцию; пропуск yield делает императивным (foreach). Работает с любым типом, имеющим flatMap/map (Monad): List, Option, Future, Try, IO. Освоение for-comprehensions — ключ к идиоматичному Scala — они заменяют вложенные maps/flatMaps читаемым последовательным синтаксисом.

scala
// For comprehension: syntactic sugar for flatMap/map
val result = for {
  x <- List(1, 2, 3)
  y <- List(10, 20)
} yield x + y
// List(11, 21, 12, 22, 13, 23)

// Desugared:
List(1, 2, 3).flatMap { x =>
  List(10, 20).map { y => x + y }
}

// With filters (if guards)
val evens = for {
  x <- 1 to 10
  if x % 2 == 0
} yield x
// Vector(2, 4, 6, 8, 10)

// Desugared:
(1 to 10).withFilter(_ % 2 == 0).map(identity)

// Without yield: imperative (foreach)
for (x <- 1 to 3) println(x)  // 1, 2, 3

For с Option и Future

For-comprehensions работают с любой Monad. С Option они коротко-замыкают на None (возвращают None). С Future они коротко-замыкают при неудаче. Это делает последовательный асинхронный/могущий-не-удаться код похожим на прямолинейный императивный, оставаясь функциональным. Каждая строка <- может зависеть от предыдущих привязок. Это гораздо чище вложенных вызовов flatMap. Тот же синтаксис работает для Try, Either, IO и пользовательских монад.

scala
// Option: chain operations that might return None
def getUser(id: Int): Option[User] = ???
def getEmail(user: User): Option[String] = ???

val email: Option[String] = for {
  user <- getUser(42)
  email <- getEmail(user)
} yield email

// Desugared:
getUser(42).flatMap(user => getEmail(user).map(email => email))

// Future: chain async operations
val result: Future[Int] = for {
  user <- fetchUser(1)      // Future[User]
  posts <- fetchPosts(user) // Future[List[Post]]
} yield posts.size

// If any returns None/failed Future, the whole chain short-circuits
// This is the power of monadic composition

For с Either и обработкой ошибок

Either — типизированная обработка ошибок Scala. For-comprehensions компонуют Either, коротко-замыкая на Left (ошибка). Это функциональная обработка ошибок — без исключений, ошибки являются значениями. Тип Left — ошибка (обычно String или sealed trait). Either право-смещён в Scala 2.12+ (map/flatMap работают с Right). Этот паттерн заменяет try/catch компонуемым, типобезопасным распространением ошибок. Validated из Cats — альтернатива для накопления ошибок.

scala
// Either for error handling
def parseAge(s: String): Either[String, Int] =
  s.toIntOption.toRight(s"not a number: $s")

def validateAge(age: Int): Either[String, Int] =
  if (age >= 0) Right(age) else Left(s"negative: $age")

val result: Either[String, Int] = for {
  age <- parseAge("30")     // Right(30)
  valid <- validateAge(age) // Right(30)
} yield valid

// With error in chain
val error: Either[String, Int] = for {
  age <- parseAge("abc")    // Left("not a number: abc")
  valid <- validateAge(age) // skipped!
} yield valid
// error == Left("not a number: abc")

// Scala 3: for-comprehensions work with Either directly
// (Scala 2 needed either.map(_.right) or withFilter)

Десахаривание и пользовательские монады

Любой тип с flatMap и map поддерживает for-comprehensions — это паттерн Monad. Определите эти методы на вашем типе для включения for-синтаксиса. = (не <-) создаёт локальную привязку (не десахаривается в flatMap). Понимание десахаривания помогает отлаживать сложные comprehensions и реализовать пользовательские монады. Компилятор переводит for-comprehensions в цепочки flatMap/map/withFilter. Поэтому for работает единообразно с List, Option, Future, IO и т.д.

scala
// For-comprehensions require flatMap, map, withFilter
// Define your own monad to use for-comprehensions

case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
  def flatMap[B](f: A => Box[B]): Box[B] = f(value)
  def withFilter(p: A => Boolean): Box[A] =
    if (p(value)) this else throw new NoSuchElementException
}

val result = for {
  x <- Box(10)
  y <- Box(20)
  if x < y
} yield x + y
// Box(30)

// Desugaring steps:
// 1. Box(10).flatMap { x =>
// 2.   Box(20).withFilter(_ > x... wait, order matters
// Actual:
// Box(10).flatMap(x =>
//   Box(20).withFilter(y => x < y).map(y => x + y))

// Assignment within for (= instead of <-)
for {
  x <- List(1, 2, 3)
  doubled = x * 2  // local val
} yield doubled

For vs Map/FlatMap (когда использовать)

Используйте for-comprehensions для 2+ зависимых операций — они читаемее вложенных flatMaps. Для одного преобразования map яснее. Для выравнивания flatMap напрямую. Для побочных эффектов (без результата) используйте for без yield. For-comprehensions сияют, когда каждый шаг зависит от предыдущего (монадическое связывание). Они делают асинхронный/обрабатывающий ошибки код читаемым последовательно. Избегайте глубоко вложенных for (>5 уровней) — извлекайте помощники для читаемости.

scala
// For: sequential, dependent operations
val result = for {
  user <- fetchUser(id)
  profile <- fetchProfile(user.id)
  avatar <- fetchAvatar(profile.avatarId)
} yield avatar

// Equivalent with flatMap (harder to read):
fetchUser(id).flatMap(user =>
  fetchProfile(user.id).flatMap(profile =>
    fetchAvatar(profile.avatarId)))

// Map: single transformation (no chaining)
users.map(_.name)  // simple, use map

// FlatMap: when you need to chain but for is overkill
users.flatMap(_.posts)  // List[Post]

// For without yield: side effects
for (user <- users) {
  saveToDb(user)
  sendEmail(user)
}

// Guidelines:
// - 1 operation: use map/flatMap directly
// - 2+ dependent operations: use for-comprehension
// - Side effects: use for without yield
14

Scala 3: Given и Using

Given-экземпляры (замена implicit val)

Scala 3 заменяет implicit val/def на 'given'. given-ы яснее и более явные. Анонимные given-ы (given Type = ...) имеют сгенерированные компилятором имена. 'given T with' определяет экземпляры с несколькими методами. Условные given-ы (given [A: Ordering]: Ordering[List[A]]) заменяют implicit defs. given-ы в companion-объектах автоматически в неявной области, как в Scala 2. Смена ключевого слова уменьшает перегрузку 'implicit' (которая означала 4 вещи в Scala 2).

scala
// Scala 2: implicit val
// implicit val ec: ExecutionContext = ExecutionContext.global

// Scala 3: given
given ec: ExecutionContext = ExecutionContext.global

// Anonymous given (inferred name)
given ExecutionContext = ExecutionContext.global

// Given with 'with' for complex types
given Show[Int] with
  def show(a: Int): String = a.toString

// Given in companion object (automatic scope)
object MyType:
  given Ordering[MyType] = Ordering.by(_.id)

// Conditional given (like implicit def)
given [A: Ordering]: Ordering[List[A]] with
  def compare(a: List[A], b: List[A]): Int =
    a.zip(b).find((x, y) => x != y) match
      case Some((x, y)) => summon[Ordering[A]].compare(x, y)
      case None => a.length - b.length

Using-предложения (замена неявных параметров)

Scala 3 заменяет неявные параметры на 'using'. Это разделяет два значения 'implicit' (параметры vs преобразования). using-предложения передаются явно с ключевым словом 'using'. Context bounds [A: T] остаются теми же. summon[T] заменяет implicitly[T] (более ясное имя). Ключевое слово using делает очевидным в местах вызова, когда предоставляете контекст. Это чисто синтаксическое изменение — семантика та же, что у неявных параметров.

scala
// Scala 2: implicit parameter
// def process[A](data: List[A])(implicit ec: ExecutionContext): Unit

// Scala 3: using
def process[A](data: List[A])(using ec: ExecutionContext): Unit =
  data.foreach(println)

// Context bound (unchanged)
def sort[A: Ordering](list: List[A]): List[A] = list.sorted

// Multiple using clauses
def log(msg: String)(using level: Level, logger: Logger): Unit =
  logger.log(level, msg)

// summon replaces implicitly
def max[A: Ordering](a: A, b: A): A =
  val ord = summon[Ordering[A]]
  if ord.gt(a, b) then a else b

// Provide explicitly with 'using'
process(data)(using myEC)
log("hello")(using Level.INFO, myLogger)

Методы расширения (Scala 3)

Scala 3 заменяет implicit classes на 'extension' — яснее и сфокусированнее. extension (self: T) определяет методы на T. Расширения могут быть обобщёнными (extension [A]). Сочетая с using, они предоставляют синтаксис класса типов (42.show). Расширения — просто добавления методов, они не создают объектов-обёрток (нулевые накладные расходы с AnyVal в Scala 2, нативные в Scala 3). Это идиоматичный способ добавления методов к существующим типам в Scala 3.

scala
// Scala 2: implicit class
// implicit class RichInt(val self: Int) extends AnyVal {
//   def squared: Int = self * self
// }

// Scala 3: extension
extension (self: Int)
  def squared: Int = self * self
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)

5.squared   // 25
3.times { println("hi") }

// Extension on generic type
extension [A](self: List[A])
  def takeWhileInclusive(p: A => Boolean): List[A] = ???

// Extension with using (type class syntax)
extension [A](self: A) def show(using s: Show[A]): String = s.show(self)

42.show  // uses given Show[Int]

// Multiple extensions in one block
extension (s: String)
  def isBlank: Boolean = s.trim.isEmpty
  def words: List[String] = s.split(" ").toList

Enums и ADT (Scala 3)

Enums Scala 3 заменяют sealed trait + case objects для ADT. Они лаконичнее и поддерживают параметры, поля и методы. Case-ы enum могут иметь параметры (как case-классы). Сопоставление шаблонов проверяется на исчерпываемость. Enums могут быть обобщёнными (Option[A]). Это объединяет enums и ADT в одну конструкцию. Для открытых иерархий (расширяемых) используйте всё ещё sealed trait + case-классы. Для закрытых перечислений/ADT enum чище.

scala
// Scala 3 enum (replaces sealed trait + case objects)
enum Color:
  case Red, Green, Blue

enum HttpStatus(val code: Int):
  case Ok extends HttpStatus(200)
  case NotFound extends HttpStatus(404)
  case Error extends HttpStatus(500)

// Pattern matching (exhaustive)
def describe(c: Color): String = c match
  case Color.Red => "red"
  case Color.Green => "green"
  case Color.Blue => "blue"

// Access fields
HttpStatus.Ok.code  // 200

// Parameterized enum cases
enum Option[+A]:
  case Some(value: A)
  case None

// ADT with methods
enum Tree[+A]:
  case Leaf(value: A)
  case Node(left: Tree[A], right: Tree[A])

  def size: Int = this match
    case Leaf(_) => 1
    case Node(l, r) => l.size + r.size + 1

Определения верхнего уровня и отступы

Scala 3 допускает определения верхнего уровня — не нужно оборачивать всё в object. Это упрощает структуру файлов (как Python/Go). Новый синтаксис поддерживает как фигурные скобки, так и значимые отступы (опционально). if/then заменяет if/else со скобками. match может использоваться как выражение без скобок. Эти изменения делают Scala 3 более доступным, сохраняя обратную совместимость. Можно смешивать стили — используйте скобки, где они добавляют ясность, отступы где уменьшают шум.

scala
// Scala 3: top-level definitions (no class wrapper needed)
// File: MyMath.scala
def add(a: Int, b: Int): Int = a + b  // top-level function

val Pi: Double = 3.14159  // top-level val

type Id = Long  // top-level type alias

given Show[Int] = (a: Int) => a.toString  // top-level given

extension (i: Int) def squared: Int = i * i  // top-level extension

// Indentation-based syntax (optional, braces still work)
def factorial(n: Int): Int =
  if n <= 1 then 1
  else n * factorial(n - 1)

// Or with braces:
def factorial2(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial2(n - 1)
}

// if/then, match/case without braces
val sign = if x > 0 then 1 else -1
val desc = x match
  case 0 => "zero"
  case _ => "nonzero"
15

Методы расширения и синтаксис

Implicit Classes (Scala 2)

implicit class (Scala 2.10+) добавляет методы расширения к существующим типам. Расширение AnyVal делает его без аллокаций (компилятор стирает обёртку). Класс должен принимать один параметр конструктора (расширяемый тип). Методы implicit class становятся доступными на расширяемом типе. Так Scala обогащает Int, String и т.д. Помещайте implicit classes в package object или utility object для разделения. В Scala 3 используйте 'extension' вместо.

scala
// Scala 2: implicit class for extension methods
import scala.language.implicitConversions

implicit class RichString(val s: String) extends AnyVal {
  def wordCount: Int = s.split("\\s+").length
  def slug: String = s.toLowerCase.replaceAll("[^a-z0-9]+", "-")
  def encrypt(key: Int): String = s.map(c => (c + key).toChar)
}

"Hello World".wordCount  // 2
"My Blog Post!".slug     // "my-blog-post-"
"abc".encrypt(1)         // "bcd"

// AnyVal avoids allocation (zero overhead)
// implicit class must be in a trait, class, or object

// For generic extensions
implicit class RichList[A](val list: List[A]) extends AnyVal {
  def middle: Option[A] = list.lift(list.length / 2)
}

List(1, 2, 3, 4, 5).middle  // Some(3)

Методы расширения на практике

Методы расширения — стандартный способ добавления утилит к коллекциям и другим типам. distinctBy, chunked, tap — распространённые дополнения. Операторы === и =!= (через класс типов Eq) обеспечивают типобезопасное равенство (в отличие ==, допускающего кросс-типовые). Группируйте расширения в object и импортируйте где нужно. Это сохраняет базовые типы чистыми, позволяя доменно-специфические методы. Библиотека коллекций Scala сама активно использует этот паттерн.

scala
// Common pattern: add utility methods to collections
object CollectionExtensions {
  implicit class RichSeq[A](val seq: Seq[A]) extends AnyVal {
    def chunked(size: Int): Seq[Seq[A]] =
      seq.grouped(size).toSeq

    def distinctBy[B](f: A => B): Seq[A] =
      seq.groupBy(f).values.map(_.head).toSeq

    def tap(f: A => Unit): Seq[A] = {
      seq.foreach(f)
      seq
    }
  }
}

import CollectionExtensions._

List(1, 2, 3, 4, 5).chunked(2)  // List(List(1,2), List(3,4), List(5))
List("aa", "bb", "ab").distinctBy(_.head)  // List("aa", "ab")
List(1, 2, 3).tap(println)  // prints 1,2,3, returns List(1,2,3)

// Type class syntax via extension
implicit class EqOps[A](val a: A) extends AnyVal {
  def ===(b: A)(implicit eq: Eq[A]): Boolean = eq.eqv(a, b)
  def =!=(b: A)(implicit eq: Eq[A]): Boolean = !eq.eqv(a, b)
}

Неявные преобразования (используйте осторожно)

Неявные преобразования автоматически конвертируют типы, но они опасны — поведение кода становится неочевидным. Scala 2.10+ требует явного opt-in (scala.language.implicitConversions). Предпочитайте методы расширения (добавляющие методы без изменения типов) преобразованиям (изменяющим типы). Легитимные применения: Java interop (преобразование между типами коллекций), построение DSL. Компилятор предупреждает о неявных преобразованиях — относитесь к этим предупреждениям серьёзно. В Scala 3 given Conversion[T, U] — явный механизм.

scala
import scala.language.implicitConversions

// Implicit conversion between types
implicit def stringToInt(s: String): Int = s.toInt
val n: Int = "42"  // stringToInt("42")

// Dangerous: can cause surprising behavior
implicit def intToBoolean(n: Int): Boolean = n != 0
if (1) println("yes")  // works! (intToBoolean(1))

// Safer: use extension methods instead
extension (n: Int) def toBool: Boolean = n != 0
if (1.toBool) println("yes")

// When implicit conversions are appropriate:
// 1. Java interop (java.util.List <-> scala.List)
implicit def javaListToScala[A](jl: java.util.List[A]): List[A] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

// 2. Backward compatibility layers
// 3. DSL construction (use sparingly)

// Enable per-file: import scala.language.implicitConversions

Программирование на уровне типов

Программирование на уровне типов кодирует информацию в типах, проверяемых компилятором. Фантомные типы (неиспользуемые параметры типа) отслеживают состояние (Open/Closed), предотвращая неправильное использование (нельзя прочитать закрытый файл). Натуралы Пеано представляют числа как типы. Это обеспечивает гарантии корректности на этапе компиляции — ошибки становятся ошибками компиляции. Используется в конечных автоматах, единицах измерения, векторах с размером. Мощно, но сложно — используйте, когда безопасность стоит типовой сложности. Библиотеки вроде shapeless (Scala 2) обеспечивают продвинутое программирование на уровне типов.

scala
// Phantom types: track state at type level
sealed trait State
trait Open extends State
trait Closed extends State

class File[S <: State] private (val path: String)

object File {
  def open(path: String): File[Open] = new File[Open](path)
}

def close[S <: Open](f: File[S]): File[Closed] =
  new File[Closed](f.path)  // unsafe cast internally

def read[S <: Open](f: File[S]): String = "data"
// def read[S <: Closed](f: File[S]): String  // compile error

val f = File.open("test.txt")
read(f)  // OK: f is File[Open]
val closed = close(f)
// read(closed)  // COMPILE ERROR: closed is File[Closed]

// Type-level natural numbers (Peano)
sealed trait Nat
trait Zero extends Nat
trait Succ[N <: Nat] extends Nat

// Compile-time list length check
type _0 = Zero
type _1 = Succ[_0]
type _2 = Succ[_1]

Opaque Types (Scala 3)

Opaque types (Scala 3) создают newtypes с нулевыми накладными расходами — в отличие от value classes (AnyVal), они никогда не упаковываются и являются просто базовым типом во время выполнения. Различие типа существует только во время компиляции, предотвращая путаницу (Celsius vs Fahrenheit, UserId vs Long). Внутри определяющего объекта тип и его базовый тип взаимозаменяемы; снаружи они различны. Это паттерн 'newtype' из Haskell — доменно-ориентированные типы без накладных расходов во время выполнения. Используйте для ID, единиц и доменных примитивов.

scala
// Scala 2: value classes for zero-overhead wrappers
// case class UserId(value: Long) extends AnyVal

// Scala 3: opaque types (true zero overhead, no boxing)
object Types:
  type UserId = Long
  object UserId:
    def apply(value: Long): UserId = value

  type Email = String
  object Email:
    def apply(s: String): Email =
      require(s.contains("@"), "invalid email")
      s

import Types.*
val id: UserId = UserId(42)  // just a Long at runtime
val email: Email = Email("[email protected]")

// UserId and Long are NOT interchangeable outside the object
// def wrong(x: Long): UserId = x  // ERROR
// But inside the object, they're the same

// Newtype pattern: domain types without overhead
type Celsius = Double
type Fahrenheit = Double
// Prevents mixing up Celsius and Fahrenheit
def toF(c: Celsius): Fahrenheit = c * 9 / 5 + 32
16

Тестирование (ScalaTest и ScalaCheck)

Стили ScalaTest

ScalaTest предлагает несколько стилей. FunSuite простейший (test("name") { ... }). FlatSpec BDD-стиль ("A Stack" should "..."). Matchers предоставляет читаемые утверждения (shouldBe, should contain, should throw). Выберите один стиль на проект для согласованности. FunSuite популярен для модульных тестов; FlatSpec для тестов, сфокусированных на поведении. Все стили поддерживают одни и те же матчеры и хуки жизненного цикла. Стиль влияет на синтаксис, а не возможности.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

// FunSuite: simple test functions
class MyTest extends AnyFunSuite with Matchers {
  test("addition works") {
    1 + 1 should be (2)
    1 + 1 shouldBe 2
    List(1, 2, 3) should contain (2)
  }

  test("string operations") {
    "hello".length shouldBe 5
    "hello" should startWith ("he")
  }
}

// FlatSpec: BDD-style
import org.scalatest.flatspec.AnyFlatSpec
class StackSpec extends AnyFlatSpec with Matchers {
  "A Stack" should "pop values in LIFO order" in {
    val stack = Stack(1, 2, 3)
    stack.pop() shouldBe 3
  }

  it should "throw on empty pop" in {
    val stack = Stack()
    a [NoSuchElementException] shouldBe thrownBy(stack.pop())
  }
}

// WordSpec, FreeSpec, PropSpec also available

Утверждения и Matchers

Matchers обеспечивают выразительные утверждения. shouldBe/should be для равенства. contain, have size, have key для коллекций. startWith/endWith/include для строк. thrownBy для исключений. Пользовательские матчеры (be >, be <=) для сравнений. DSL 'should' читается как английский, делая тесты самодокументируемыми. Для сложных утверждений используйте пользовательские матчеры или простой assert(condition). Избегайте избыточного связывания матчеров — читаемость важнее хитрости.

scala
import org.scalatest.matchers.should.Matchers._

// Equality
result shouldBe 42
result should be (42)
result should equal (42)

// Collections
list should contain (5)
list should not contain (0)
list shouldBe empty
list should have size 5
list should contain inOrder (1, 2, 3)
map should contain key ("name")
map should contain value ("Alice")

// Strings
s should startWith ("Hello")
s should endWith ("world")
s should include ("lo")
s should fullyMatch regex "H.*d".r

// Exceptions
a [IOException] should be thrownBy riskyOp()
the [IOException] thrownBy riskyOp() should have message "fail"

// Custom matchers
result should be > 0
result should be <= 100

// Type checks
result shouldBe a [List[_]]
result should be an [IllegalArgumentException]

BeforeAndAfter и фикстуры

BeforeAndAfterEach запускает setup/teardown вокруг каждого теста. Переопределяйте beforeEach/afterEach. Для управления ресурсами loan-паттерн (withDb { conn => ... }) чище — обеспечивает очистку через try/finally и делает ресурс явным. ScalaTest также поддерживает контексты фикстур (FixtureContext) и разделяемые фикстуры через трейты. Предпочитайте loan-паттерн или методы-фикстуры изменяемому beforeEach-состоянию — это более функционально и избегает багов разделяемого состояния между тестами.

scala
import org.scalatest.BeforeAndAfterEach
import org.scalatest.funsuite.AnyFunSuite

class DbTest extends AnyFunSuite with BeforeAndAfterEach {
  var conn: Connection = _

  override def beforeEach(): Unit = {
    conn = DriverManager.getConnection("jdbc:h2:mem:test")
    conn.execute("CREATE TABLE users (id INT, name VARCHAR)")
  }

  override def afterEach(): Unit = {
    conn.close()
  }

  test("insert works") {
    conn.execute("INSERT INTO users VALUES (1, 'Alice')")
    val count = conn.query("SELECT COUNT(*) FROM users")
    count shouldBe 1
  }
}

// Fixture via loan pattern
class FixtureTest extends AnyFunSuite {
  def withDb(test: Connection => Unit): Unit = {
    val conn = DriverManager.getConnection("jdbc:h2:mem:test")
    try test(conn) finally conn.close()
  }

  test("query works") {
    withDb { conn =>
      conn.execute("INSERT ...")
      // assertions
    }
  }
}

Тестирование на основе свойств (ScalaCheck)

ScalaCheck генерирует случайные тестовые входы, проверяя свойства на множестве случаев. forAll запускает свойство с 100 случайными входами по умолчанию. whenever фильтрует входы. Пользовательские Gen-типы ограничивают сгенерированные данные (Gen.choose, Gen.nonEmptyListOf). Это ловит краевые случаи, которые пропустят примерные тесты (пустые списки, отрицательные числа, большие значения). Table-Driven тесты (Table) для конкретных случаев. Тестирование на основе свойств мощно для чистых функций и преобразований данных — определите, что всегда должно быть истинно.

scala
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.Prop.forAll

class ListSpec extends AnyFunSuite with ScalaCheckPropertyChecks {
  // Property: reversing twice = identity
  test("reverse twice is identity") {
    forAll { (xs: List[Int]) =>
      xs.reverse.reverse shouldBe xs
    }
  }

  // Property with conditions
  test("head of sorted list is min") {
    forAll { (xs: List[Int]) =>
      whenever(xs.nonEmpty) {
        xs.sorted.head shouldBe xs.min
      }
    }
  }

  // Custom generators
  import org.scalacheck.Gen
  val smallInt = Gen.choose(1, 100)
  val nonEmptyList = Gen.nonEmptyListOf(smallInt)

  test("custom generator") {
    forAll(nonEmptyList) { (xs: List[Int]) =>
      xs should not be empty
      xs.forall(_ >= 1) shouldBe true
    }
  }

  // Table-driven tests
  test("addition table") {
    val cases = Table(
      ("a", "b", "sum"),
      (1, 2, 3),
      (10, 20, 30),
      (-1, 1, 0)
    )
    forAll(cases) { (a, b, sum) =>
      a + b shouldBe sum
    }
  }
}

Мокирование и тестовые двойники

Mockito (через ScalaTestPlus) создаёт тестовые двойники. mock[T] создаёт мок; when(...).thenReturn(...) заглушает методы; verify проверяет взаимодействия. Используйте моки для изоляции тестируемого модуля от зависимостей (БД, API). Матчеры аргументов (argThat) проверяют конкретные аргументы. Не мокируйте избыточно — если мокируете всё, тестируйте реальную интеграцию вместо. Предпочитайте fakes (in-memory реализации) мокам для сложных зависимостей — они надёжнее и читаемее.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._

class UserServiceSpec extends AnyFunSuite with MockitoSugar {
  test("getUser returns user from repo") {
    // Create mock
    val repo = mock[UserRepository]
    val user = User(1, "Alice")

    // Stub: when X then Y
    when(repo.findById(1)).thenReturn(Some(user))

    val service = new UserService(repo)
    val result = service.getUser(1)

    result shouldBe Some(user)

    // Verify: was X called?
    verify(repo).findById(1)
    verify(repo, never()).findById(2)
  }

  test("createUser saves to repo") {
    val repo = mock[UserRepository]
    val service = new UserService(repo)

    service.createUser("Bob")

    // Verify with argument matcher
    verify(repo).save(argThat((u: User) => u.name == "Bob"))
  }
}

// Stubbing exceptions
when(repo.findById(99)).thenThrow(new RuntimeException("not found"))
17

Функции высшего порядка и ФП

Функции как значения

Функции — значения в Scala, можно хранить, передавать и возвращать. (A, B) => C — тип функции. Эта-расширение (multiply _) преобразует методы в значения-функции. Функции высшего порядка (принимающие/возвращающие функции) обеспечивают мощные абстракции: map, filter, reduce — HOF. Композиция функций (andThen, compose) строит конвейеры. Каррирование (adder(5) возвращает функцию) — частичное применение. Это основа функционального программирования в Scala.

scala
// Functions are first-class values
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x

// Apply
add(2, 3)  // 5
square(4)  // 16

// Method to function (eta-expansion)
def multiply(a: Int, b: Int): Int = a * b
val mul = multiply _  // or just 'multiply' in Scala 3
mul(3, 4)  // 12

// Higher-order: function taking function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(square, 2)  // 16

// Higher-order: function returning function
def adder(n: Int): Int => Int = _ + n
val add5 = adder(5)
add5(10)  // 15

// Function composition
val f: Int => Int = _ + 1
val g: Int => Int = _ * 2
val h = f andThen g  // f then g: (x+1)*2
val h2 = f compose g  // g then f: (x*2)+1
h(3)  // 8
h2(3)  // 7

Каррирование и частичное применение

Каррирование разделяет функцию на несколько списков параметров, обеспечивая частичное применение (зафиксировать некоторые args, получить функцию для остальных). Это помогает выводу типа (ранние параметры ограничивают поздние) и создаёт настраиваемые функции (withDb(config)). Заполнитель _ частично применяет: sum(1, _) создаёт функцию. Несколько списков параметров — механизм каррирования Scala. Используйте каррирование, когда некоторые args — 'конфигурация', другие — 'данные'.

scala
// Curried function: multiple parameter lists
def add(a: Int)(b: Int): Int = a + b
val add5: Int => Int = add(5)  // partial application
add5(10)  // 15

// Multiple parameter lists for type inference
def map[A, B](list: List[A])(f: A => B): List[B] =
  list.map(f)
map(List(1, 2, 3))(x => x * 2)  // A, B inferred from first list

// Curried form (Function types)
val curriedAdd: Int => Int => Int = a => b => a + b
curriedAdd(5)(10)  // 15

// Uncurrying
def uncurriedAdd(a: Int, b: Int): Int = a + b

// Practical: configuration via currying
def withDb(config: DbConfig)(f: Connection => Result): Result = ???
val withMyDb = withDb(myConfig) _
withMyDb { conn => /* ... */ }

// Partial application with placeholder
val sum: (Int, Int) => Int = _ + _
val addOne: Int => Int = sum(1, _)

Чистые функции и ссылочная прозрачность

Чистые функции всегда возвращают одинаковый выход для одинакового входа и не имеют побочных эффектов. Их легко тестировать, рассуждать о них, параллелить и компоновать. Ссылочная прозрачность означает, что можно заменить вызов функции её результатом без изменения поведения. Scala не обеспечивает чистоту, но библиотеки вроде Cats Effect (IO-монада) позволяют изолировать побочные эффекты. Чистое ядро + IO на краях — распространённая ФП-архитектура: бизнес-логика чистая, I/O обёрнуто в IO.

scala
// Pure function: same input → same output, no side effects
def pureAdd(a: Int, b: Int): Int = a + b  // pure

// Impure: depends on external state
var counter = 0
def impureAdd(a: Int): Int = { counter += 1; a + counter }  // impure

// Impure: side effect
def impurePrint(a: Int): Int = { println(a); a }  // side effect

// Referential transparency: can replace call with result
val x = pureAdd(2, 3)
val y = x + x  // same as pureAdd(2, 3) + pureAdd(2, 3)

// Benefits of purity:
// 1. Easy to test (no setup/teardown)
// 2. Easy to reason about (no hidden state)
// 3. Parallelizable (no shared state)
// 4. Memoizable (cache results)
// 5. Composable (predictable)

// IO monad for side effects (Cats Effect)
import cats.effect.IO
val program: IO[Unit] = IO.println("hello")
val mapped: IO[String] = IO.pure("world").map(_.toUpperCase)
// Side effects captured in IO, run at edge of program

Неизменяемые данные и персистентные коллекции

Неизменяемые структуры данных возвращают новые копии при модификации, разделяя структуру внутренне для эффективности (персистентные структуры данных). Коллекции Scala неизменяемы по умолчанию. case class copy создаёт модифицированные копии. Для глубоких обновлений библиотеки Lens (Monocle) предоставляют компонуемые аксессоры. Неизменяемость устраняет целые классы ошибок (гонки, неожиданные мутации) и облегчает рассуждение о коде. Стоимость производительности часто приемлема из-за структурного разделения (O(log n), не O(n)).

scala
// Immutable: operations return new collections
val list1 = List(1, 2, 3)
val list2 = list1 :+ 4  // List(1, 2, 3, 4)
// list1 is unchanged: List(1, 2, 3)

// Persistent data structures: share structure (efficient)
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1 + ("c" -> 3)  // shares structure with map1
// O(log n) due to structural sharing, not O(n) copy

// Case classes: copy for modification
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // new instance, alice unchanged

// Lens (Monocle library) for nested updates
import monocle.macros.GenLens
val ageLens = GenLens[User](_.age)
val updated = ageLens.modify(_ + 1)(alice)  // User("Alice", 31)

// Benefits:
// - No bugs from shared mutation
// - Easy to reason about (values don't change)
// - Free concurrency (no locks needed)
// - Undo/redo trivial (keep old versions)

Рекурсия и хвостовые вызовы

Хвостовая рекурсия (где рекурсивный вызов — последняя операция) оптимизируется компилятором Scala в цикл — без роста стека. @tailrec проверяет это во время компиляции. Паттерн аккумулятора (передача накопленного результата) преобразует не-хвостовую рекурсию в хвостовую. foldLeft/foldRight инкапсулируют распространённые паттерны рекурсии. Для глубокой не-хвостовой рекурсии используйте трамплины (Cats) или перепишите через folds. Предпочитайте folds явной рекурсии для ясности — они хвостовые и идиоматичные.

scala
// Regular recursion (can stack overflow)
def factorial(n: Int): Int =
  if (n <= 1) 1 else n * factorial(n - 1)
factorial(10000)  // StackOverflowError!

// Tail recursion: compiler optimizes to loop
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, acc: Int = 1): Int =
  if (n <= 1) acc else factorialTail(n - 1, n * acc)
factorialTail(10000)  // works (no stack overflow)

// @tailrec annotation: compiler verifies it's tail-recursive
// Error if not actually tail-recursive

// Trampoline for non-tail recursion
// (Cats' Trampoline or Free monads)
// Converts stack recursion to heap

// Fold as alternative to recursion
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
// foldLeft is tail-recursive internally

// Pattern: accumulator pattern
@tailrec
def reverse[A](list: List[A], acc: List[A] = Nil): List[A] =
  list match
    case Nil => acc
    case head :: tail => reverse(tail, head :: acc)
18

Case-классы и ADT

Основы case-классов

Case-классы — основной инструмент моделирования данных Scala. Компилятор генерирует equals, hashCode, toString, copy, apply, unapply (для сопоставления шаблонов) и методы-аксессоры. Они неизменяемы по умолчанию (val поля). Используйте для DTO, value-объектов, сообщений и доменных сущностей (когда неизменяемы). apply companion позволяет конструировать без 'new'. copy обеспечивает не-деструктивные обновления. Case-классы — основа ADT (алгебраических типов данных).

scala
// Case class: immutable data holder with boilerplate generated
case class Person(name: String, age: Int)

val alice = Person("Alice", 30)
val bob = Person("Bob", 25)

// Auto-generated methods:
alice.toString  // "Person(Alice,30)"
alice == Person("Alice", 30)  // true (structural equality)
alice.hashCode  // based on fields

// Pattern matching
alice match
  case Person(name, age) => s"$name is $age"

// Copy with modifications
val older = alice.copy(age = 31)  // Person("Alice", 31)

// Companion object with apply (no 'new' needed)
val p = Person("Charlie", 40)  // Person.apply called

// Fields accessed directly
alice.name  // "Alice"
alice.age   // 30

// Case class with default values
case class Point(x: Double = 0, y: Double = 0)
Point()  // Point(0.0, 0.0)
Point(y = 5)  // Point(0.0, 5.0)

Sealed-трейты и ADT

ADT (алгебраические типы данных) моделируют данные как закрытое множество случаев (sealed trait + case-классы/объекты). 'sealed' означает, что все подтипы в том же файле, обеспечивая исчерпывающее сопоставление шаблонов — компилятор предупреждает, если пропущен случай. Добавление нового case показывает все места, нуждающиеся в обновлении (безопасный рефакторинг). Case-объекты — синглтоны (без параметров). Option, List, Either — все ADT. Это функциональный способ моделирования доменов — каждый вариант — case, поведение в сопоставлении шаблонов.

scala
// ADT: sealed trait + case classes/objects
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(a: Double, b: Double, c: Double) extends Shape

// Exhaustive pattern matching (compiler checks all cases)
def area(shape: Shape): Double = shape match
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h
  case Triangle(a, b, c) =>
    val s = (a + b + c) / 2
    math.sqrt(s * (s-a) * (s-b) * (s-c))

// Adding a new case: compiler warns about non-exhaustive matches
case class Square(side: Double) extends Shape
// area now needs a Square case!

// Case objects for singletons (no parameters)
sealed trait Status
case object Active extends Status
case object Inactive extends Status
case object Pending extends Status

// Option and List are ADTs:
// sealed trait Option[+A]
// case class Some[A](value: A) extends Option[A]
// case object None extends Option[Nothing]

Возможности case-классов

Case-классы могут иметь методы, пользовательский apply (для проверки), реализовать трейты и быть обобщёнными. Пользовательский apply в companion может проверять перед конструированием (делать незаконные состояния непредставимыми). Case-классы могут реализовывать трейты для полиморфизма. Приватный конструктор (case class Email private) принуждает использовать apply companion для проверки. Это сочетает моделирование данных с инкапсуляцией — конструкторы, которые не могут тихо провалиться.

scala
// Case class with methods
case class Vec2(x: Double, y: Double) {
  def +(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)
  def magnitude: Double = math.sqrt(x*x + y*y)
  def dot(other: Vec2): Double = x*other.x + y*other.y
}

Vec2(1, 2) + Vec2(3, 4)  // Vec2(4.0, 6.0)
Vec2(3, 4).magnitude  // 5.0

// Case class with custom apply (validation)
case class Email private (value: String)
object Email {
  def apply(value: String): Email = {
    require(value.contains("@"), "invalid email")
    new Email(value)  // bypass public apply
  }
}
// Email("notanemail")  // IllegalArgumentException
Email("[email protected]")  // works

// Case class implementing trait
trait Jsonable {
  def toJson: String
}
case class User(name: String, age: Int) extends Jsonable {
  def toJson: String = s"""{"name":"$name","age":$age}"""
}

// Case class with type parameters
case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
}

Сопоставление шаблонов: глубокое погружение

Сопоставление шаблонов — мощный инструмент деструктуризации Scala. Поддерживает: шаблоны литералов (0, "hello"), шаблоны типов (s: String), шаблоны case-классов (User(name, age)), guards (if condition), вложенные шаблоны и привязку (@). Компилятор проверяет исчерпываемость для sealed-типов. Шаблоны пробуются сверху вниз. Используйте @ для привязки всего значения при извлечении частей. Сопоставление шаблонов заменяет if-else цепочки, проверки типов и деструктуризацию единым, читаемым синтаксисом.

scala
// Various pattern types
val x: Any = (1, "hello")

x match
  case (a: Int, b: String) => s"int $a, string $b"
  case (a, b) => s"pair: $a, $b"
  case _ => "other"

// Guards
def classify(n: Int): String = n match
  case n if n < 0 => "negative"
  case 0 => "zero"
  case n if n % 2 == 0 => "even positive"
  case _ => "odd positive"

// Case class patterns (nested)
case class Point(x: Int, y: Int)
case class Shape(center: Point, size: Int)

def isOrigin(s: Shape): Boolean = s match
  case Shape(Point(0, 0), _) => true
  case _ => false

// Named patterns (bind the whole while extracting)
case class User(name: String, age: Int)
def describe(u: User): String = u match
  case u @ User(name, age) if age < 18 => s"$name is a minor"
  case User(name, _) => s"$name is an adult"

// Type patterns
def handle(x: Any): String = x match
  case s: String => s"string: $s"
  case n: Int => s"int: $n"
  case list: List[_] => s"list of size ${list.size}"
  case None => "none"
  case _ => "unknown"

Экстракторы и пользовательские шаблоны

Экстракторы (unapply) обеспечивают сопоставление шаблонов на любом типе, не только case-классах. Это развязывает шаблон сопоставления от представления данных. Регулярные выражения — экстракторы (группы становятся привязками). Можно писать экстракторы для внешних типов (JSON, URL) без их изменения. unapply возвращает Option[(T1, T2, ...)] для извлечения или Boolean для простого сопоставления. Экстракторы делают сопоставление шаблонов расширяемым на любой источник данных. Case-классы автогенерируют unapply; пользовательские экстракторы добавляют сопоставление для не-case-классовых типов.

scala
// Custom extractor via unapply
object Email {
  def unapply(str: String): Option[(String, String)] = {
    val parts = str.split("@")
    if (parts.length == 2) Some((parts(0), parts(1)))
    else None
  }
}

// Use in pattern matching
"[email protected]" match
  case Email(user, domain) => s"user: $user, domain: $domain"
  case _ => "not an email"

// Boolean extractor (no extracted values)
object Even {
  def unapply(n: Int): Boolean = n % 2 == 0
}

5 match
  case Even() => "even"
  case _ => "odd"

// Extractor with variable arity
object Pair {
  def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t._1, t._2)
}

// Regex as extractor
val Date = "(\\d{4})-(\\d{2})-(\\d{2})".r
"2024-01-15" match
  case Date(year, month, day) => s"$year/$month/$day"
  case _ => "not a date"

// Practical: parse without case classes
object Json {
  def unapply(s: String): Option[Any] =
    scala.util.Try(ujson.read(s)).toOption
}
19

Коллекции: глубокое погружение

Неизменяемые коллекции

Коллекции Scala неизменяемы по умолчанию. List — связный список (O(n) случайный доступ). Vector — дерево с O(log n) доступом. Map и Set — на основе хеша. Операции возвращают новые коллекции, разделяя структуру для эффективности.

scala
val list = List(1, 2, 3)
val vector = Vector(1, 2, 3)  // Fast random access
val set = Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)
// All immutable: operations return new collections
val updated = map + ("c" -> 3)  // New map

Операции с коллекциями

map преобразует элементы. filter выбирает. reduce комбинирует. grouped разбивает на чанки. flatten объединяет вложенные коллекции. flatMap отображает и выравнивает. Все возвращают новые коллекции. Ленивые коллекции используют .view или .iterator для эффективности.

scala
val nums = (1 to 10).toList
val doubled = nums.map(_ * 2)
val evens = nums.filter(_ % 2 == 0)
val sum = nums.reduce(_ + _)
val grouped = nums.grouped(3).toList  // List(List(1,2,3), List(4,5,6), ...)
val flat = List(List(1,2), List(3,4)).flatten  // List(1,2,3,4)

Сопоставление шаблонов коллекций

Сопоставление шаблонов работает на коллекциях. :: (cons) совпадает с головой и хвостом. List(a, b, c) совпадает ровно с 3 элементами. Nil совпадает с пустым списком. _ — шаблон с подстановкой. Полезно для разбора и деструктуризации. Исчерпывающее сопоставление предотвращает ошибки.

scala
val list = List(1, 2, 3)
list match {
    case head :: tail => println(s"Head: $head")
    case Nil => println("Empty")
}
List(1, 2, 3) match {
    case List(a, b, c) => println(s"$a, $b, $c")
    case _ => println("Other")
}

Изменяемые коллекции

Изменяемые коллекции (ArrayBuffer, mutable.Set, mutable.Map) модифицируют на месте. Быстрее для частых обновлений, но не потокобезопасны. Используйте, когда важна производительность и неизменяемость не нужна. Преобразуйте в неизменяемые через .toMap, .toSet.

scala
import scala.collection.mutable
val buffer = mutable.ArrayBuffer(1, 2, 3)
buffer += 4  // Add element
buffer -= 2  // Remove element
buffer(0) = 10  // Update
val mset = mutable.Set(1, 2, 3)
val mmap = mutable.Map("a" -> 1)
mmap("b") = 2

Ленивые коллекции

view создаёт ленивое представление: операции откладываются до принуждения. Полезно для цепочечных операций на больших коллекциях. LazyList (Scala 2.13+) — ленивая последовательность. Бесконечные последовательности возможны с ленивым вычислением. Принуждайте через .toList, .toArray.

scala
val lazyView = (1 to 1000000).view.map(_ * 2).filter(_ > 100)
// No computation yet
val first = lazyView.head  // Only computes first
val result = lazyView.take(10).toList  // Only 10 elements processed
// LazyList (formerly Stream)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
20

Implicits

Неявные параметры

Неявные параметры передаются автоматически, когда в области видимости. Компилятор ищет соответствующее неявное значение. Используется для конфигурации, классов типов и контекста. Можно переопределить явно. Несколько implicits должны иметь разные типы.

scala
def connect(url: String)(implicit timeout: Int): Unit = {
    println(s"Connecting to $url with timeout $timeout")
}
implicit val defaultTimeout: Int = 5000
connect("http://example.com")  // Uses 5000
connect("http://example.com")(3000)  // Explicit override

Неявные преобразования

Неявные преобразования автоматически конвертируют типы. Могут быть опасными (неожиданные преобразования). implicit class добавляет методы расширения. Scala 3 использует given/using и extension для ясности. Предпочитайте методы расширения сырым преобразованиям.

scala
import scala.language.implicitConversions
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // Converts via intToString
// Extension methods (Scala 3 preferred)
implicit class RichInt(val n: Int) extends AnyVal {
    def squared: Int = n * n
}
5.squared  // 25

Классы типов

Классы типов — паттерн для ad-hoc полиморфизма. Трейт определяет поведение, экземпляры предоставляют реализации для конкретных типов. Неявное разрешение находит экземпляр. Гибче наследования. Распространено в Cats и Shapeless.

scala
trait Show[A] {
    def show(a: A): String
}
object Show {
    implicit val intShow: Show[Int] = (a: Int) => a.toString
    implicit val stringShow: Show[String] = (a: String) => a
    def apply[A](a: A)(implicit s: Show[A]): String = s.show(a)
}
Show(42)  // "42"
Show("hello")  // "hello"

Context Bounds

Context bounds [A: TypeClass] — синтаксический сахар для неявных параметров. Экземпляр класса типов доступен через implicitly. Чистый синтаксис для ограничений класса типов. Распространён с Ordering, Numeric и пользовательскими классами типов.

scala
def max[A: Ordering](a: A, b: A): A = {
    val ord = implicitly[Ordering[A]]
    if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
    if (ord.gt(a, b)) a else b

Given/Using (Scala 3)

Scala 3 заменяет implicits на given/using для ясности. given определяет экземпляр, using объявляет параметр. extension заменяет implicit class. Более явно и читаемо. Инструменты миграции преобразуют implicits Scala 2.

scala
// Scala 3 syntax
given defaultTimeout: Int = 5000
def connect(url: String)(using timeout: Int): Unit =
    println(s"Connecting with $timeout")
connect("http://example.com")  // Uses given
// Extension methods
extension (n: Int)
    def squared: Int = n * n
21

Параллелизм

Future

Future представляет асинхронное вычисление. onComplete обрабатывает завершение. map/flatMap компонуют операции. ExecutionContext предоставляет потоки. Futures неизменяемы и одноразовые. Используйте for-comprehension для нескольких futures.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
    Thread.sleep(1000)
    42
}
f.onComplete {
    case Success(v) => println(v)
    case Failure(e) => println(e)
}
// Map/flatMap for chaining
val f2 = f.map(_ * 2).flatMap(x => Future(x + 1))

for-comprehension с Futures

for-comprehension десахаривается в flatMap/map. Последовательно: каждый шаг ждёт предыдущий. Для параллельного выполнения запустите futures до for. Гораздо читаемее вложенных обратных вызовов. Работает с любой монадой (Future, Option, List).

scala
def getUser(id: Int): Future[User] = ...
def getOrders(user: User): Future[List[Order]] = ...
val result: Future[List[Order]] = for {
    user <- getUser(1)
    orders <- getOrders(user)
} yield orders
// Equivalent to flatMap/map chaining

Параллельные Futures

Запуск futures до for-comprehension выполняет их параллельно. Future.sequence преобразует List[Future[T]] в Future[List[T]]. Future.traverse отображает и последовательно соединяет за один шаг. zip комбинирует два futures. Все завершаются, когда завершается самый медленный.

scala
val f1 = Future { compute1() }
val f2 = Future { compute2() }
val combined: Future[(Int, Int)] = for {
    r1 <- f1
    r2 <- f2
} yield (r1, r2)
// Or: Future.sequence(List(f1, f2))
// Or: Future.traverse(list)(compute)

Актёры (Akka)

Актёры Akka инкапсулируют состояние и общаются через сообщения. Нет разделяемого изменяемого состояния. Каждый актёр обрабатывает одно сообщение за раз. ! (tell) отправляет fire-and-forget. ? (ask) возвращает Future. Супервизия обрабатывает неудачи. Идеально для параллельных состоянии-хранящих систем.

scala
import akka.actor.*
class Counter extends Actor {
    var count = 0
    def receive = {
        case "inc" => count += 1
        case "get" => sender() ! count
    }
}
val system = ActorSystem("mySystem")
val counter = system.actorOf(Props[Counter], "counter")
counter ! "inc"
counter ! "get"

Cats Effect IO

Cats Effect IO — чистая функциональная IO-монада. Ссылочно прозрачна: IO(println("x")) — значение. Компонуется через for-comprehension. Отменяемая и ресурсо-безопасная. unsafeRunSync запускает на краю программы. Альтернатива Future с лучшей семантикой.

scala
import cats.effect.IO
val program: IO[Int] = for {
    _ <- IO(println("Start"))
    result <- IO.pure(42)
    _ <- IO(println(s"Got $result"))
} yield result
// Run at end of world
program.unsafeRunSync()
// Referentially transparent, cancellable
22

Сопоставление шаблонов: глубокое погружение

Case-классы

Case-классы автогенерируют equals, hashCode, toString и apply/unapply. Идеальны для сопоставления шаблонов. Сопоставление шаблонов деструктурирует их. Неизменяемы по умолчанию. Используйте для алгебраических типов данных. copy() создаёт модифицированные копии.

scala
case class Point(x: Int, y: Int)
val p = Point(1, 2)
p match {
    case Point(0, 0) => "origin"
    case Point(0, _) => "on y-axis"
    case Point(x, 0) => s"on x-axis at $x"
    case Point(x, y) => s"at ($x, $y)"
}

Sealed-трейты

sealed-трейты ограничивают подтипы тем же файлом. Компилятор проверяет исчерпываемость в сопоставлении шаблонов. Добавление нового подтипа вызывает предупреждения во всех match. Идеально для закрытых иерархий типов. Сочетается с case-классами для ADT.

scala
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
    case Circle(r) => math.Pi * r * r
    case Square(s) => s * s
    case Rectangle(w, h) => w * h
}  // Compiler warns if not exhaustive

Guards и экстракторы

Guards (if) добавляют условия к шаблонам. Пользовательские экстракторы (unapply) обеспечивают сопоставление шаблонов на любом типе. unapply возвращает Option для индикации совпадения. Экстракторы развязывают сопоставление от типа. Мощно для DSL и разбора.

scala
def classify(n: Int): String = n match {
    case x if x < 0 => "negative"
    case 0 => "zero"
    case x if x % 2 == 0 => "even"
    case _ => "odd"
}
// Custom extractor
object Even {
    def unapply(n: Int): Option[Int] =
        if (n % 2 == 0) Some(n / 2) else None
}
4 match { case Even(half) => s"half is $half" }

Частичные функции

Частичные функции определены только для некоторых входов. isDefinedAt проверяет. collect применяет только где определено. Полезно для обратных вызовов и маршрутизации. orElse комбинирует частичные функции. lift преобразует в полную функцию, возвращающую Option.

scala
val pf: PartialFunction[Int, String] = {
    case 1 => "one"
    case 2 => "two"
}
pf.isDefinedAt(1)  // true
pf.isDefinedAt(3)  // false
// Collect = filter + map
List(1, 2, 3, 1).collect(pf)  // List("one", "two", "one")

Сопоставление шаблонов типов

Сопоставление шаблонов работает с типами, но стирание типов затрагивает обобщённые. List(a, b) совпадает со списком из 2 элементов. List(_*) совпадает с любым списком. Избегайте сопоставления по обобщённым типам вроде List[Int] (стирается). Используйте type tags для информации типа во время выполнения.

scala
def describe(x: Any): String = x match {
    case i: Int => s"Int: $i"
    case s: String => s"String: $s"
    case List(a, b) => s"Two-element list: $a, $b"
    case List(_*) => "List with elements"
    case Some(v) => s"Some: $v"
    case None => "None"
    case _ => "Unknown"
}
23

Функциональное программирование

Функции высшего порядка

Функции высшего порядка принимают или возвращают функции. Каррирование разделяет много-аргументные функции на одно-аргументные цепочки. Частичное применение фиксирует некоторые аргументы. Обеспечивает композицию и переиспользование функций. _ создаёт частично применённую функцию.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ + 3, 5)  // 11
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _  // Partial application
add5(3)  // 8

Option и Either

Option представляет опциональные значения: Some или None. Избегает null. getOrElse предоставляет значение по умолчанию. Either представляет успех (Right) или неудачу (Left). Лучше исключений для ожидаемых ошибок. Оба — монады: map, flatMap, for-comprehension.

scala
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
find(1).getOrElse("Unknown")  // "Alice"
find(-1).getOrElse("Unknown")  // "Unknown"
// Either for error handling
def parse(s: String): Either[String, Int] =
    try Right(s.toInt)
    catch { case _: Exception => Left(s"Not a number: $s") }

Композиция функций

compose компонует функции справа налево (как в математике). andThen компонует слева направо (более читаемо). Обе создают новые функции. Полезно для построения конвейеров. Функции — значения первого класса в Scala.

scala
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
// Compose (right to left)
val f = addOne compose double  // double then addOne
f(3)  // 7
// AndThen (left to right)
val g = addOne andThen double  // addOne then double
g(3)  // 8

Рекурсия и хвостовая рекурсия

Хвостовая рекурсия оптимизируется в цикл компилятором. Рекурсивный вызов должен быть последней операцией. Аннотация @tailrec проверяет это во время компиляции. Паттерн аккумулятора переносит состояние. Предотвращает переполнение стека для глубокой рекурсии.

scala
// Not tail-recursive: stack overflow for large n
def factorial(n: Int): Int =
    if (n <= 1) 1 else n * factorial(n - 1)
// Tail-recursive: optimized to loop
import scala.annotation.tailrec
@tailrec
def factorial(n: Int, acc: Int = 1): Int =
    if (n <= 1) acc else factorial(n - 1, n * acc)

Монады

Монады имеют flatMap и unit (pure/pure). Они связывают операции с контекстом (Option: отсутствие, List: не-детерминизм, Future: асинхронность). for-comprehension десахаривается в flatMap/map. Законы монады обеспечивают корректную композицию. Cats предоставляет класс типов Monad.

scala
// Monad laws: left identity, right identity, associativity
// Option is a monad:
Some(5).flatMap(x => Some(x + 1))  // Some(6)
None.flatMap(x => Some(x + 1))  // None
// List is a monad:
List(1, 2).flatMap(x => List(x, x * 10))  // List(1, 10, 2, 20)
// for-comprehension is monadic sugar:
for {
    x <- Some(5)
    y <- Some(x + 1)
} yield y  // Some(6)
24

Распространённые подводные камни

Null vs Option

Scala имеет null для совместимости с Java, но это не поощряется. Option явно представляет отсутствие. Сопоставление шаблонов принуждает обрабатывать None. Используйте .toOption на Try для исключений. Избегайте null в коде Scala; оставьте для Java interop.

scala
// BAD: null
def find(id: Int): String =
    if (id > 0) "Alice" else null
// GOOD: Option
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
// Scala avoids null; use Option
// NullPointer exceptions are rare in idiomatic Scala

Var vs Val

val неизменяем (значение), var изменяем (переменная). Предпочитайте val для более безопасного, предсказуемого кода. Изменяемое состояние усложняет рассуждение и параллелизм. Используйте var только для локальной производительности или когда действительно нужно. Коллекции неизменяемы по умолчанию.

scala
var x = 1  // Mutable
x = 2  // OK
val y = 1  // Immutable
// y = 2  // Error
// Prefer val for immutability
// Use var only when necessary
// Mutable state causes bugs in concurrent code

Равенство

Scala == вызывает equals (значимое равенство), в отличие от Java. eq проверяет равенство ссылок. ne — отрицание eq. Всегда используйте == для значимого сравнения. Case-классы имеют корректные equals. Для пользовательских классов переопределяйте equals и hashCode.

scala
val a = List(1, 2)
val b = List(1, 2)
a == b  // true (value equality)
a eq b  // false (reference equality)
// In Java: a == b compares references
// In Scala: == calls equals (value)
// Use eq for reference equality (rarely needed)

Неявная неоднозначность

Несколько implicits одного типа вызывают ошибки неоднозначности. Держите неявную область чистой. Используйте конкретные типы (newtypes) для различения. given/using в Scala 3 яснее. Избегайте неявных преобразований; они вызывают неожиданное поведение.

scala
implicit val s1: String = "hello"
implicit val s2: String = "world"
def greet(implicit s: String) = println(s)
greet  // Error: ambiguous implicit values
// Fix: only one implicit in scope
// Or: pass explicitly
greet(s1)

By-Name vs By-Value

By-name параметры (=> T) вычисляются лениво, каждый раз при использовании. By-value параметры вычисляются один раз до вызова. By-name обеспечивает пользовательские управляющие структуры (unless, while). Может вызвать многократное вычисление; кэшируйте через lazy val если нужно.

scala
// By-value: evaluated once
def log(msg: String) = println(msg)
log(expensiveComputation())  // Evaluated before call
// By-name: evaluated each use
def log(msg: => String) = println(msg)
log(expensiveComputation())  // Evaluated only if called
// Useful for lazy evaluation and control flow
def unless(cond: Boolean)(body: => Unit): Unit =
    if (!cond) body
25

Актёры Akka

Система актёров

ActorSystem — контейнер для актёров. actorOf создаёт актёров с именем. ! (tell) отправляет сообщение асинхронно. ? (ask) отправляет и возвращает Future. Актёры идентифицируются по пути. terminate останавливает систему корректно.

scala
import akka.actor.*
val system = ActorSystem("MySystem")
val actor = system.actorOf(Props[MyActor], "myActor")
actor ! "Hello"  // Fire-and-forget
val future = actor ? "Query"  // Ask pattern
system.terminate()

Жизненный цикл актёра

preStart выполняется при создании, postStop при завершении. preRestart/postRestart обрабатывают супервизию. Актёр останавливается перед рестартом, затем запускается заново. Внутреннее состояние теряется при рестарте. Используйте preStart для инициализации и postStop для очистки.

scala
class MyActor extends Actor {
    override def preStart(): Unit = println("Starting")
    override def postStop(): Unit = println("Stopped")
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        println("Restarting")
        super.preRestart(reason, message)
    }
    def receive = {
        case "ping" => sender() ! "pong"
    }
}

Супервизия

Супервизия определяет, как обрабатываются неудачи. Resume: продолжить с тем же состоянием. Restart: пересоздать с чистым состоянием. Stop: terminate навсегда. Escalate: пусть родитель обработает. OneForOneStrategy затрагивает только упавшего дочернего. AllForOneStrategy затрагивает всех дочерних.

scala
class Supervisor extends Actor {
    override val supervisorStrategy = OneForOneStrategy() {
        case _: ArithmeticException => Resume  // Continue
        case _: NullPointerException => Restart  // Restart actor
        case _: Exception => Stop  // Stop actor
    }
    def receive = { case _ => }
}

Роутеры

Роутеры распределяют сообщения нескольким актёрам. Pool создаёт и управляет воркерами. Group использует существующих актёров. Стратегии: RoundRobin, Random, SmallestMailbox, ConsistentHashing, ScatterGatherFirst. Роутеры улучшают пропускную способность, параллеля работу.

scala
// Pool router: creates workers
val router = system.actorOf(
    RoundRobinPool(5).props(Props[Worker]),
    "router"
)
router ! "work"  // Distributed to one of 5 workers
// Group router: uses existing actors
val group = system.actorOf(
    RoundRobinGroup(paths).props(),
    "group"
)

Персистентность

PersistentActor сохраняет события в журнал (event sourcing). Команды валидируются, затем персистятся как события. При рестарте события воспроизводятся через receiveRecover. Состояние восстанавливается из событий. Обеспечивает восстановление после сбоев. Используйте EventSourcedBehavior для Akka Typed.

scala
class Counter extends PersistentActor {
    var count = 0
    override def persistenceId = "counter-1"
    override def receiveCommand = {
        case "inc" => persist(Incremented) { event =>
            count += 1
        }
    }
    override def receiveRecover = {
        case Incremented => count += 1
    }
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.