Где Скала ищет неявки?

Неявным вопросом для новичков в 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) [email protected] scala> foo(0L) [email protected] 

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

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

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

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

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

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

    Говоря очень кратко о последнем типе, если вы назовите метод m на объекте o classа C , и этот class не поддерживает метод 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 может получать любой объект, если существует неявное преобразование, доступное из его classа в 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 ).

    Контекстные frameworks

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

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

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

     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 .

    Откуда берутся Имплициты?

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

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

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

    1. Первый взгляд в текущей области
      • Имплициты, определенные в текущей области
      • Явный импорт
      • импорт подстановочных знаков
      • Такая же область в других файлах
    2. Теперь посмотрим на связанные типы в
      • Сопутствующие объекты типа
      • Неявная область действия аргумента (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)(_ + _) } 

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

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

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

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

    Здесь есть два объектных компаньона. Во-первых, рассматривается объект-компаньон типа «источник». Например, внутри объекта 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 ожидает TraversableOnce , какой Option нет. Затем компилятор заглядывает в компаньона объекта Option и находит преобразование в Iterable , которое является TraversableOnce , что делает это выражение правильным.

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

     List(1, 2, 3).sorted 

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

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

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

    Так Скала обнаружила неявные 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.

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

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

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

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

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

     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 bm } val a = new A(5) val b = new aB(3) val s: String = b // s == "B: 3" 

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

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

    РЕДАКТИРОВАТЬ

    Связанные вопросы:

    • Границы контекста и представления
    • Цепочные импликации
    • Scala: Неявный приоритет разрешения параметров

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

    Вот список:

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

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

    Давайте будем гением компьютера.