Scala 3 — Book

Почему Scala 3?

Language

Использование Scala, и Scala 3 в частности, дает много преимуществ. Трудно перечислить их все, но “топ десять” может выглядеть так:

  1. Scala сочетает в себе функциональное программирование (ФП) и объектно-ориентированное программирование (ООП)
  2. Scala статически типизирован, но часто ощущается как язык с динамической типизацией
  3. Синтаксис Scala лаконичен, но все же удобочитаем; его часто называют выразительным
  4. Implicits в Scala 2 были определяющей функцией, а в Scala 3 они были улучшены и упрощены
  5. Scala легко интегрируется с Java, поэтому вы можете создавать проекты со смешанным кодом Scala и Java, а код Scala легко использует тысячи существующих библиотек Java
  6. Scala можно использовать на сервере, а также в браузере со Scala.js
  7. Стандартная библиотека Scala содержит десятки готовых функциональных методов, позволяющих сэкономить ваше время и значительно сократить потребность в написании пользовательских циклов for и алгоритмов
  8. “Best practices”, встроенные в Scala, поддерживают неизменность, анонимные функции, функции высшего порядка, сопоставление с образцом, классы, которые не могут быть расширены по умолчанию, и многое другое
  9. Экосистема Scala предлагает самые современные ФП библиотеки в мире
  10. Сильная система типов

1) Слияние ФП/ООП

Больше, чем любой другой язык, Scala поддерживает слияние парадигм ФП и ООП. Как заявил Мартин Одерски, сущность Scala — это слияние функционального и объектно-ориентированного программирования в типизированной среде, где:

  • Функции для логики
  • Объекты для модульности

Возможно, одними из лучших примеров модульности являются классы стандартной библиотеки. Например, List определяется как класс—технически это абстрактный класс—и новый экземпляр создается следующим образом:

val x = List(1, 2, 3)

Однако то, что кажется программисту простым List, на самом деле построено из комбинации нескольких специализированных типов, включая трейты с именами Iterable, Seq и LinearSeq. Эти типы также состоят из других небольших модульных единиц кода.

В дополнение к построению типа наподобие List из серии модульных трейтов, List API также состоит из десятков других методов, многие из которых являются функциями высшего порядка:

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

xs.map(_ + 1)         // List(2, 3, 4, 5, 6)
xs.filter(_ < 3)      // List(1, 2)
xs.find(_ > 3)        // Some(4)
xs.takeWhile(_ < 3)   // List(1, 2)

В этих примерах значения в списке не могут быть изменены. Класс List неизменяем, поэтому все эти методы возвращают новые значения, как показано в каждом комментарии.

2) Ощущение динамики

Вывод типов (type inference) в Scala часто заставляет язык чувствовать себя динамически типизированным, даже если он статически типизирован. Это верно для объявления переменной:

val a = 1
val b = "Hello, world"
val c = List(1,2,3,4,5)
val stuff = ("fish", 42, 1_234.5)

Это также верно при передаче анонимных функций функциям высшего порядка:

list.filter(_ < 4)
list.map(_ * 2)
list.filter(_ < 4)
    .map(_ * 2)

и при определении методов:

def add(a: Int, b: Int) = a + b

Это как никогда верно для Scala 3, например, при использовании типов объединения:

// параметр типа объединения
def help(id: Username | Password) =
  val user = id match
    case Username(name) => lookupName(name)
    case Password(hash) => lookupPassword(hash)
  // дальнейший код ...

// значение типа объединения
val b: Password | Username = if (true) name else password

3) Лаконичный синтаксис

Scala — это неформальный, “краткий, но все же читабельный“ язык. Например, объявление переменной лаконично:

val a = 1
val b = "Hello, world"
val c = List(1,2,3)

Создание типов, таких как трейты, классы и перечисления, является кратким:

trait Tail:
  def wagTail(): Unit
  def stopTail(): Unit

enum Topping:
  case Cheese, Pepperoni, Sausage, Mushrooms, Onions

class Dog extends Animal, Tail, Legs, RubberyNose

case class Person(
  firstName: String,
  lastName: String,
  age: Int
)

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

list.filter(_ < 4)
list.map(_ * 2)

Все эти и многие другие выражения кратки и при этом очень удобочитаемы: то, что мы называем выразительным (expressive).

4) Implicits, упрощение

Implicits в Scala 2 были главной отличительной особенностью дизайна. Они представляли собой фундаментальный способ абстрагирования от контекста с единой парадигмой, обслуживающей множество вариантов использования, среди которых:

  • Реализация типовых классов
  • Установление контекста
  • Внедрение зависимости
  • Выражение возможностей

С тех пор другие языки внедрили аналогичные концепции, все из которых являются вариантами основной идеи вывода терминов: при заданном типе компилятор синтезирует “канонический” термин этого типа.

Хотя implicits были определяющей функцией в Scala 2, их дизайн был значительно улучшен в Scala 3:

  • Есть единственный способ определить значения “given”
  • Есть единственный способ ввести неявные параметры и аргументы
  • Есть отдельный способ импорта givens, который не позволяет им потеряться в море обычного импорта
  • Существует единственный способ определить неявное преобразование, которое четко обозначено как таковое и не требует специального синтаксиса

К преимуществам этих изменений относятся:

  • Новый дизайн позволяет избежать взаимодействия функциональностей и делает язык более согласованным
  • Это делает implicits более простыми для изучения и более сложными для злоупотребления
  • Это значительно улучшает ясность 95% программ Scala, использующих implicits
  • У него есть потенциал, чтобы сделать вывод терминов принципиальным способом, который также доступен и удобен

Эти возможности подробно расписаны в соответствующих разделах, таких как введение в контекстную абстракцию, а также раздел о given и предложениях using для получения более подробной информации.

5) Полная интеграция с Java

Взаимодействие между Scala и Java не вызывает затруднений во многих ситуациях. Например:

  • Вы можете использовать все тысячи библиотек Java, доступных в ваших проектах Scala
  • Scala String — это, по сути, Java String с дополнительными возможностями
  • Scala легко использует классы даты/времени из Java пакета java.time._

Вы также можете использовать классы коллекций Java в Scala, а для придания им большей функциональности Scala включает методы, позволяющие преобразовывать их в коллекции Scala.

Несмотря на то, что почти каждое взаимодействие является бесшовным, в главе “Взаимодействие с Java” показано, как лучше использовать некоторые функции вместе, в том числе как использовать:

  • Коллекции Java в Scala
  • Java Optional в Scala
  • Интерфейсы Java в Scala
  • Коллекции Scala в Java
  • Scala Option в Java
  • Scala traits в Java
  • Методы Scala, вызывающие исключения в Java коде
  • Scala varargs параметры в Java

Подробнее об этих функциях см. в этой главе.

6) Клиент & сервер

Scala можно использовать на стороне сервера с потрясающими фреймворками:

  • Play Framework позволяет создавать масштабируемые серверные приложения и микросервисы
  • Akka Actors позволяет использовать модель акторов для значительного упрощения распределенных и параллельных программных приложений

Scala также можно использовать в браузере с проектом Scala.js, который является безопасной заменой JavaScript. В экосистеме Scala.js есть десятки библиотек, позволяющих использовать React, Angular, jQuery и многие другие библиотеки JavaScript и Scala в браузере.

В дополнение к этим инструментам проект Scala Native “представляет собой оптимизирующий опережающий компилятор и облегченную управляемую среду выполнения, разработанную специально для Scala”. Он позволяет создавать бинарные исполняемые приложения в “системном” стиле с помощью простого кода Scala, а также позволяет использовать низкоуровневые примитивы.

7) Стандартные библиотечные методы

Вам довольно редко понадобится писать пользовательский цикл for, потому что десятки готовых функциональных методов в стандартной библиотеке Scala сэкономят ваше время и помогут сделать код более согласованным в разных приложениях.

В следующих примерах показаны некоторые из встроенных методов коллекций, а также многие другие. Хотя все они используют класс List, одни и те же методы работают с другими классами коллекций, такими как Seq, Vector, LazyList, Set, Map, Array и ArrayBuffer.

Вот некоторые примеры:

List.range(1, 3)                          // List(1, 2)
List.range(start = 1, end = 6, step = 2)  // List(1, 3, 5)
List.fill(3)("foo")                       // List(foo, foo, foo)
List.tabulate(3)(n => n * n)              // List(0, 1, 4)
List.tabulate(4)(n => n * n)              // List(0, 1, 4, 9)

val a = List(10, 20, 30, 40, 10)          // List(10, 20, 30, 40, 10)
a.distinct                                // List(10, 20, 30, 40)
a.drop(2)                                 // List(30, 40, 10)
a.dropRight(2)                            // List(10, 20, 30)
a.dropWhile(_ < 25)                       // List(30, 40, 10)
a.filter(_ < 25)                          // List(10, 20, 10)
a.filter(_ > 100)                         // List()
a.find(_ > 20)                            // Some(30)
a.head                                    // 10
a.headOption                              // Some(10)
a.init                                    // List(10, 20, 30, 40)
a.intersect(List(19,20,21))               // List(20)
a.last                                    // 10
a.lastOption                              // Some(10)
a.map(_ * 2)                              // List(20, 40, 60, 80, 20)
a.slice(2, 4)                             // List(30, 40)
a.tail                                    // List(20, 30, 40, 10)
a.take(3)                                 // List(10, 20, 30)
a.takeRight(2)                            // List(40, 10)
a.takeWhile(_ < 30)                       // List(10, 20)
a.filter(_ < 30).map(_ * 10)              // List(100, 200, 100)

val fruits = List("apple", "pear")
fruits.map(_.toUpperCase)                 // List(APPLE, PEAR)
fruits.flatMap(_.toUpperCase)             // List(A, P, P, L, E, P, E, A, R)

val nums = List(10, 5, 8, 1, 7)
nums.sorted                               // List(1, 5, 7, 8, 10)
nums.sortWith(_ < _)                      // List(1, 5, 7, 8, 10)
nums.sortWith(_ > _)                      // List(10, 8, 7, 5, 1)

8) Встроенные “best practices”

Идиомы Scala поощряют “best practices” во многих ситуациях. Для неизменяемости рекомендуется создавать неизменяемые val переменные:

val a = 1                 // неизменяемая переменная

Вам также рекомендуется использовать неизменяемые классы коллекций, такие как List и Map:

val b = List(1,2,3)       // List неизменяем
val c = Map(1 -> "one")   // Map неизменяема

Case классы в первую очередь предназначены для использования в моделировании предметной области, и их параметры также неизменяемы:

case class Person(name: String)
val p = Person("Michael Scott")
p.name           // Michael Scott
p.name = "Joe"   // compiler error (переназначение val name)

Как показано в предыдущем разделе, классы коллекций Scala поддерживают функции высшего порядка, и вы можете передавать в них методы (не показаны) и анонимные функции:

a.dropWhile(_ < 25)
a.filter(_ < 25)
a.takeWhile(_ < 30)
a.filter(_ < 30).map(_ * 10)
nums.sortWith(_ < _)
nums.sortWith(_ > _)

Выражения match позволяют использовать сопоставление с образцом, и они действительно являются выражениями, которые возвращают значения:

val numAsString = i match {
  case 1 | 3 | 5 | 7 | 9 => "odd"
  case 2 | 4 | 6 | 8 | 10 => "even"
  case _ => "too big"
}
val numAsString = i match
  case 1 | 3 | 5 | 7 | 9 => "odd"
  case 2 | 4 | 6 | 8 | 10 => "even"
  case _ => "too big"

Поскольку они могут возвращать значения, их часто используют в качестве тела метода:

def isTruthy(a: Matchable) = a match {
  case 0 | "" => false
  case _ => true
}
def isTruthy(a: Matchable) = a match
  case 0 | "" => false
  case _ => true

9) Библиотеки экосистемы

Библиотеки Scala для функционального программирования, такие как Cats и Zio, являются передовыми библиотеками в сообществе ФП. Об этих библиотеках можно сказать все модные словечки, такие как высокопроизводительная, типобезопасная, параллельная, асинхронная, ресурсобезопасная, тестируемая, функциональная, модульная, бинарно-совместимая, эффективная, эффектная и т.д.

Мы могли бы перечислить здесь сотни библиотек, но, к счастью, все они перечислены в другом месте: подробности см. в списке “Awesome Scala”.

10) Сильная система типов

В Scala есть сильная система типов, и она была еще больше улучшена в Scala 3. Цели Scala 3 были определены на раннем этапе, и к ним относятся:

  • Упрощение
  • Устранение несоответствий
  • Безопасность
  • Эргономика
  • Производительность

Упрощение достигается за счет десятков измененных и удаленных функций. Например, изменения перегруженного ключевого слова implicit в Scala 2 на термины given и using в Scala 3 делает язык более понятным, особенно для начинающих разработчиков.

Устранение несоответствий связано с десятками удаленных функций, измененных функций, и добавленных функций в Scala 3. Некоторые из наиболее важных функций в этой категории:

  • Типы пересечения
  • Типы объединения
  • Неявные функциональные типы
  • Зависимые функциональные типы
  • Параметры трейтов
  • Generic кортежи

Безопасность связана с несколькими новыми и измененными функциями:

  • Мультиверсальное равенство
  • Ограничение неявных преобразований
  • Null безопасность
  • Безопасная инициализация

Хорошими примерами эргономики являются перечисления и методы расширения, добавленные в Scala 3 довольно удобочитаемым образом:

// перечисления
enum Color:
  case Red, Green, Blue

// методы расширения
extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2
  def diameter: Double = c.radius * 2
  def area: Double = math.Pi * c.radius * c.radius

Производительность относится к нескольким областям. Одним из них являются непрозрачные типы. В Scala 2 было несколько попыток создать решения, соответствующие практике проектирования, управляемого предметной областью (DDD), когда значениям присваивались более осмысленные типы. Эти попытки включали:

  • Псевдонимы типов
  • Классы значений
  • Case классы

К сожалению, у всех этих подходов были недостатки, как описано в SIP непрозрачных типов. И наоборот, цель непрозрачных типов, как описано в этом SIP, заключается в том, что “операции с этими типами-оболочками не должны создавать дополнительных накладных расходов во время выполнения, но при этом обеспечивать безопасное использование типов во время компиляции”.

Дополнительные сведения о системе типов см. в справочной документации.

Другие замечательные функции

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

Contributors to this page: