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.
- Pierwsze spojrzenie w bieżącym zakresie
- Implikacje zdefiniowane w bieżącym zakresie
- Jawny import
- importowanie symboli wieloznacznych
Ten sam zakres w innych plikach
- 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: