Где Scala ищет последствия?

339

Неявный вопрос для новичков в Scala выглядит следующим образом: где компилятор ищет implicits? Я подразумеваю подразумеваемый, потому что вопрос никогда не кажется полностью сформированным, как будто для этого не было слов.:-) Например, где значения для integral приведены ниже?

scala> import scala.math._
import scala.math._

scala> def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
foo: [T](t: T)(implicit integral: scala.math.Integral[T])Unit

scala> foo(0)
scala.math.Numeric$IntIsIntegral$@3dbea611

scala> foo(0L)
scala.math.Numeric$LongIsIntegral$@48c610af

Еще один вопрос, который следует за теми, кто решил узнать ответ на первый вопрос, заключается в том, как компилятор выбирает, что имплицитно использовать, в определенных ситуациях кажущейся двусмысленности (но это компилируется в любом случае)?

Например, scala.Predef определяет два преобразования из String: один в WrappedString, а другой - StringOps. Однако оба класса имеют множество методов, поэтому почему Scala не жалуется на двусмысленность, если, например, вызывает map?

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

Теги:
implicit-conversion
implicits

2 ответа

510
Лучший ответ

Типы впечатлений

Implicits в Scala означает либо значение, которое может передаваться "автоматически", так сказать, или преобразование из одного типа в другое, которое создается автоматически.

Неявное преобразование

Говоря очень кратко о последнем типе, если вы вызываете метод m для объекта o класса C, и этот класс не поддерживает метод m, тогда Scala будет искать неявное преобразование из C в то, что поддерживает m. Простым примером может быть метод map на String:

"abc".map(_.toInt)

String не поддерживает метод map, но StringOps делает, и там доступно неявное преобразование от String до StringOps (см. implicit def augmentString на Predef).

Неявные параметры

Другим видом неявного является неявный параметр. Они передаются вызовам метода, как и любой другой параметр, но компилятор пытается их автоматически заполнить. Если он не может, он будет жаловаться. Можно явно передать эти параметры, например, использовать breakOut (см. Вопрос о breakOut, в тот день, когда вы испытываете трудности).

В этом случае нужно объявить необходимость неявного, например, объявление метода foo:

def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}

Просмотреть оценки

Там одна ситуация, когда неявное является неявным преобразованием и неявным параметром. Например:

def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)

getIndex("abc", 'a')

Метод getIndex может получать любой объект, если существует неявное преобразование, доступное из его класса, в Seq[T]. Из-за этого я могу передать String в getIndex, и он будет работать.

За кулисами компилятор изменяет seq.IndexOf(value) на conv(seq).indexOf(value).

Это настолько полезно, что для их написания есть синтаксический сахар. Используя этот синтаксический сахар, getIndex можно определить следующим образом:

def getIndex[T, CC <% Seq[T]](seq: CC, value: T) = seq.indexOf(value)

Этот синтаксический сахар описывается как оценка, сродная верхней границе (CC <: Seq[Int]) или нижней границе (T >: Null).

Контекстные рамки

Другим распространенным шаблоном в неявных параметрах является шаблон типа. Этот шаблон позволяет предоставлять общие интерфейсы классам, которые не объявляли их. Он может служить как шаблон моста - получение разделения проблем - и как шаблон адаптера.

Класс Integral, который вы упомянули, является классическим примером шаблона типа. Другим примером стандартной библиотеки Scala является Ordering. Там есть библиотека, которая сильно использует этот шаблон под названием Scalaз.

Это пример его использования:

def sum[T](list: List[T])(implicit integral: Integral[T]): T = {
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

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

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Границы контекста более полезны, когда вам просто нужно передать их другим методам, которые их используют. Например, для метода sorted на Seq нужен неявный Ordering. Чтобы создать метод reverseSort, можно написать:

def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse

Поскольку Ordering[T] был неявно передан в reverseSort, он может затем передать его неявно на sorted.

Откуда берутся Implicits?

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

Этот поиск подчиняется определенным правилам, которые определяют, какие импликации видны, а какие нет. Следующая таблица, показывающая, где компилятор будет искать implicits, была взята из превосходной презентации об имплицитах Джоша Суэрета, которую я от всей души рекомендую всем, кто хочет улучшить их знания Scala. Он был дополнен с тех пор обратной связью и обновлениями.

Признаки, доступные под номером 1 ниже, имеют приоритет над единицами под номером 2. Кроме этого, если есть несколько подходящих аргументов, которые соответствуют типу неявных параметров, наиболее конкретный будет выбран с использованием правил статической перегрузки разрешения (см. Scala Спецификация §6.26.3). Более подробную информацию можно найти в вопросе, на который я ссылаюсь в конце этого ответа.

  • Первый взгляд в текущей области
    • Имплициты, определенные в текущей области
    • Явный импорт
    • импорт подстановок
    • То же самое в других файлах
  • Теперь посмотрим на связанные типы в
    • Сопутствующие объекты типа
    • Неявная область действия типа аргумента (2.9.1)
    • Неявная область аргументов типа (2.8.0)
    • Внешние объекты для вложенных типов
    • Другие размеры

Приведем несколько примеров для них:

Имплициты, определенные в текущем диапазоне

implicit val n: Int = 5
def add(x: Int)(implicit y: Int) = x + y
add(5) // takes n from the current scope

Явный импорт

import scala.collection.JavaConversions.mapAsScalaMap
def env = System.getenv() // Java map
val term = env("TERM")    // implicit conversion from Java Map to Scala Map

Импорт подстановок

def sum[T : Integral](list: List[T]): T = {
    val integral = implicitly[Integral[T]]
    import integral._   // get the implicits in question into scope
    list.foldLeft(integral.zero)(_ + _)
}

Те же области в других файлах

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

Это похоже на первый пример, но предполагая, что неявное определение находится в другом файле, чем его использование. Смотрите также, как объекты пакета могут использоваться для приведения в действие implicits.

Сопутствующие объекты типа

Здесь есть два объектных компаньона. Во-первых, рассматривается объект-компаньон типа "источник". Например, внутри объекта Option существует неявное преобразование в Iterable, поэтому можно называть Iterable методы на Option, или передавать Option на что-то ожидающее Iterable. Например:

for {
    x <- List(1, 2, 3)
    y <- Some('x')
} yield, (x, y)

Это выражение переводится компилятором на

List(1, 2, 3).flatMap(x => Some('x').map(y => (x, y)))

Однако List.flatMap ожидает a TraversableOnce, который Option не является. Затем компилятор просматривает внутри Option объект-компаньон и находит преобразование в Iterable, которое является TraversableOnce, что делает это выражение правильным.

Во-вторых, объект-компаньон ожидаемого типа:

List(1, 2, 3).sorted

Метод sorted принимает неявный Ordering. В этом случае он просматривает объект Ordering, компаньон к классу Ordering и находит там неявный Ordering[Int].

Обратите внимание, что также рассматриваются сопутствующие объекты суперклассов. Например:

class A(val n: Int)
object A { 
    implicit def str(a: A) = "A: %d" format a.n
}
class B(val x: Int, y: Int) extends A(y)
val b = new B(5, 2)
val s: String = b  // s == "A: 2"

Вот как Scala нашел неявный Numeric[Int] и Numeric[Long] в вашем вопросе, кстати, поскольку они находятся внутри Numeric, а не Integral.

Неявный масштаб типа аргумента

Если у вас есть метод с типом аргумента A, тогда будет рассмотрен неявный объем типа A. Под "неявной областью" я подразумеваю, что все эти правила будут применяться рекурсивно - например, объект-компаньон A будет искать имплициты в соответствии с приведенным выше правилом.

Обратите внимание, что это не означает, что неявная область A будет искать преобразования этого параметра, а всего выражения. Например:

class A(val n: Int) {
  def +(other: A) = new A(n + other.n)
}
object A {
  implicit def fromInt(n: Int) = new A(n)
}

// This becomes possible:
1 + new A(1)
// because it is converted into this:
A.fromInt(1) + new A(1)

Это доступно с Scala 2.9.1.

Неявная область аргументов типа

Это необходимо для того, чтобы сделать шаблон класса типов действительно выполненным. Рассмотрим Ordering, например: он поставляется с некоторыми имплицитами в своем сопутствующем объекте, но вы не можете добавлять к нему материал. Итак, как вы можете сделать Ordering для своего собственного класса, который автоматически найден?

Начнем с реализации:

class A(val n: Int)
object A {
    implicit val ord = new Ordering[A] {
        def compare(x: A, y: A) = implicitly[Ordering[Int]].compare(x.n, y.n)
    }
}

Итак, подумайте, что происходит, когда вы вызываете

List(new A(5), new A(2)).sorted

Как мы видели, метод sorted ожидает Ordering[A] (на самом деле он ожидает Ordering[B], где B >: A). Внутри Ordering нет такой вещи, и нет типа "источника", на который нужно смотреть. Очевидно, он находит его внутри A, который является аргументом типа Ordering.

Это также показывает, как работают различные методы коллекции, ожидающие CanBuildFrom: имплициты находятся внутри объектов-компаньонов с параметрами типа CanBuildFrom.

Примечание: Ordering определяется как trait Ordering[T], где T является параметром типа. Ранее я сказал, что Scala заглянул в параметры типа, что не имеет большого смысла. Неявным, смотрящим выше, является Ordering[A], где A является фактическим типом, а не параметром type: это аргумент типа Ordering. См. Раздел 7.2 спецификации Scala.

Это доступно с Scala 2.8.0.

Внешние объекты для вложенных типов

Я действительно не видел примеров этого. Я был бы признателен, если бы кто-то мог поделиться им. Принцип прост:

class A(val n: Int) {
  class B(val m: Int) { require(m < n) }
}
object A {
  implicit def bToString(b: A#B) = "B: %d" format b.m
}
val a = new A(5)
val b = new a.B(3)
val s: String = b  // s == "B: 3"

Другие размеры

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

ИЗМЕНИТЬ

Связанные с этим вопросы:

  • 56
    Пришло время начать использовать свои ответы в книге, а теперь нужно только собрать все это вместе.
  • 3
    @pedrofurla Меня считают написанием книги на португальском языке. Если кто-то может найти мне контакт с техническим издателем ...
Показать ещё 24 комментария
22

Я хотел узнать приоритет неявного разрешения параметров, а не только там, где он выглядит, поэтому я написал сообщение в блоге пересмотреть implicits без налога на импортнеявный приоритет параметра после некоторой обратной связи).

Вот список:

  • 1) подразумевает видимость текущей области вызова через локальное объявление, импорт, внешнюю область, наследование, объект пакета, доступный без префикса.
  • 2) неявная область, содержащая все виды сопутствующих объектов и объект пакета, которые имеют некоторое отношение к неявному типу, который мы ищем (т.е. пакетный объект типа, сопутствующий объект самого типа его конструктора типа if любой, его параметров, если таковой имеется, а также его супертипа и супертрайтов).

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

  • 2
    Это могло бы быть улучшено, если бы вы написали какой-то код, просто определяющий пакеты, объекты, признаки и классы и использующий их буквы при обращении к области действия. Нет необходимости помещать какое-либо объявление метода - только имена, кто кого расширяет и в какой области.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню