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ę m
na obiekcie o
klasy C
, a ta klasa nie obsługuje metody m
, wówczas Scala będzie szukała niejawnej konwersji z C
czegoś, co obsługuje m
. Przykładem byłaby prosta metoda map
na String
:
"abc".map(_.toInt)
String
nie obsługuje tej metody map
, ale ją StringOps
obsługuje i istnieje domniemana konwersja z String
na StringOps
dostępny (patrz implicit def augmentString
dalej 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 foo
deklaracja 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 getIndex
może odbierać dowolny obiekt, o ile istnieje domyślna konwersja z jego klasy na Seq[T]
. Z tego powodu mogę przekazać String
do 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 getIndex
moż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 Integral
klasa 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 sorted
na Seq
potrzeby 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 Option
znajduje się niejawna konwersja na Iterable
, więc można wywoływać Iterable
metody Option
lub przekazywać Option
coś, 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.flatMap
oczekuje TraversableOnce
, co Option
nie jest. Następnie kompilator zagląda do Option
obiektu towarzyszącego i znajduje konwersję na Iterable
, co oznacza TraversableOnce
poprawność tego wyrażenia.
Po drugie, obiekt towarzyszący oczekiwanego typu:
List(1, 2, 3).sorted
Metoda sorted
ma charakter niejawny Ordering
. W tym przypadku zagląda do obiektu Ordering
, towarzyszącego klasie Ordering
i 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 A
bę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 A
zostanie przeszukany pod kątem implicitów, zgodnie z powyższą regułą.
Zauważ, że nie oznacza to, że domyślny zakres A
bę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ę Ordering
na przykład: Ma on w sobie obiekt pośredni, ale nie można do niego dodawać żadnych elementów. Jak więc stworzyć Ordering
wł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 sorted
oczekuje Ordering[A]
(faktycznie oczekuje Ordering[B]
, gdzie B >: A
). W środku nie ma czegoś takiego Ordering
i 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 : Ordering
definiuje się jako trait Ordering[T]
, gdzie T
jest 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 A
jest 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: