Основы
Переменные и типы
Предпочитайте val (неизменяемое) var (изменяемое) для более безопасного, предсказуемого кода. Scala выводит типы, но явные аннотации помогают читаемости для публичных API. Всё — объект, нет примитивов (Int, Boolean — классы).
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-последовательности. Эти префиксы делают построение строк типобезопасным и читаемым.
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-классы для именованных полей и лучшей читаемости.
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 или устранения неоднозначности.
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 inferredUnit и Nothing
Unit подобен void (одно значение: ()). Nothing — нижний тип без экземпляров, используется для функций, которые никогда не возвращаются (throw, бесконечный цикл). Nothing — подтип всех типов, обеспечивая гибкий вывод типа.
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")Строки
Распространённые строковые методы
Строки Scala — Java-строки с дополнительными методами через неявные преобразования (StringOps). Большинство методов возвращают новые строки (неизменяемые). Используйте их вместо ручных циклов для ясности и корректности.
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 или шаблонов, встроенных в код.
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 для построения больших строк в циклах. Избегайте повторной конкатенации + в циклах (создаёт много промежуточных объектов).
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 для функциональной обработки ошибок без исключений.
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 под капотом.
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#Структуры данных
List и Seq
List — неизменяемый односвязный список: O(1) head/prepend, O(n) случайный доступ. Используйте Vector для случайного доступа (O(1) эффективный). +: добавляет в начало, :+ добавляет в конец. Предпочитайте неизменяемые коллекции для потокобезопасности.
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.
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.
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, делая отсутствие явным в типе.
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 для индексного доступа.
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Поток управления
If / Else (выражение)
В Scala if/else — выражение, возвращающее значение. Это устраняет необходимость тернарного оператора. Обе ветви должны иметь совместимые типы. Используйте для лаконичного условного присваивания.
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 neededFor-comprehension
for-comprehensions итерируют и могут фильтровать (if guards) и преобразовывать (yield). Без yield это цикл; с yield строит коллекцию. to включает конец, until исключает. Эквивалентно цепочкам flatMap/map/filter.
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), сопоставление типов и деструктуризацию. Должно быть исчерпывающим (компилятор предупреждает о пропущенных случаях). Это выражение, возвращающее значение.
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 только когда требует производительность или для побочных эффектов.
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 immutabilityTry / Catch / Finally
try/catch/finally как в Java, но использует сопоставление шаблонов в catch. Предпочитайте Try для функциональной обработки ошибок — оборачивает исключения как значения Success/Failure, обеспечивая цепочки map/flatMap без шаблонности try/catch.
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Функции
Определение метода
Методы используют def name(params): ReturnType = body. Поддерживаются параметры по умолчанию и именованные аргументы. Возврат Unit = только побочный эффект. Одно-выражительные тела опускают фигурные скобки. = обязателен (без него это процедура, возвращающая Unit).
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 и т.д.
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. Это сердце функционального программирования.
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 коллекций.
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 откладывает инициализацию до первого доступа — для дорогих или опциональных значений.
// 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 }Классы и ООП
Класс и конструктор
Параметры первичного конструктора в сигнатуре класса. Параметры val становятся неизменяемыми полями (публичный getter), var — изменяемыми. Тело класса ЕСТЬ конструктор. Вспомогательные конструкторы используют def this(...) и должны вызывать другой конструктор.
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) // trueCase-класс
Case-классы — неизменяемые классы данных с автогенерацией equals, hashCode, toString, copy и companion-объектом с apply/unapply. Используйте для моделирования данных и сопоставления шаблонов. 'new' не нужен. Это основа ADT в 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 и стека-модификаций через линеаризацию.
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 linearizationObject (singleton)
object объявляет singleton (один экземпляр). Companion-объекты (то же имя, что и класс) содержат static-подобные методы, фабрики (apply) и экстракторы (unapply). Используйте apply для фабричных методов, чтобы вызывающий опускал 'new'. Это идиоматичный 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, когда нужны параметры конструктора или базовый тип. Одиночное наследование классов.
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Коллекции и функциональное
Map / Filter / Fold
map преобразует, filter выбирает, reduce/foldLeft агрегирует. Это ядро функциональной трансформации данных. foldLeft принимает seed и ассоциативен; reduce требует непустую коллекцию. Используйте их вместо циклов для ясности и неизменяемости.
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, 5FlatMap и For-Comprehension
flatMap отображает и выравнивает за один шаг — необходимо для вложенных коллекций и монадических операций. For-comprehensions — синтаксический сахар для цепочек flatMap/map/filter. Используйте для сложных вложенных преобразований — более читаемо.
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 с компаратором. Возвращают новые коллекции (неизменяемые). Используйте для анализа данных, категоризации и упорядочивания.
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 для функционального распространения ошибок.
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) позволяет бесконечные последовательности — элементы вычисляются по требованию.
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(_ + _)Сопоставление шаблонов
Сопоставление case-классов
Sealed-трейты + case-классы формируют алгебраические типы данных (ADT). Компилятор проверяет исчерпываемость — добавьте case, и он предупредит, где нужно обновить match. Сопоставление шаблонов деструктурирует case-классы напрямую. Это идиоматичное моделирование 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 для диапазонов, условий или сложной логики, которую простые шаблоны не могут выразить.
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-шаблон деструктурирует списки на голову и хвост. _* совпадает с нулём или более элементов в массивах/списках. Эти шаблоны обеспечивают рекурсивную обработку списков и структурную декомпозицию. Мощно для разбора и обхода деревьев.
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 проверок.
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.
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")
}Обобщённые типы и неявные
Обобщённые классы и методы
Обобщённые типы (параметры типа [A]) пишут типобезопасный, переиспользуемый код. Используйте [A] для одного типа, [A, B] для двух. Вывод типа обычно определяет тип. Обобщённые типы стираются во время выполнения (ограничение JVM), но проверяются во время компиляции.
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 вместо. Эти ограничения сужают параметры типа.
// 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-объектах.
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. Гибче наследования.
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'.
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Параллелизм и Future
Future и Async
Future представляет асинхронное вычисление. onComplete регистрирует обратный вызов. Никогда не блокируйте (Await.result) в продакшен-веб-серверах — занимает потоки. Используйте for-comprehensions для функциональной цепочки futures. Требуется ExecutionContext.
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 отображает и последовательно соединяет за один шаг. Это идиоматичный способ компоновать асинхронную работу.
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 работы.
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 collectionsPromise (ручной Future)
Promise — записываемая сторона Future — завершаете его вручную через success/failure. Используйте при мостинге callback-based API к Futures или когда нужно завершить Future из нескольких мест. Future только для чтения; Promise одноразовой записи.
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 sideSync vs Async (Await)
Await.result блокирует текущий поток до завершения Future (с таймаутом). Используйте только в тестах или main-методах — блокировка в асинхронном коде нарушает цель. В продакшене используйте обратные вызовы (onComplete, map) или for-comprehensions для неблокирующего режима.
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 insteadImplicits: глубокое погружение
Неявные параметры
Неявные параметры передаются автоматически компилятором, когда неявное значение соответствующего типа в области видимости. Это уменьшает шаблонность для 'контекстных' параметров (ExecutionContext, логирование, конфигурация). Компилятор ищет: локальная область, companion-объекты, неявная область. Можно всегда передать явно для переопределения. Избыточное использование усложняет трассировку кода — используйте для подлинно контекстных зависимостей.
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.
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. Понимание порядка разрешения критично для дизайна библиотек — помещайте значения по умолчанию в низкоприоритетные трейты, чтобы пользователи могли переопределять.
// 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]. Это идиоматичный способ выражения требований класса типов.
// 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 для пакета. Этот дизайн обеспечивает использование классов типов 'без импорта'.
// 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
}Классы типов
Определение класса типов
Класс типов — трейт, параметризованный типом, с экземплярами, предоставляющими поведение для конкретных типов. В отличие от наследования, можно добавлять экземпляры класса типов ретроактивно (для типов, которыми не владеете). Show[A] определяет, как отображать A. Экземпляры живут в companion-объекте (автоматическая неявная область). Это ad-hoc полиморфизм — разное поведение на тип без изменения типов. Классы типов — самая мощная абстракция 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, использующие класс типов.
// 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. Классы типов позволяют писать обобщённый, переиспользуемый код, работающий с многими типами.