Что такое вообще context bound’ы и их предшественники - view bound’ы? Немного предыстории type class’ов.

И тот и другой были попыткой достичь в той или иной степени эффекта type class’ов, которые уже существовали в Haskell. Сперва появились в Scala появились т.н. view (см. спецификацию Scala, раздел 7.3) - т.е. implicit’ные преобразования (conversion). На базе implicit’ных параметров и методов можно построить implicit’ные преобразования, или views. Такое преобразование из типа S в тип T определяется implicit’ным же значением, имеющим тип функции вида S=>T или (=>S)=>T.

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

Как известно, функция неявного преобразования, или implicit conversion, является функция с одним параметром и ключевым словом implicit, автоматически преобразующая значения одного типа в значения другого типа. Например, мы хотим сконвертировать целые значения n в дробные значения n / 1. В таком случае преобразование будет выглядеть вот так:

implicit def int2Fraction(n: Int) = Fraction(n, 1) 

И срабатывает вот так:

val result = 3 * Fraction(4, 5) // Неявно вызывает int2Fraction(3) 

Т.е. целое число 3 превращается в объект Fraction, который затем умножается на Fraction(4, 5).

Теперь немного отвлечемся на ограничения типов - существуют ситуации, когда нам нужно наложить ограничения на параметрические типы. Например, пусть есть тип Pair, в котором оба значения имеют одинаковые типы:

class Pair[T](val first: T, val second: T) 

В этом типе присутствует метод smaller, который возвращает меньшее из значений:

class Pair[T](val first: T, val second: T) {
  def smaller = if (first.compareTo(second) < 0) first else second
// Error
}

Что, естественно, не будет работать — поскольку о типе T нам неизвестно ничего, в том числе и то, что он имеет метод compareTo. Для этого нам нужно добавить upper bound, или верхнюю границу типа T <: Comparable[T], с помощью которой мы декларируем, что T должен быть подтипом Comparable[T].

class Pair[T <: Comparable[T]](val first: T, val second: T) {
  def smaller = if (first.compareTo(second) < 0) first else second
}

Но этот пример тоже несколько упрощенный и не без недостатков. Если мы попробуем воспользоваться Pair(4, 2), то компилятор скажет, что T = Int, и ограничение T <: Comparable[T] не выполнено - Int из стандартной библиотеки Scala не является подтипом Comparable[Int], в отличие джавовского java.lang.Integer. И существует тип-обертка RichInt, который реализует Comparable[Int], вместе с соответствующим implicit преобразованием из Int в RichInt. Для того, чтобы мы могли задекларировать, что тип T может при необходимости быть неявно сконвертирован, используется т.н. view bound:

class Pair[T <% Comparable[T]] 

Оператор <% как раз и означает, что T может быть сконвертирован в Comparable[T] при наличии соответствующего implicit’ного преобразования. View bound и был введен в Scala для того чтобы использовать некоторый тип A там, где требуется некоторый тип B. Обобщенно типичный синтаксис view bound’ов можно изобразить как:

def f[A <% B](a: A) = a.bMethod

Другими словами, должны быть доступны implicit’ные преобразования A в B, для того чтобы мы могли вызывать методы B у объекта типа A. До Scala 2.8.0 view bound’ы использовались довольно активно, после того как они стали deprecated, их можно найти буквально в нескольких местах в библиотеке.

Но с view bound’ами есть определенные проблемы в смысле гибкости - например, также как и в случае обертки-декоратора, мы теряем информацию об исходном типе, не говоря уже о том, что часто нам нужно создать новый объект. Поэтому следующим шагом на пути реализации type классов стали context bounds - которые появились в Scala 2.8.0.

Context Bound

В то время как view bound’ы можно использовать с простыми, непараметрическими типами (например, A <% String), context bound работает только с параметрическими типами, такими как Ordered[A].

Context bound базируется на implicit’ном значении - вместо implicit’ного преобразования, как в случае view bound. Параметр показывает, что для некоторого типа A, существует implicit’ное значение (implicit value) типа B[A]. Синтаксис метода с context bound’ами выглядит приблизительно так:

def f[A : B](a: A) = g(a) // g требует implicit'ного значения типа B[A]

View bound вида T <% V требует существования implicit’ного преобразования conversion из T в V. Сontext bound же имеет вид T : M, где M - другой параметрический тип, и требует наличия implicit-ного значения типа T[M]. Например, class Pair[T : Ordering] требует implicit-ного значения Ordering[T], которое затем может быть использовано внутри метода; тот факт, что нам нужно implicit’ное значение, объявляется с помощью implicit’ного параметра:

class Pair[T: Ordering](val first: T, val second: T) {
  def smaller(implicit ord: Ordering[T]) = if (ord.compare(first, second) < 0) first else second
}

Другой пример. Для того, чтобы в Scala, начиная с версии 2.8, создать значение типа Array[T], нам понадобится передать Manifest[T] в качестве imlicit’ного параметра. Это необходимо из-за особенностей массивов в JVM (type erasure). Например, если T - это Int, то мы хотели бы в конце концов получить массив типа int[].

def makePair[T](first: T, second: T)(implicit evidence: Manifest[T]): Array[T] = {
  val r = new Array[T](2); r(0) = first; r(1) = second; r
}

Или, если мы воспользуемся context bound’ом, то можно записать этот как

def makePair[T: Manifest](first: T, second: T) = {
  val r = new Array[T](2); r(0) = first; r(1) = second; r
}

Для вызова makePair(4, 9), компилятор должен найти implicit’ный объект Manifest[Int], т.е. полный вызов выглядит как makePair(4, 9)(intManifest).

Теперь снова ненадолго вернемся к ограничениям типов - чтобы лучше понять, что такое evidence. Type constraint’ы дают нам дополнительные возможности для наложения ограничений на типы. Существуют 3 типа ограничений:

T =:= U // тип `T` совпадает с `U`
T <:< U // тип `T` является подтипом `U`
T <%< U // для типа `T` должна присутствовать implicit'ная конвертация в тип `U` 

Для того, чтобы воспользоваться этими ограничениями, мы добавляем implicit’ный параметр - т.н. “evidence” (видимо, это означает, что параметр является, так сказать, свидетелем типа, т.е. содержит дополнительную информацию о типе):

class Pair[T](val first: T, val second: T)(implicit ev: T <:< Comparable[T])

Кстати, эти type constraint’ы являются частью библиотеки, а не языка. В приведенном примере можно было бы воспользоваться type bound’ом, а именно

class `Pair[T <: Comparable[T]]`. 

Однако, type constraint’ы обладают некоторой дополнительной гибкостью. Например, метод orNull в классе Option:

val friends = Map("Fred" -> "Barney", ...) 
val friendOpt = friends.get("Wilma") // An Option[String] 
val friendOrNull = friendOpt.orNull  // A String or null 

Метод orNull будет работать только для типов, которые могут иметь null в качестве валидного значения - т.е. мы можем им воспользоваться для типа String, но не можем воспользоваться для типа Int, но поскольку orNull реализует это ограничение как Null <:< A, мы все равно можем создать Option[Int] - при условии что мы не будем пользоваться методом orNull для такого значения.

Другой случай использования type constraint’ов - для лучшего выведения типов (type inference). Например, пусть нам нужно написать функцию, возвращающую первый и последний элемент из некоего Iterable:

def firstLast[A, C <: Iterable[A]](it: C) = (it.head, it.last) 

Если мы напишем вызов firstLast(List(1, 2, 3)), то получим от компилятора сообщение inferred type arguments [Nothing,List[Int]] do not conform to method firstLast’s type parameter bounds [A,C <: Iterable[A]] don’t conform to [A, C <: Iterable[A]]. Почему Nothing? Компонент, занимающийся выведением типов, не может вывести тип A из List(1, 2, 3) и одновременно с этим C за один шаг. Для того, чтобы исправить ситуацию, мы можем сделать так, что сначала будет выведен C, а затем A:

def firstLast[A, C](it: C)(implicit ev: C <:< Iterable[A]) = (it.head, it.last)

Как уже отмечалось, параметр типа может иметь context bound вида T : M, где M - другой generic тип, и context bound требует implicit’ного значения типа T[M]. Например,

class Pair[T : Ordering] 

потребует наличия implicit’ного значения типа Ordering[T]. И как мы уже знаем, эта запись разворачивается в

class Pair[T](implicit evidence : Ordering[T])

Это implicit’ное значение затем может быть использовано в теле функции. Например:

class Pair[T: Ordering](val first: T, val second: T) {
  def smaller(implicit ord: Ordering[T]) = if (ord.compare(first, second) < 0) first else second
}

Если мы напишем Pair(40, 2), то компилятор выведет, что нам нужен тип Pair[Int]. Поскольку автоматически нам доступно значение Ordering[Int], объявленное в scala.math.Ordering:

object Ordering extends LowPriorityOrderingImplicits {
... 
  implicit object Int extends IntOrdering
...
}

то Int удовлетворяет нашему context bound’у. При необходимости, мы можем получить evidence с помощью функции implicitly из Predef:

class Pair[T: Ordering](val first: T, val second: T) {
  def smaller = if (implicitly[Ordering[T]].compare(first, second) < 0) first else second
}

Функция implicitly чрезвычайно проста:

def implicitly[T](implicit e: T) = e // For summoning implicit values from the nether world 

Т.е. создать объект Pair[T] можно при наличии соответствующего Ordering[T]. Например, если нам нужен Pair[Point], то мы должны создать соответствующие implicit’ное значение типа Ordering[Point]:

implicit object PointOrdering extends Ordering[Point] {
  def compare(a: Point, b: Point) = ...
}

Использование context bound’ов в стандартной библиотеке

Типичный пример из стандартной библиотеки, не связанный с type class’ами:

object Array {
//...
  def ofDim[T: ClassManifest](n1: Int): Array[T] =
    new Array[T](n1)
//...
}

Инициализация массива параметрического типа требует наличия ClassManifest’а, из-за type erasure в JVM с одной стороны, а с другой - того факта, что массивы в Scala ничем не отличаются от других параметрических типов (таких, как например, List[T]), и все равно требуют указания типа T.

Другой типичный паттерн, который мы уже видели, и встречающийся в стандартной библиотеке:

def f[A : Ordering](a: A, b: A) = implicitly[Ordering[A]].compare(a, b)

Здесь, с помощью implicitly, мы получаем implicit’ное значение типа Ordering[A].

Ну и как мы уже видели, реализация context bound’ов базируется на implicit’ных параметрах, т.е. context bound’ы являются по сути синтаксическим сахаром, и запись:

def g[A : B](a: A) = h(a)

эквивалентна

def g[A](a: A)(implicit ev: B[A]) = h(a)

Поэтому, никто не запрещает нам пользоваться “несахаризированным” вариантом записи:

def f[A](a: A, b: A)(implicit ord: Ordering[A]) = ord.compare(a, b)

Context bound’ы, как мы уже видели, являются краеугольным камнем для классов типов - паттерн, который является чем-то вроде implicit’ного адаптера.

Примером из библиотеки является использование появившегося в Scala 2.8 Ordering. В отличие от Ordered, Ordering не предполагается для непосредственного наследования, его можно сравнить с компаратором в JDK.

def f[A : Ordering](a: A, b: A) = if (implicitly[Ordering[A]].lt(a, b)) a else b

Хотя, часто можно встретить “несахаризированный” вариант; кстати, имея доступ к Ordering, мы можем задействовать implicit’ные для Ordering в scala.math.Ops, чтобы пользоваться операторами:

def f[A](a: A, b: A)(implicit ord: Ordering[A]) = {
    import ord._
    if (a < b) a else b
}

Context bound и типы классов улучшают модульность и уменьшают количество зависимостей между компонентами, и view bound’ы, теоретически, при правильном дизайне, не должны быть востребованы.

Уже имеющиеся view bound’ы часто можно заменить context bound’ами. Приведем несколько синтетический пример. Пусть у нас есть функция с view bound’ом, сигнализирующем о том, что для некоторого типа T у нас должно существовать implicit’ное преобразование в Int:

scala> def foo[T <% Int](x: T):Int = x
foo: [T](x: T)(implicit evidence$1: T => Int)Int
 
scala> implicit def convertToInt[T](n:T) = n match {
 | case x:String => x.toInt
 | }
warning: there were 1 feature warning(s); re-run with -feature for details
convertToInt: [T](n: T)Int
 
scala> foo("23")
res4: Int = 23

Естественно, мы получим warning о том, что view bound’ы уже deprecated. Перепишем это с использованием context bound - нам также понадобиться implicit’ное значение типа T => Int:

type L[X] = X => Int

implicit def convertToInt[T](n:T): Int = n match {
  case x:String => x.toInt
}

def foo[T : L](x: T):Int = x

Источники: