Де Скала шукає наслідки?


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. Однак обидва класи поділяють безліч методів, так чому ж Скала не скаржиться на неоднозначність, коли, скажімо, дзвонить 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)(_ + _)
}

Межі контексту корисніші, коли вам просто потрібно передати їх іншим методам, які ними користуються. Наприклад, метод 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"

Ось як Скала знайшов неявне, 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параметр типу. Раніше я говорив, що Скала заглянув усередину параметрів типу, що не має особливого сенсу. Неявне шукав вище 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"

Інші розміри

Я впевнений, що це був жарт, але ця відповідь може не бути актуальною. Тому не сприймайте це питання як остаточний арбітр того, що відбувається, і якщо ви помітили, що воно застаріло, повідомте мене, щоб я міг його виправити.

EDIT

Пов'язані питання, що цікавлять:


60
Настав час, щоб ви почали використовувати свої відповіді в книзі, поки це лише питання скласти їх разом.
pedrofurla

3
@pedrofurla Мене вважали написанням книги португальською мовою. Якщо хтось може знайти мені контакт з технічним видавцем ...
Даніель К. Собрал

2
Обшукуються також об'єкти пакетів супутників частин типу. lampvn.epfl.ch/trac/scala/ticket/4427
ретронім

1
У цьому випадку це частина неявної сфери. Сайт для дзвінків не повинен бути в межах цього пакету. Це мене здивувало.
ретронім

2
Так, так stackoverflow.com/questions/8623055 висвітлює саме це, але я помітив, що ви написали "Наступний список призначений для подання у порядку пріоритетності ... будь ласка, повідомте про це". В основному, внутрішні списки повинні бути не упорядкованими, оскільки всі вони мають однакову вагу (принаймні в 2.10).
Євген Йокота

23

Я хотів з’ясувати пріоритет неявної роздільної здатності параметрів, а не лише там, де це шукає, тому я написав допис у блозі, в якому переглядав імпліцити без податку на імпортнеявний параметр переваги параметрів знову після деяких відгуків).

Ось список:

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

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


3
Це можна покращити, якби ви написали якийсь код, що просто визначав пакети, об'єкти, ознаки та класи та використовував їх букви, коли ви посилаєтесь на область застосування. Зовсім не потрібно ставити декларацію про метод - лише імена та хто розширює кого та в якій області.
Даніель К. Собрал
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.