image Привет, Хабр! Сдали в типографию новую книгу «Scala. Профессиональное программирование. 5-е изд.». «Scala. Профессиональное программирование» — главная книга по Scala, популярному языку для платформы Java, в котором сочетаются концепции объектно-ориентированного и функционального программирования, благодаря чему он превращается в уникальное и мощное средство разработки. Этот авторитетный труд, написанный создателями Scala, поможет вам пошагово изучить язык и идеи, лежащие в его основе. Пятое издание значительно обновлено, чтобы охватить многочисленные изменения, появившиеся в Scala 3.


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

Об авторах
Мартин Одерски, создатель языка Scala, — профессор в Федеральной политехнической школе Лозанны, Швейцария (EPFL), и основатель Lightbend, Inc. Работает над языками программирования и системами, в частности над темой совмещения объектно-ориентированного и функционального подходов. С 2001 года сосредоточен на проектировании, реализации и улучшении Scala. Внес вклад в разработку Java как соавтор обобщенных типов и создатель текущего эталонного компилятора javac. Мартину было присвоено звание действительного члена ACM.
Лекс Спун — разработчик программного обеспечения в компании Square Inc., создающей простое в использовании программное обеспечение для бизнеса и мобильных платежей. Занимался Scala на протяжении двух лет в ходе постдокторантуры в EPFL. Помимо Scala, участвовал в разработке самых разнообразных языков, включая динамический язык Smalltalk, научный язык X10 и логический язык CodeQL.
Билл Веннерс — президент Artima, Inc., занимающейся консалтингом, курсами, книгами и инструментами для работы со Scala. Автор книги Inside the Java Virtual Machine про архитектуру и внутреннее устройство платформы Java. Билл представляет сообщество в Scala Center и является ведущим разработчиком и проектировщиком фреймворка тестирования ScalaTest и библиотеки Scalactic, предназначенной для функционального и объектно-ориентированного программирования.
Фрэнк Соммерс — основатель и президент компании Autospaces Inc., предоставляющей решения для автоматизации рабочих процессов в сфере финансовых услуг. Фрэнк ежедневно работает с языком Scala уже свыше двенадцати лет.

Гивены


Поведение функции зачастую зависит от контекста, в котором она вызывается. Например, она может менять свое поведение в зависимости от контекстных данных, таких как системные свойства, разрешения безопасности, аутентифицированный пользователь, транзакция базы данных или заданное время ожидания. Функция также может зависеть от контекстного поведения — алгоритма, имеющего смысл в контексте, в котором эта функция вызывается. Например, функция сортировки может зависеть от алгоритма сравнения, который определяет, как упорядочивать сортируемые элементы. Разные контексты могут требовать разных алгоритмов сравнения.

Для предоставления функции такой контекстной информации и поведения существует множество приемов, однако в функциональном программировании решение традиционно сводится к одному: передавать все в качестве параметров. И хотя это вполне рабочий подход, у него есть недостаток: чем больше вы передаете функции данных и алгоритмов, тем более общей и полезной она становится, но при этом увеличивается количество аргументов, которые нужно указывать при каждом ее вызове. К сожалению, передача всего в виде параметров может быстро сделать ваш код повторяющимся и шаблонным.

В этой главе описываются контекстные параметры, которые часто называют гивенами (given). Они позволяют вам опускать некоторые аргументы при вызове функций, давая возможность компилятору подставить подходящие значения для каждого контекста в зависимости от типа.

Как это работает


Компилятор иногда меняет someCall(a) на someCall(a)(b) или SomeClass(a) на new SomeClass(a)(b), добавляя тем самым один или несколько недостающих списков параметров, чтобы сделать вызов функции завершенным. Предоставляются не отдельные параметры, а целые их каррированные списки. Например, если недостающий список параметров someCall состоит из трех значений, компилятор может подставить someCall(a)(b, c, d) вместо someCall(a). В этом случае подставленные идентификаторы, такие как b, c и d в (b, c, d), должны быть помечены как заданные (given) в месте их определения, а сам список параметров в определении someCall или someClass должен начинаться с using.

Представьте, к примеру, что у вас есть множество методов, принимающих приглашение командной строки (например, "$ " или "> "), которое предпочитает текущий пользователь. Вы можете сократить количество шаблонного кода, сделав запрос контекстным параметром. Для начала нужно создать специальный тип, инкапсулирующий строку с предпочитаемым приглашением:

class PreferredPrompt(val preference: String)

Далее нужно отредактировать каждый метод, который принимает приглашение, заменив параметр отдельным списком параметров с ключевым словом using. Например, у следующего объекта Greeter есть метод greet, который принимает PreferredPrompt в качестве контекстного параметра:

object Greeter:
   def greet(name: String)(using prompt: PreferredPrompt) =
      println(s"Welcome, $name. The system is ready.")
      println(prompt.preference)

Чтобы компилятор мог неявно подставлять контекстный параметр, вы должны определить given-экземпляр ожидаемого типа (в данном случае PreferredPrompt) с использованием ключевого слова given. Это можно сделать в объекте настроек, как показано далее:

object JillsPrefs:
   given prompt: PreferredPrompt =
      PreferredPrompt("Your wish> ")

Теперь компилятор может автоматически подставлять этот экземпляр PreferredPrompt, но только при условии, что тот находится в области видимости:

scala> Greeter.greet("Jill")
1 |Greeter.greet("Jill")
   | ˆ
   |no implicit argument of type PreferredPrompt was found
   |for parameter prompt of method greet in object Greeter

Если сделать этот объект доступным, он будет использоваться для предоставления недостающего списка параметров:

scala> import JillsPrefs.prompt
scala> Greeter.greet("Jill")
Welcome, Jill. The system is ready.
Your wish>

Поскольку приглашение командной строки объявлено в качестве контекстного параметра, оно не скомпилируется, если вы попытаетесь передать аргумент как обычно, явным образом:

scala> Greeter.greet("Jill")(JillsPrefs.prompt)
1 |Greeter.greet("Jill")(JillsPrefs.prompt)
   |ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
   |method greet in object Greeter does not take more
   |parameters

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

scala> Greeter.greet("Jill")(using JillsPrefs.prompt)
Welcome, Jill. The system is ready.
Your wish>

Обратите внимание на то, что ключевое слово using относится не к отдельным параметрам, а ко всему списку. В листинге 21.1 показан пример, в котором второй список параметров метода greet из объекта Greeter (который опять же помечен как using) состоит из двух элементов:
prompt (типа PreferredPrompt) и drink (типа PreferredDrink).

Листинг 21.1. Неявный список с несколькими параметрами

class PreferredPrompt(val preference: String)
class PreferredDrink(val preference: String)
object Greeter:
   def greet(name: String)(using prompt: PreferredPrompt,
          drink: PreferredDrink) =
      println(s"Welcome, $name. The system is ready.")
      print("But while you work, ")
      println(s"why not enjoy a cup of ${drink.preference}?")
      println(prompt.preference)
object JoesPrefs:
   given prompt: PreferredPrompt =
      PreferredPrompt("relax> ")
  given drink: PreferredDrink =
      PreferredDrink("tea")

Объект-одиночка объявляет два given-экземпляра: prompt типа PreferredPrompt и drink типа PreferredDrink. Но, как и прежде, они не будут использоваться для подстановки недостающего списка параметров в greet, если они находятся вне области видимости:

scala> Greeter.greet("Joe")
1 |Greeter.greet("Joe")
   | ˆ
   |no implicit argument of type PreferredPrompt was found
   |for parameter prompt of method greet in object Greeter

Вы можете сделать оба given-экземпляра из листинга 21.1 доступными с помощью инструкции import:

scala> import JoesPrefs.{prompt, drink}

Поскольку и prompt, и drink теперь находятся в области видимости в качестве отдельных идентификаторов, вы можете использовать их для явного предоставления последнего списка параметров:

scala> Greeter.greet("Joe")(using prompt, drink)
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
relax>

И поскольку ваши контекстные параметры теперь удовлетворяют всем правилам, вы можете также позволить компилятору Scala подставить prompt и drink автоматически, целиком опустив весь список параметров:

scala> Greeter.greet("Joe")
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
relax>

Одной из особенностей предыдущих примеров является то, что мы не использовали String в качестве типа для prompt или drink, хотя в итоге оба этих значения предоставили именно String через свои поля preference. Поскольку компилятор выбирает контекстные параметры путем сопоставления типов параметров и типов given-экземпляров, контекстные параметры должны иметь достаточно редкие, или особенные типы, которые делают случайное совпадение маловероятным. Например, типы PreferredPrompt и PreferredDrink в листинге 21.1 были определены исключительно для контекстных параметров. В результате given-экземпляры этих типов, скорее всего, не будут существовать, если только они не предназначены для использования в качестве контекстных параметров для таких методов, как greet.

Параметризованные given-типы


Контекстные параметры, наверное, чаще всего используются для предоставления информации о типе, явно указанном в предыдущем списке параметров, подобно классам типов (type class) в Haskell. Это важный механизм достижения специального полиморфизма (ad hoc polymorphism) при написании функций в Scala: ваши функции можно применять к значениям с подходящими типами, но при использовании для значений любых других типов код не скомпилируется. Представьте, к примеру, двухстрочную сортировку вставками, показанную в листинге 14.1. Это определение isort работает только для списка целых чисел. Чтобы сортировать списки других типов, вам нужно сделать тип аргумента isort более общим. Для этого первым делом можно ввести параметр типа, T, и подставить его вместо Int в параметре типа List:

// Не компилируется
def isort[T](xs: List[T]): List[T] =
   if xs.isEmpty then Nil
   else insert(xs.head, isort(xs.tail))
def insert[T](x: T, xs: List[T]): List[T] =
   if xs.isEmpty || x <= xs.head then x :: xs
   else xs.head :: insert(x, xs.tail)

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

6 | if xs.isEmpty || x <= xs.head then x :: xs
   | ˆˆˆˆ
   | value <= is not a member of T, ...

Если класс Int определяет метод <=, устанавливающий, является ли одно целое число меньше или равно другому, то для других типов могут потребоваться альтернативные стратегии сравнения или же их и вовсе нельзя сравнивать. Чтобы метод isort мог работать со списками, элементы которых имеют типы, отличные от Int, ему нужно предоставить чуть больше информации, позволяющей определить способ сравнения двух элементов.

Чтобы решить эту проблему, методу isort можно передать функцию «меньше или равно», подходящую для типа List. Эта функция должна принимать два экземпляра T и возвращать значение Boolean, указывающее на то, является ли первый экземпляр T меньше или равным второму:

def isort[T](xs: List[T])(lteq: (T, T) => Boolean): List[T] =
   if xs.isEmpty then Nil
   else insert(xs.head, isort(xs.tail)(lteq))(lteq)
def insert[T](x: T, xs: List[T])
      (lteq: (T, T) => Boolean): List[T] =
   if xs.isEmpty || lteq(x, xs.head) then x :: xs
   else xs.head :: insert(x, xs.tail)(lteq)

Теперь вместо <= вспомогательная функция insert использует параметр lteq для сравнения двух элементов во время сортировки. Это позволяет сортировать список любого типа T, главное — предоставить методу isort функцию сравнения, которая подходит для T. Например, с помощью этой версии isort можно сортировать списки Int, String и класса Rational, представленного в листинге 6.5:

isort(List(4, -10, 10))((x: Int, y: Int) => x <= y)
// List(-10, 4, 10)
isort(List("cherry", "blackberry", "apple", "pear"))
   ((x: String, y: String) => x.compareTo(y) <= 0)
// List(apple, blackberry, cherry, pear)
isort(List(Rational(7, 8), Rational(5, 6), Rational(1, 2)))
   ((x: Rational, y: Rational) =>
      x.numer * y.denom <= x.denom * y.numer)
// List(1/2, 5/6, 7/8)

Как уже описывалось в разделе 14.10, компилятор Scala последовательно определяет типы параметров в каждом списке, продвигаясь слева направо. Таким образом, он может определить типы x и y, указанные во втором списке параметров, исходя из типа элемента T экземпляра List[T], переданного в первом списке параметров:

isort(List(4, -10, 10))((x, y) => x <= y)
// List(-10, 4, 10)
isort(List("cherry", "blackberry", "apple", "pear"))
   ((x, y) => x.compareTo(y) < 1)
// List(apple, blackberry, cherry, pear)
isort(List(Rational(7, 8), Rational(5, 6), Rational(1, 2)))
   ((x, y) => x.numer * y.denom <= x.denom * y.numer)
// List(1/2, 5/6, 7/8)

Теперь функция isort полезна в более общем смысле, однако за эту обобщенность приходится платить потерей лаконичности: при каждом вызове необходимо указывать функцию сравнения, которую определение isort теперь должно передавать каждому рекурсивному вызову isort, а также каждому вызову вспомогательной функции insert. Эта версия isort больше не является простым выражением сортировки, как прежде.

Вы можете сделать более лаконичной как реализацию метода isort, так и его вызовы, если оформите функцию сравнения в виде контекстного параметра. Вы могли бы использовать контекстный параметр (Int, Int) => Boolean, но этот тип слишком общий, что делает его не самым оптимальным решением. У вашей программы, к примеру, может быть много функций, которые принимают целочисленные параметры и возвращают логическое значение, но при этом не имеют ничего общего с сортировкой. Поскольку поиск given-значений происходит по типу, вы должны позаботиться о том, чтобы тип вашего given-экземпляра выражал его назначение.

Определение типов с определенным назначением, таким как сортировка, обычно является хорошим решением, но, как упоминалось ранее, некоторые типы становятся особенно полезными при использовании контекстных параметров. Помимо гарантии использования подходящего given-экземпляра, тщательно определенные типы могут помочь вам более ясно выразить ваши намерения. Это позволяет вам развивать ваши программы постепенно, расширяя типы за счет дополнительного функционала, но не нарушая при этом существующие между ними контракты. Вы можете определить тип, чтобы выбрать, в каком порядке должны размещаться два элемента:

trait Ord[T]:
   def compare(x: T, y: T): Int
   def lteq(x: T, y: T): Boolean = compare(x, y) < 1

Этот трейт реализует функцию «меньше или равно» в виде более общего абстрактного метода compare. Контракт этого метода состоит в том, что он возвращает 0, если два параметра равны, положительное целое число, если первый параметр больше второго, и отрицательное целое число, если второй параметр больше первого. Теперь, имея это определение, вы можете указать стратегию сравнения для T, используя Ord[T] в качестве контекстного параметра, как показано в листинге 21.2.

Листинг 21.2. Контекстные параметры, передаваемые с помощью using

def isort[T](xs: List[T])(using ord: Ord[T]): List[T] =
   if xs.isEmpty then Nil
   else insert(xs.head, isort(xs.tail))
def insert[T](x: T, xs: List[T])
      (using ord: Ord[T]): List[T] =
   if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs
   else xs.head :: insert(x, xs.tail)

Как уже описывалось ранее, чтобы параметры можно было передавать неявно, перед ними нужно указать using. После этого вам больше не нужно предоставлять эти параметры вручную при вызове функции: если доступно значение подходящего типа, компилятор возьмет его и передаст вашей функции. Чтобы сделать значение given-экземпляром типа, его следует объявить с помощью ключевого слова given.

Хорошим местом для размещения given-экземпляров, представляющих «естественный» вариант использования типа, такой как сортировка целых чисел в порядке возрастания, является объект-компаньон «вовлеченного» типа. Например, естественный given-экземпляр Ord[Int] можно было бы разместить в объекте-компаньоне для Ord или Int — двух типов, «фигурирующих» в Ord[Int]. Если компилятор не найдет given-экземпляр Ord[Int] в лексической области видимости, он проведет дополнительный поиск в этих двух объектах-компаньонах. Поскольку компаньон Int не подлежит изменению, лучшим выбором является компаньон Ord:
object Ord:

// (Пока что не является устоявшимся решением)
   given intOrd: Ord[Int] =
   new Ord[Int]:
      def compare(x: Int, y: Int) =
         if x == y then 0 else if x > y then 1 else 1

Все примеры given-объявлений, показанные до сих пор в этой главе, называются псевдонимными (alias). Имя по левую сторону от знака равенства является псевдонимом значения, указанного справа. Поскольку при объявлении псевдонимного given-экземпляра справа от знака равенства зачастую определяют анонимный экземпляр трейта или класса, Scala предлагает сокращенный синтаксис, который позволяет подставить вместо знака равенства и «имени нового класса» ключевое слово with. В листинге 21.3 показано более компактное определение intOrd.

Листинг 21.3. Объявление естественного given-экземпляра в компаньоне

object Ord:
   // Общепринятое решение
   given intOrd: Ord[Int] with
      def compare(x: Int, y: Int) =
         if x == y then 0 else if x > y then 1 else 1

Теперь, когда в объекте Ord имеется given-экземпляр Ord[Int], сортировка с использованием isort снова становится лаконичной:

isort(List(10, 2, -10))
// List(-10, 2, 10)

Если опустить второй параметр isort, компилятор начнет искать для него заданное значение с учетом его типа. Если речь идет о сортировке значений Int, этим типом будет Ord[Int]. Вначале компилятор поищет given-экземпляр Ord[Int] в лексической области видимости, и, если его там не обнаружится, он пройдется по объектам-компаньонам вовлеченных типов Ord и Int. Поскольку в листинге 21.3 заданное значение intOrd имеет явно указанный тип, компилятор подставит intOrd вместо недостающего списка параметров.

Для сортировки строк достаточно предоставить given-экземпляр для параметра, предназначенного для сравнения строковых значений:

// Добавлено в объект Ord
given stringOrd: Ord[String] with
   def compare(s: String, t: String) = s.compareTo(t)

Теперь, когда в компаньоне Ord определен given-экземпляр Ord[String], вы можете использовать isort для сортировки списков строк:

isort(List("mango", "jackfruit", "durian"))
// List(durian, jackfruit, mango)

Если заданное объявление не принимает параметризованные значения, given-экземпляр инициализируется при первом к нему обращении, что похоже на ленивые значения. Эта инициализация проводится потокобезопасным образом. Если же given-экземпляр принимает параметры, он создается заново при каждом обращении, подобно тому как ведет себя def. Действительно, компилятор Scala преобразует given-экземпляры в val или def, дополнительно делая их доступными для параметров using.

Анонимные given-экземпляры


Заданное объявление можно считать частным случаем ленивого val или def, однако оно обладает одной важной особенностью. При объявлении val, к примеру, нужно задать выражение, указывающее на значение val:

val age = 42

В этом выражении компилятор должен определить тип age. Поскольку для инициализации age используется значение 42, которое, как известно компилятору, имеет тип Int, для age будет выбран тот же тип. Вы фактически предоставляете выражение, age, а компилятор определяет его тип, Int.

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

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

given revIntOrd: Ord[Int] with
   def compare(x: Int, y: Int) =
      if x == y then 0 else if x > y then -1 else 1
given revStringOrd: Ord[String] with
   def compare(s: String, t: String) = -s.compareTo(t)

можно написать

given Ord[Int] with
   def compare(x: Int, y: Int) =
      if x == y then 0 else if x > y then -1 else 1
given Ord[String] with
   def compare(s: String, t: String) = -s.compareTo(t)

Компилятор автоматически синтезирует имена для этих анонимных given-экземпляров. Вместо второго параметра функции isort будет подставлено это синтезированное значение, которое затем станет доступным внутри функции. Таким образом, если вам нужно, чтобы given-экземпляр был неявно предоставлен в качестве контекстных параметров, вам не нужно объявлять для него выражение.

Оформить предзаказ бумажной книги можно на нашем сайте

Комментарии (2)


  1. NeoCode
    23.06.2022 09:58

    Идея весьма интересная... нечто среднее между явной передачей аргументов и аргументами по умолчанию.

    А в чем смысл и польза каррированных списов аргументов? (когда список аргументов не один, а несколько)?


  1. leon_nikitin
    25.06.2022 14:57

    Может, Гудвины, а не Гивены, все же?