W Scali możemy użyć co najmniej dwóch metod do modernizacji istniejących lub nowych typów. Załóżmy, że chcemy wyrazić, że coś można określić ilościowo za pomocąInt
. Możemy zdefiniować następującą cechę.
Niejawna konwersja
trait Quantifiable{ def quantify: Int }
A potem możemy użyć niejawnych konwersji do kwantyfikacji np. Ciągów znaków i list.
implicit def string2quant(s: String) = new Quantifiable{
def quantify = s.size
}
implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{
val quantify = l.size
}
Po ich zaimportowaniu możemy wywołać metodę quantify
na łańcuchach i listach. Zauważ, że wymierna lista przechowuje swoją długość, więc pozwala uniknąć kosztownego przechodzenia przez listę przy kolejnych wywołaniachquantify
.
Klasy typu
Alternatywą jest zdefiniowanie „świadka”, Quantified[A]
który stwierdza, że pewien typ A
można określić ilościowo.
trait Quantified[A] { def quantify(a: A): Int }
Następnie udostępniamy instancje tego typu klasy dla String
i List
gdzieś.
implicit val stringQuantifiable = new Quantified[String] {
def quantify(s: String) = s.size
}
A jeśli następnie napiszemy metodę, która wymaga kwantyfikacji swoich argumentów, piszemy:
def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) =
as.map(ev.quantify).sum
Lub używając składni związanej z kontekstem:
def sumQuantities[A: Quantified](as: List[A]) =
as.map(implicitly[Quantified[A]].quantify).sum
Ale kiedy użyć której metody?
Teraz pojawia się pytanie. Jak mogę zdecydować między tymi dwoma koncepcjami?
Co zauważyłem do tej pory.
klasy typów
- klasy typów pozwalają na ładną składnię związaną z kontekstem
- z klasami typów nie tworzę nowego obiektu opakowującego przy każdym użyciu
- składnia związana z kontekstem nie działa już, jeśli klasa typu ma wiele parametrów typu; wyobraź sobie, że chcę określić ilościowo rzeczy nie tylko za pomocą liczb całkowitych, ale za pomocą wartości pewnego rodzaju ogólnego
T
. Chciałbym utworzyć klasę typuQuantified[A,T]
niejawna konwersja
- ponieważ tworzę nowy obiekt, mogę tam przechowywać wartości lub obliczyć lepszą reprezentację; ale czy powinienem tego unikać, ponieważ może się to zdarzyć kilka razy, a jawna konwersja prawdopodobnie zostanie wywołana tylko raz?
Czego oczekuję od odpowiedzi
Przedstaw jeden (lub więcej) przypadków użycia, w których różnica między oboma pojęciami ma znaczenie i wyjaśnij, dlaczego wolałbym jedno od drugiego. Również wyjaśnienie istoty tych dwóch pojęć i ich wzajemnych relacji byłoby miłe, nawet bez przykładu.
źródło
size
wykazu w wartości i powiedzieć, że unika się kosztownych przechodzenie z listy po kolejnych zaproszeń do oszacowania, ale na każdym wywołaniu do zostanie wywołany od nowa, przywracając w ten sposób i ponownie obliczając majątek. Mówię, że w rzeczywistości nie ma sposobu na buforowanie wyników za pomocą niejawnych konwersji.quantify
list2quantifiable
Quantifiable
quantify
Odpowiedzi:
Chociaż nie chcę powielać mojego materiału ze Scala In Depth , myślę, że warto zauważyć, że klasy typu / cechy typu są nieskończenie bardziej elastyczne.
def foo[T: TypeClass](t: T) = ...
ma możliwość przeszukiwania lokalnego środowiska w poszukiwaniu domyślnej klasy typu. Jednak w dowolnym momencie mogę zmienić domyślne zachowanie na jeden z dwóch sposobów:
Oto przykład:
def myMethod(): Unit = { // overrides default implicit for Int implicit object MyIntFoo extends Foo[Int] { ... } foo(5) foo(6) // These all use my overridden type class foo(7)(new Foo[Int] { ... }) // This one needs a different configuration }
To sprawia, że klasy typów są nieskończenie bardziej elastyczne. Inną rzeczą jest to, że klasy / cechy typów lepiej obsługują niejawne wyszukiwanie .
W pierwszym przykładzie, jeśli używasz niejawnego widoku, kompilator wykona niejawne wyszukiwanie:
Function1[Int, ?]
Który obejrzy
Function1
obiekt towarzyszący iInt
obiekt towarzyszący.Zauważ, że nie
Quantifiable
ma tego nigdzie w niejawnym wyszukiwaniu. Oznacza to, że musisz umieścić niejawny widok w obiekcie pakietu lub zaimportować go do zakresu. Zapamiętywanie tego, co się dzieje, wymaga więcej pracy.Z drugiej strony klasa typu jest jawna . Widzisz, czego szuka w sygnaturze metody. Masz również niejawne wyszukiwanie
Quantifiable[Int]
który będzie szukał
Quantifiable
obiektu towarzyszącego iInt
obiektu towarzyszącego. Oznacza to, że możesz podać wartości domyślne, a nowe typy (takie jakMyString
klasa) mogą zapewnić wartość domyślną w swoim obiekcie towarzyszącym i zostaną one niejawnie przeszukane.Generalnie używam klas typów. W pierwszym przykładzie są one nieskończenie bardziej elastyczne. Jedynym miejscem, w którym używam niejawnych konwersji, jest użycie warstwy API między opakowaniem Scala a biblioteką Java, a nawet to może być „niebezpieczne”, jeśli nie jesteś ostrożny.
źródło
Jednym z kryteriów, które mogą wejść w grę, jest to, jak chcesz, aby nowa funkcja „czuła się”; używając niejawnych konwersji, możesz sprawić, że będzie wyglądać, jakby to była tylko inna metoda:
"my string".newFeature
... podczas korzystania z klas typów zawsze będzie wyglądać tak, jakbyś wywoływał funkcję zewnętrzną:
newFeature("my string")
Jedną z rzeczy, które można osiągnąć za pomocą klas typów, a nie za pomocą niejawnych konwersji, jest dodanie właściwości do typu , a nie do wystąpienia typu. Następnie możesz uzyskać dostęp do tych właściwości, nawet jeśli nie masz dostępnego wystąpienia tego typu. Przykładem kanonicznym byłoby:
trait Default[T] { def value : T } implicit object DefaultInt extends Default[Int] { def value = 42 } implicit def listsHaveDefault[T : Default] = new Default[List[T]] { def value = implicitly[Default[T]].value :: Nil } def default[T : Default] = implicitly[Default[T]].value scala> default[List[List[Int]]] resN: List[List[Int]] = List(List(42))
Ten przykład pokazuje również, jak ściśle powiązane są pojęcia: klasy typów nie byłyby tak użyteczne, gdyby nie istniał mechanizm do tworzenia nieskończenie wielu ich instancji; bez
implicit
metody (bez konwersji, co prawda) mógłbym mieć tylko skończenie wiele typów posiadających tęDefault
własność.źródło
default
dla przyszłych czytelników.Możesz wyobrazić sobie różnicę między tymi dwiema technikami przez analogię do zastosowania funkcji, tylko z nazwanym opakowaniem. Na przykład:
trait Foo1[A] { def foo(a: A): Int } // analogous to A => Int trait Foo0 { def foo: Int } // analogous to Int
Wystąpienie pierwszego zawiera funkcję typu
A => Int
, podczas gdy wystąpienie drugiego zostało już zastosowane do plikuA
. Możesz kontynuować wzór ...trait Foo2[A, B] { def foo(a: A, b: B): Int } // sort of like A => B => Int
w ten sposób można by pomyśleć o
Foo1[B]
częściowym zastosowaniuFoo2[A, B]
do jakiejśA
instancji. Doskonały przykład tego został opisany przez Milesa Sabina jako „Zależności funkcjonalne w Scali” .Tak naprawdę chodzi mi o to, że w zasadzie:
źródło