Где Scala ищет последствия? - PullRequest
386 голосов
/ 08 апреля 2011

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

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

Ответы [ 2 ]

543 голосов
/ 08 апреля 2011

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

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

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

Если говорить очень кратко о последнем типе, если кто-то вызывает метод m для объекта o класса C, и этот класс не поддерживает метод m, то Scala будет искать неявный преобразование из C во что-то, что поддерживает m. Простым примером будет метод map on 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)(_ + _)
}

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

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.

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

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

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

Метод 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 - фактический тип, а не параметр типа: это аргумент типа до 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"

Другие измерения

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

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

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

23 голосов
/ 01 января 2012

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

Вот список:

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

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

...