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

398

Неявный вопрос новичков в 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?

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

Даниэль С. Собрал
источник

Ответы:

554

Типы последствий

Под влиянием в 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. Есть библиотека, которая интенсивно использует этот шаблон, называется Scalaz.

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

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)(_ + _)
}

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

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

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

Откуда берутся последствия?

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

Этот поиск подчиняется определенным правилам, которые определяют, какие последствия видны, а какие нет. В следующей таблице, показывающей, где компилятор будет искать импликации, была взята отличная презентация имплики, Джоша Суерета о имплицитах, которую я искренне рекомендую всем, кто хочет улучшить свои знания 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

Wildcard Imports

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

Метод 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"

Вот как Скала обнаружил неявное, 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, например: Он поставляется с некоторыми implicits в своем объекте компаньона, но вы не можете добавить материал к нему. Так как же создать 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фактический тип, а не параметр типа: это аргумент типа для 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"

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

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

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

Смежные вопросы, представляющие интерес:

Даниэль С. Собрал
источник
60
Пришло время начать использовать свои ответы в книге, а теперь нужно только собрать все это вместе.
педрофурла
3
@pedrofurla Меня считают написанием книги на португальском. Если кто-то может найти мне контакт с техническим издателем ...
Даниэль С. Собрал
2
Объекты пакета компаньонов частей типа также ищутся. lampvn.epfl.ch/trac/scala/ticket/4427
ретроним
1
В этом случае это часть неявной области видимости. Сайт вызова не обязательно должен быть в этом пакете. Это было удивительно для меня.
ретроним
2
Да, так что stackoverflow.com/questions/8623055 охватывает это конкретно, но я заметил, что вы написали: «Следующий список предназначен для представления в порядке приоритета ... пожалуйста, сообщите». По сути, внутренние списки должны быть неупорядоченными, поскольку все они имеют одинаковый вес (по крайней мере, в 2.10).
Евгений Йокота
23

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

Вот список:

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

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

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