Scala в примерах: различия между версиями

Содержимое удалено Содержимое добавлено
Добавлен перевод параграфа 8.2
Правка перевода и форматирования Главы 15.
Строка 948:
Неявные(имплицитные) параметры и преобразования – это мощные инструменты для настройки существующих библиотек и для создания высокоуровневых абстракций. Например, давайте начнем с абстрактного класса полугрупп, который поддерживает операцию сложения.
 
<font size=3><syntaxhighlight lang=Scala>
<code>
'''abstract''' class SemiGroup[A] {
'''def''' add(x: A, y: A): A }</code>
}
А это подкласс SemiGroup – класс Monoid, который добавляет поле unit.
</syntaxhighlight></font>
 
А вот подкласс <tt>SemiGroup</tt> — класс <tt>Monoid</tt>, который добавляет поле <tt>unit</tt> (единичный элемент).
 
<font size=3><syntaxhighlight lang=Scala>
abstract class Monoid[A] extends SemiGroup[A] {
def unit: A
}
</syntaxhighlight></font>
 
<code>
'''abstract''' '''class''' Monoid[A] '''extends''' SemiGroup[A] {
'''def''' unit: A
}</code>
Вот две реализации моноидов:
 
<font size=3><syntaxhighlight lang=Scala>
<code>
'''object''' stringMonoid '''extends''' Monoid[String] {
'''def''' add(x: String, y: String): String = x.concat(y)
'''def''' unit: String = ""
}
'''object''' intMonoid '''extends''' Monoid[Int] {
'''def''' add(x: Int, y: Int): Int = x + y
'''def''' unit: Int = 0
}</code>
 
object intMonoid extends Monoid[Int] {
Метод суммирования, который работает с произвольными моноидами, может быть написан на чистой Scala следующим образом:
def add(x: Int, y: Int): Int = x + y
def unit: Int = 0
}
</syntaxhighlight></font>
 
Метод суммирования, который работает с произвольными моноидами, может быть написан на чистом Scala следующим образом:
 
<font size=3><syntaxhighlight lang=Scala>
def sum[A](xs: List[A])(m: Monoid[A]): A =
if (xs.isEmpty) m.unit
else m.add(xs.head, sum(m)(xs.tail)
</syntaxhighlight></font>
 
<code>
'''def''' sum[A](xs: List[A])(m: Monoid[A]): A =
'''if''' (xs.isEmpty) m.unit
'''else''' m.add(xs.head, sum(m)(xs.tail)
</code>
Этот метод может быть вызван, например, так:
 
<font size=3><syntaxhighlight lang=Scala>
<code>
sum(List("a", "bc", "def"))(stringMonoid)
sum(List(1, 2, 3))(intMonoid)</code>
</syntaxhighlight></font>
Все это работает, но выглядит не очень здорово. Проблема в том, что реализации моноида должны быть переданы в код, который их использует. Иногда мы можем захотеть, чтобы система могла вычислить нужные аргументы автоматически, аналогично тому, как это делается при выводе типов аргументов. Это то, чего помогают добиться неявные параметры.
 
Все это работает, но выглядит не очень здорово. Проблема в том, что реализации моноида должны быть переданы в код, который их использует. Порой мы можем захотеть, чтобы система могла вычислить нужные аргументы автоматически, аналогично тому, как это делается при выводе типов аргументов. Это то, чего помогают добиться неявные параметры.
 
=== Неявные параметры: основы ===
 
В Scala 2 появилось новое ключевое слово '''<tt>implicit;</tt>''', оно может быть использовано в начале списка параметров. Синтаксис:
 
<code>
<font size=3><syntaxhighlight lang=Scala>
ParamClauses ::= {‘(’ [Param {‘,’ Param}] ’)’}
[ParamClauses ::= {‘(’ implicit [Param {‘,’ Param}] )’]</code>}
[‘(’ implicit Param {‘,’ Param} ‘)’]</code>
</syntaxhighlight></font>
 
Если ключевое слово присутствует, то оно делает все параметры в списке неявными. Например, в следующей версии метода sum параметр m является неявным.
 
<font size=3><syntaxhighlight lang=Scala>
<code>
'''def''' sum[A](xs: List[A])('''implicit''' m: Monoid[A]): A =
'''if''' (xs.isEmpty) m.unit
'''else''' m.add(xs.head, sum(xs.tail))</code>
</syntaxhighlight></font>
 
Как можно заметить из примера, возможно комбинировать обычные и неявные параметры. Однако, для метода или конструктора может быть только один неявный параметр, и он должен быть последним.
 
Ключевое слово '''<tt>implicit</tt>''' может также быть использовано как модификатор в определениях и объявлениях. Примеры:
 
<font size=3><syntaxhighlight lang=Scala>
<code>
'''implicit''' object stringMonoid '''extends''' Monoid[String] {
'''def''' add(x: String, y: String): String = x.concat(y)
'''def''' unit: String = ""
}
'''implicit''' '''object''' intMonoid '''extends''' Monoid[Int] {
'''def''' add(x: Int, y: Int): Int = x + y
'''def''' unit: Int = 0
}</code>
Основная идея неявных параметров – это то, что аргументы для них могут быть выведены из вызова метода. Если аргументы, отвечающие за секцию неявного параметра отсутствуют, тогда они выводятся Scala компилятором.
Реальные аргументы, которые могут быть переданы в качестве неявного параметра, – это все идентификаторы X, которые доступны в точке вызова метода без префикса и которые помечены как неявные.
Если существует несколько аргументов, подходящих по типу к неявному параметру, компилятор Scala выберет наиболее точный (специфичный), используя стандартные правила разрешения статической перегрузки. Например, пусть вызывается
 
implicit object intMonoid extends Monoid[Int] {
<code>
def add(x: Int, y: Int): Int = x + y
sum(List(1, 2, 3))</code>
def unit: Int = 0
в контексте, где доступны stringMonoid и intMonoid. Мы знаем, что формальный параметр метода sum должен быть типа int. Единственное значение, подходящее к типу формального параметра Monoid[int], это intMonoid, поэтому этот объект будет передан как неявный параметр.
}
Это обсуждение также показывает, что неявный параметр выводится после того, как выведен любой тип аргументов (?).
</syntaxhighlight></font>
 
Основная идея неявных параметров — это то, что аргументы для них могут быть выведены из вызова метода. Если аргументы, отвечающие за секцию неявного параметра отсутствуют, тогда они выводятся компилятором Scala.
 
Действительные аргументы, которые могут быть переданы в качестве неявного параметра, — это все идентификаторы <tt>X</tt>, которые доступны в точке вызова метода без префикса и которые помечены как неявные.
 
Если существует несколько аргументов, подходящих по типу к неявному параметру, компилятор Scala выберет наиболее точный (специфичный), используя стандартные правила разрешения статической перегрузки. Допустим, вызывается
 
<font size=3><syntaxhighlight lang=Scala>
sum(List(1, 2, 3))</code>
</syntaxhighlight></font>
 
в контексте, где доступны <tt>stringMonoid</tt> и <tt>intMonoid</tt>. Мы знаем, что формальный параметр метода <tt>sum</tt> должен быть типа <tt>Int</tt>. Единственное значение, подходящее к типу формального параметра <tt>Monoid[Int]</tt>, это <tt>intMonoid</tt>, поэтому этот объект и будет передан как неявный параметр.
 
Это обсуждение также показывает, что неявный параметр выводится после того, как выведен какой-нибудь типовой аргумент.
 
=== Неявные преобразования ===
 
Пусть у вас есть выражение ''E'' типа ''T'', хотя ожидается тип ''S''. ''T'' не приводится к ''S'' ни одним из предопределенных преобразований. Тогда компилятор Scala попытается применить последнее средство: неявное преобразование ''I(E)''. Здесь, ''I'' это идентификатор, выражающий неявное определение или параметр, доступный без префикса в точке преобразования, применимый к аргументам типа ''T'' и чей результирующий тип совместим с типом ''S''.
Неявные преобразования также могут быть применены в селекторах членов. Получив E.x, где x – это не член типа E, компилятор Scala постарается добавить неявное преобразование I(E).x так, чтобы x был членом I(E). Это пример неявного преобразования функции, которая преобразует целые числа в экземпляры класса scala.Ordered:
 
Неявные преобразования также могут быть применены в селекторах членов. Получив ''E.x'', где ''x'' — не член типа ''E'', компилятор Scala постарается добавить неявное преобразование ''I(E).x'' так, чтобы ''x'' был членом ''I(E)''.
<code>
 
'''implicit''' '''def''' int2ordered(x: Int): Ordered[Int] = '''new''' Ordered[Int] {
Вот пример неявного преобразования функции, которая преобразует целые числа в экземпляры класса <tt>scala.Ordered</tt>:
'''def''' compare(y: Int): Int =
 
'''if''' (x < y1) 1
<font size=3><syntaxhighlight lang=Scala>
'''else''' if (x > y1) 1
implicit def int2ordered(x: Int): Ordered[Int] = new Ordered[Int] {
'''else''' 0
def compare(y: Int): Int =
}</code>
if (x < y) -1
else if (x > y) 1
else 0
}
</syntaxhighlight></font>
 
=== View Bounds ===
 
View bounds (ограничения по видимости) — это удобный синтаксический сахар для неявных параметров. Рассмотрим обобщенный метод сортировки:
 
<font size=3><syntaxhighlight lang=Scala>
<code>
'''def''' sort[A <% Ordered[A]](xs: List[A]): List[A] =
'''if''' (xs.isEmpty || xs.tail.isEmpty) xs
'''else''' {
'''val''' {ys, zs} = xs.splitAt(xs.length / 2)
merge(ys, zs)
}
}</code>
</syntaxhighlight></font>
View bounded параметр типа [A <% Ordered[A]] выражает, что сортировка применима для списков таких типов, для которых существует неявное преобразование A к Ordered[A]. Определение трактуется как укороченный синтаксис для следующей сигнатуры метода с неявным параметром:
 
Ограниченный по видимости параметр типа <tt>[A <% Ordered[A]]</tt> выражает, что сортировка применима для списков таких типов, для которых существует неявное преобразование <tt>A</tt> к <tt>Ordered[A]</tt>. Определение трактуется как укороченный синтаксис для следующей сигнатуры метода с неявным параметром:
<code>
'''def''' sort[A](xs: List[A])('''implicit''' c: A => Ordered[A]): List[A] = …</code>
 
<font size=3><syntaxhighlight lang=Scala>
(Здесь имя параметра c выбрано произвольно с учетом того, чтобы оно не конфликтовало с другими именами в программе.)
def sort[A](xs: List[A])(implicit c: A => Ordered[A]): List[A] = ...
В качестве более детального примера, рассмотрим метод merge, который появился в методе sort выше:
</syntaxhighlight></font>
 
(Здесь имя параметра ''c'' выбрано произвольно с учетом того, чтобы оно не конфликтовало с другими именами в программе.)
<code>
 
'''def''' merge[A <% Ordered[A]](xs: List[A], ys: List[A]): List[A] =
В качестве более детального примера, рассмотрим метод <tt>merge</tt>, который идет вместе с методом <tt>sort</tt>:
'''if''' (xs.isEmpty) ys
 
'''else''' '''if''' (ys.isEmpty) xs
<font size=3><syntaxhighlight lang=Scala>
'''else''' '''if''' (xs.head < ys.head) xs.head :: merge(xs.tail, ys)
def merge[A <% Ordered[A]](xs: List[A], ys: List[A]): List[A] =
'''else''' '''if''' ys.head :: merge(xs, ys.tail)</code>
if (xs.isEmpty) ys
После раскрытия view bounds и вставки неявных преобразований, реализация метода принимает вид:
else if (ys.isEmpty) xs
<code>
else if (xs.head < ys.head) xs.head :: merge(xs.tail, ys)
def merge[A](xs: List[A], ys: List[A])
else if ys.head :: merge(xs, ys.tail)
</syntaxhighlight></font>
 
После раскрытия ограничений по видимости и вставки неявных преобразований реализация метода принимает вид:
 
<font size=3><syntaxhighlight lang=Scala>
def merge[A](xs: List[A], ys: List[A])
(implicit c: A => Ordered[A]): List[A] =
'''if''' (xs.isEmpty) ys
'''else''' '''if''' (ys.isEmpty) xs
'''else''' '''if''' (c(xs.head) < ys.head) xs.head :: merge(xs.tail, ys)
'''else''' '''if''' ys.head :: merge(xs, ys.tail)(c)</code>
</syntaxhighlight></font>
Последние две строки определения этого метода демонстрируют два различных использования неявного параметра c. Он применяется в преобразовании в условии во второй строке с конца и передается как неявный аргумент в рекурсивный вызов merge в последней строке.
 
Последние две строки определения этого метода демонстрируют два различных использования неявного параметра ''c''. Он применяется в преобразовании в условии во второй строке с конца и передается как неявный аргумент в рекурсивный вызов <tt>merge</tt> в последней строке.
 
= Вывод типов Хиндли-Милнера =