Gdzie Scala szuka implicitów?

398

Niejawna pytanie do nowoprzybyłych do Scala wydaje się być: gdzie robi wygląd kompilator implicits? Mam na myśli domniemanie, ponieważ pytanie nigdy nie wydaje się w pełni uformowane, tak jakby nie było na to słów. :-) Na przykład, skąd integralpochodzą poniższe wartości ?

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

Kolejnym pytaniem, które dotyczy osób, które decydują się na odpowiedź na pierwsze pytanie, jest to, w jaki sposób kompilator decyduje, którego ukrytego użyć, w pewnych sytuacjach pozornej niejednoznaczności (ale i tak się kompiluje)?

Na przykład scala.Predefdefiniuje dwie konwersje z String: jednej na WrappedStringdrugą i na StringOps. Obie klasy korzystają jednak z wielu metod, więc dlaczego Scala nie narzeka na dwuznaczność, powiedzmy, kiedy dzwoni map?

Uwaga: to pytanie zostało zainspirowane innym pytaniem , w nadziei na przedstawienie problemu w bardziej ogólny sposób. Przykład został skopiowany stamtąd, ponieważ jest o nim mowa w odpowiedzi.

Daniel C. Sobral
źródło

Odpowiedzi:

554

Rodzaje implikacji

Implikacje w Scali odnoszą się albo do wartości, którą można przekazać „automatycznie”, że tak powiem, albo do konwersji z jednego typu na drugi, która jest dokonywana automatycznie.

Implikowana konwersja

Mówiąc bardzo krótko o tym drugim typie, jeśli wywołujemy metodę mna obiekcie oklasy C, a ta klasa nie obsługuje metody m, wówczas Scala będzie szukała niejawnej konwersji z Cczegoś, co obsługuje m. Przykładem byłaby prosta metoda mapna String:

"abc".map(_.toInt)

Stringnie obsługuje tej metody map, ale ją StringOpsobsługuje i istnieje domniemana konwersja z Stringna StringOpsdostępny (patrz implicit def augmentStringdalej Predef).

Implikowane parametry

Innym rodzajem niejawnym jest parametr niejawny . Są one przekazywane do wywołań metod jak każdy inny parametr, ale kompilator próbuje je wypełnić automatycznie. Jeśli nie może, będzie narzekać. Jeden może przekazać te parametry jednoznacznie, co jest, jak ktoś używa breakOut, na przykład (patrz pytanie breakOut, na dzień jesteś czuje się na wyzwanie).

W takim przypadku należy zadeklarować potrzebę niejawnego, takiego jak foodeklaracja metody:

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

Wyświetl ograniczenia

Jest jedna sytuacja, w której domniemana jest zarówno domniemana konwersja, jak i niejawny parametr. Na przykład:

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

getIndex("abc", 'a')

Metoda getIndexmoże odbierać dowolny obiekt, o ile istnieje domyślna konwersja z jego klasy na Seq[T]. Z tego powodu mogę przekazać Stringdo getIndex, a to zadziała.

Za kulisami kompilator zmienia się seq.IndexOf(value)na conv(seq).indexOf(value).

Jest to tak przydatne, że istnieje cukier syntaktyczny do ich napisania. Za pomocą tego cukru syntaktycznego getIndexmożna zdefiniować w następujący sposób:

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

Ten cukier składniowy jest opisany jako związany z widokiem , podobny do górnej granicy ( CC <: Seq[Int]) lub dolnej granicy ( T >: Null).

Kontekst Granice

Innym częstym wzorcem w niejawnych parametrach jest wzorzec klasy typów . Ten wzorzec umożliwia udostępnianie wspólnych interfejsów klasom, które ich nie deklarowały. Może służyć zarówno jako wzorzec pomostowy - oddzielając obawy - i jako wzorzec adaptera.

Wspomniana Integralklasa jest klasycznym przykładem wzorca klasy. Innym przykładem standardowej biblioteki Scali jest Ordering. Istnieje biblioteka, która intensywnie wykorzystuje ten wzór, zwana Scalaz.

Oto przykład jego użycia:

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

Istnieje również cukier syntaktyczny, zwany związanym kontekstem , który jest mniej przydatny z powodu potrzeby odwoływania się do tego, co ukryte. Prosta konwersja tej metody wygląda następująco:

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

Granice kontekstu są bardziej przydatne, gdy wystarczy przekazać je innym metodom, które ich używają. Na przykład, metoda sortedna Seqpotrzeby niejawna Ordering. Aby utworzyć metodę reverseSort, można napisać:

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

Ponieważ Ordering[T]został przekazany w sposób dorozumiany reverseSort, może następnie przekazać go w sposób dorozumiany sorted.

Skąd się biorą implicity?

Kiedy kompilator dostrzeże potrzebę niejawnego, albo dlatego, że wywołujesz metodę, która nie istnieje w klasie obiektu, albo ponieważ wywołujesz metodę wymagającą niejawnego parametru, będzie szukał niejawnej, która będzie pasować do potrzeby .

Poszukiwania są zgodne z pewnymi zasadami, które określają, które implikacje są widoczne, a które nie. Poniższa tabela pokazująca, gdzie kompilator będzie szukał implicitów, została zaczerpnięta z doskonałej prezentacji Josha Sueretha na temat implicitów, którą gorąco polecam każdemu, kto chce poprawić swoją znajomość Scali. Od tego czasu jest uzupełniany opiniami i aktualizacjami.

Implikacje dostępne pod numerem 1 poniżej mają pierwszeństwo przed implikacjami pod numerem 2. Poza tym, jeśli istnieje kilka kwalifikujących się argumentów, które pasują do typu parametru niejawnego, najbardziej szczegółowy zostanie wybrany przy użyciu reguł rozdzielczości statycznego przeciążenia (patrz Scala Specyfikacja §6.26.3). Bardziej szczegółowe informacje można znaleźć w pytaniu, do którego prowadzę link na końcu tej odpowiedzi.

  1. Pierwsze spojrzenie w bieżącym zakresie
    • Implikacje zdefiniowane w bieżącym zakresie
    • Jawny import
    • importowanie symboli wieloznacznych
    • Ten sam zakres w innych plikach
  2. Teraz spójrz na powiązane typy w
    • Obiekty towarzyszące danego typu
    • Domniemany zakres typu argumentu (2.9.1)
    • Domniemany zakres argumentów typu (2.8.0)
    • Obiekty zewnętrzne dla typów zagnieżdżonych
    • Inne wymiary

Podajmy dla nich kilka przykładów:

Implikacje zdefiniowane w bieżącym zakresie

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

Jawny import

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

Importowanie symboli wieloznacznych

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

Ten sam zakres w innych plikach

Edycja : Wygląda na to, że nie ma to innego priorytetu. Jeśli masz przykład, który pokazuje rozróżnienie pierwszeństwa, zrób komentarz. W przeciwnym razie nie polegaj na tym.

To jest jak pierwszy przykład, ale zakładając, że niejawna definicja znajduje się w innym pliku niż jego użycie. Zobacz także, w jaki sposób można użyć obiektów pakietu do wprowadzenia implikacji.

Obiekty towarzyszące typu

Istnieją tutaj dwa ważne obiekty towarzyszące. Najpierw badany jest obiekt towarzyszący typu „source”. Na przykład wewnątrz obiektu Optionznajduje się niejawna konwersja na Iterable, więc można wywoływać Iterablemetody Optionlub przekazywać Optioncoś, co oczekuje Iterable. Na przykład:

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

To wyrażenie jest tłumaczone przez kompilator na

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

Jednak List.flatMapoczekuje TraversableOnce, co Optionnie jest. Następnie kompilator zagląda do Optionobiektu towarzyszącego i znajduje konwersję na Iterable, co oznacza TraversableOncepoprawność tego wyrażenia.

Po drugie, obiekt towarzyszący oczekiwanego typu:

List(1, 2, 3).sorted

Metoda sortedma charakter niejawny Ordering. W tym przypadku zagląda do obiektu Ordering, towarzyszącego klasie Orderingi znajduje Ordering[Int]tam ukryty charakter .

Zauważ, że badane są również obiekty towarzyszące superklas. Na przykład:

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"

Tak przy okazji, Scala znalazła ukryte Numeric[Int]i Numeric[Long]twoje pytanie, ponieważ znajdują się w środku Numeric, a nie Integral.

Domniemany zakres typu argumentu

Jeśli masz metodę z typem argumentu A, wówczas domyślny zakres typu Abędzie również brany pod uwagę. Przez „zakres niejawny” rozumiem, że wszystkie te reguły będą stosowane rekurencyjnie - na przykład obiekt towarzyszący Azostanie przeszukany pod kątem implicitów, zgodnie z powyższą regułą.

Zauważ, że nie oznacza to, że domyślny zakres Abędzie przeszukiwany pod kątem konwersji tego parametru, ale całego wyrażenia. Na przykład:

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)

Jest to dostępne od wersji Scala 2.9.1.

Domniemany zakres argumentów typu

Jest to wymagane, aby wzorzec klasy typów naprawdę działał. Zastanówmy się Orderingna przykład: Ma on w sobie obiekt pośredni, ale nie można do niego dodawać żadnych elementów. Jak więc stworzyć Orderingwłasną klasę, która zostanie automatycznie znaleziona?

Zacznijmy od implementacji:

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)
    }
}

Zastanów się, co się stanie, kiedy zadzwonisz

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

Jak widzieliśmy, metoda sortedoczekuje Ordering[A](faktycznie oczekuje Ordering[B], gdzie B >: A). W środku nie ma czegoś takiego Orderingi nie ma typu „źródłowego”, na który można by spojrzeć. Oczywiście, jest znalezienie go w środku A, który jest typ argumentu z Ordering.

W ten sposób działają także różne metody gromadzenia danych CanBuildFrom: implikacje znajdują się wewnątrz obiektów towarzyszących według parametrów typu CanBuildFrom.

Uwaga : Orderingdefiniuje się jako trait Ordering[T], gdzie Tjest parametrem typu. Wcześniej mówiłem, że Scala zajrzał do parametrów typu, co nie ma większego sensu. Domniemana szukali powyżej Ordering[A], gdzie Ajest rzeczywisty typ, nie wpisać parametr: jest to rodzaj argumentu do Ordering. Patrz sekcja 7.2 specyfikacji Scala.

Jest to dostępne od wersji Scala 2.8.0.

Obiekty zewnętrzne dla typów zagnieżdżonych

Tak naprawdę nie widziałem przykładów. Byłbym wdzięczny, gdyby ktoś mógł się nim podzielić. Zasada jest prosta:

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"

Inne wymiary

Jestem prawie pewien, że to był żart, ale ta odpowiedź może nie być aktualna. Więc nie traktuj tego pytania jako ostatecznego arbitra tego, co się dzieje, a jeśli zauważysz, że stało się ono nieaktualne, poinformuj mnie, bym mógł to naprawić.

EDYTOWAĆ

Powiązane interesujące pytania:

Daniel C. Sobral
źródło
60
Nadszedł czas, abyś zaczął używać swoich odpowiedzi w książce, teraz jest to tylko kwestia złożenia wszystkich razem.
pedrofurla,
3
@pedrofurla Rozważano mi pisanie książki po portugalsku. Jeśli ktoś znajdzie mi kontakt z wydawcą technicznym ...
Daniel C. Sobral
2
Przeszukiwane są również obiekty pakietów towarzyszy elementów tego typu. lampvn.epfl.ch/trac/scala/ticket/4427
retronym
1
W takim przypadku jest to część niejawnego zakresu. Witryna połączeń nie musi znajdować się w tym pakiecie. To mnie zaskoczyło.
retronim
2
Tak, więc stackoverflow.com/questions/8623055 to dotyczy, ale zauważyłem, że napisałeś: „Poniższa lista ma być prezentowana w kolejności pierwszeństwa ... zgłoś się”. Zasadniczo listy wewnętrzne powinny być nieuporządkowane, ponieważ wszystkie mają jednakową wagę (przynajmniej w 2.10).
Eugene Yokota
23

Chciałem dowiedzieć się o pierwszeństwie niejawnego rozstrzygania parametrów, a nie tylko tam, gdzie ich szuka, dlatego napisałem post na blogu, w którym ponownie omawiam implikacje bez podatku importowego (i niejawne pierwszeństwo parametrów ponownie po kilku opiniach).

Oto lista:

  • 1) implikacje widoczne dla bieżącego zakresu wywołania poprzez deklarację lokalną, import, zakres zewnętrzny, dziedziczenie, obiekt pakietu, które są dostępne bez prefiksu.
  • 2) zakres niejawny , który zawiera wszelkiego rodzaju obiekty towarzyszące i obiekt pakietu, które mają pewien związek z szukanym typem niejawnym (tj. Obiekt pakietu typu, obiekt towarzyszący samego typu, jego ewentualny konstruktor typu jego parametry, jeśli takie istnieją, a także jego nadtyp i supertraity).

Jeśli na którymkolwiek etapie znajdziemy więcej niż jedną niejawną, do jej rozwiązania zastosowana zostanie reguła przeciążenia statycznego.

Eugene Yokota
źródło
3
Można to poprawić, jeśli napiszesz kod definiujący tylko pakiety, obiekty, cechy i klasy oraz używający ich liter w odniesieniu do zakresu. Nie trzeba w ogóle umieszczać deklaracji metod - tylko nazwy i kto rozszerza kogo iw jakim zakresie.
Daniel C. Sobral