Zależne typy metod, które wcześniej były funkcją eksperymentalną, zostały teraz domyślnie włączone w linii głównej i najwyraźniej wywołało to pewne podekscytowanie w społeczności Scala.
Na pierwszy rzut oka nie jest od razu oczywiste, do czego może to być przydatne. Heiko Seeberger napisali prosty przykład zależnych rodzaju metody tutaj , które, jak można zobaczyć w komentarzu nie mogą być łatwo reprodukowane z parametrami typu na metodach. Nie był to więc zbyt przekonujący przykład. (Mogę pominąć coś oczywistego. Popraw mnie, jeśli tak).
Jakie są praktyczne i przydatne przykłady przypadków użycia dla zależnych typów metod, w których są one wyraźnie lepsze od alternatyw?
Jakie ciekawe rzeczy możemy z nimi zrobić, które wcześniej nie były możliwe / łatwe?
Co kupują nam w porównaniu z istniejącymi funkcjami systemu typów?
Ponadto, czy zależne typy metod są analogiczne lub czerpią inspirację z jakichkolwiek cech występujących w systemach czcionek innych zaawansowanych języków, takich jak Haskell, OCaml?
źródło
Odpowiedzi:
Mniej więcej jakiekolwiek użycie typów składowych (tj. Zagnieżdżonych) może spowodować potrzebę zastosowania zależnych typów metod. W szczególności uważam, że bez metod zależnych klasyczny wzór ciasta jest bliżej bycia anty-wzorem.
Więc w czym problem? Zagnieżdżone typy w Scali zależą od otaczającej je instancji. W konsekwencji, przy braku zależnych typów metod, próby użycia ich poza tym wystąpieniem mogą być frustrująco trudne. Może to zmienić projekty, które początkowo wydają się eleganckie i atrakcyjne, w potworności, które są koszmarnie sztywne i trudne do zreformowania.
Zilustruję to ćwiczeniem, które wykonuję na moim kursie Advanced Scala ,
To przykład klasycznego wzoru ciasta: mamy rodzinę abstrakcji, które są stopniowo udoskonalane przez hierarchię (
ResourceManager
/Resource
są udoskonalane przezFileManager
/File
które z kolei są udoskonalane przezNetworkFileManager
/RemoteFile
). To przykład zabawki, ale wzorzec jest prawdziwy: jest używany w całym kompilatorze Scala i był szeroko stosowany we wtyczce Scala Eclipse.Oto przykład używanej abstrakcji,
Zauważ, że zależność od ścieżki oznacza, że kompilator zagwarantuje, że metody
testHash
itestDuplicates
onNetworkFileManager
można wywołać tylko z argumentami, które im odpowiadają, tj. jest własnyRemoteFiles
i nic więcej.Jest to niezaprzeczalnie pożądana właściwość, ale załóżmy, że chcielibyśmy przenieść ten kod testowy do innego pliku źródłowego? Z zależnymi typami metod bardzo łatwo jest przedefiniować te metody poza
ResourceManager
hierarchią,Zauważ tutaj zastosowania zależnych typów metod: typ drugiego argumentu (
rm.Resource
) zależy od wartości pierwszego argumentu (rm
).Można to zrobić bez zależnych typów metod, ale jest to niezwykle niewygodne, a mechanizm jest dość nieintuicyjny: prowadzę ten kurs od prawie dwóch lat i przez ten czas nikt nie wymyślił działającego rozwiązania bez sugestii.
Wypróbuj sam ...
Po krótkiej chwili zmagania się z tym prawdopodobnie odkryjesz, dlaczego ja (a może to był David MacIver, nie pamiętamy, który z nas ukuł ten termin) nazywam to Piekarnią Zagłady.
Edycja: konsensus jest taki, że Bakery of Doom była monetą Davida MacIvera ...
Na dodatek: forma typów zależnych Scali w ogóle (i zależne typy metod jako jej część) została zainspirowana językiem programowania Beta ... wynikają one naturalnie ze spójnej semantyki zagnieżdżania Beta. Nie znam żadnego innego, nawet słabo głównego języka programowania, który ma zależne typy w tej formie. Języki takie jak Coq, Cayenne, Epigram i Agda mają inną formę pisania zależnego, która jest pod pewnymi względami bardziej ogólna, ale różni się znacznie tym, że jest częścią systemów typów, które w przeciwieństwie do Scala nie mają podtypów.
źródło
def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")
Przypuszczam jednak, że można to uznać za inną formę typów zależnych.Gdzie indziej możemy statycznie zagwarantować, że nie pomieszamy węzłów z dwóch różnych wykresów, np .:
Oczywiście to już zadziałało, jeśli zostało zdefiniowane w środku
Graph
, ale powiedzmy, że nie możemy modyfikowaćGraph
i piszemy dla niego rozszerzenie „pimp my library”.O drugim pytaniu: typy włączane przez tę funkcję są znacznie słabsze niż kompletne typy zależne (zobacz programowanie zależne z typami w Agdzie, aby poznać smak tego). Nie sądzę, że widziałem wcześniej analogię.
źródło
Ta nowa funkcja jest potrzebna, gdy zamiast parametrów typu są używane konkretne elementy członkowskie typu abstrakcyjnego . Gdy używane są parametry typu, zależność typu polimorfizmu rodziny można wyrazić w najnowszej i niektórych starszych wersjach Scala, jak w poniższym uproszczonym przykładzie.
źródło
trait C {type A}; def f[M](a: C { type A = M}, b: M) = 0;class CI extends C{type A=Int};class CS extends C{type A=String}
Jestem opracowanie modelu dla interoption w formie deklaratywnej programowania ze stanem środowiska. Szczegóły nie są tutaj istotne (np. Szczegóły dotyczące wywołań zwrotnych i koncepcyjnego podobieństwa do modelu Actor w połączeniu z Serializatorem).
Istotnym problemem jest to, że wartości stanu są przechowywane w mapie skrótów i odwołują się do wartości klucza skrótu. Funkcje wprowadzają niezmienne argumenty, które są wartościami ze środowiska, mogą wywoływać inne takie funkcje i zapisywać stan do środowiska. Ale funkcje nie mogą odczytywać wartości ze środowiska (więc wewnętrzny kod funkcji nie jest zależny od kolejności zmian stanu i dlatego pozostaje deklaratywny w tym sensie). Jak to wpisać w Scali?
Klasa środowiska musi mieć przeciążoną metodę, która wprowadza taką funkcję do wywołania i wprowadza klucze skrótu argumentów funkcji. W ten sposób ta metoda może wywołać funkcję z niezbędnymi wartościami z mapy skrótów, bez zapewniania publicznego dostępu do odczytu wartości (w ten sposób zgodnie z wymaganiami, odmawiając funkcjom możliwości odczytu wartości ze środowiska).
Ale jeśli te klucze skrótu są ciągami lub całkowitymi wartościami skrótu, statyczne wpisywanie typu elementu mapy skrótów obejmuje Any lub AnyRef (kod mapy skrótów nie jest pokazany poniżej), a zatem może wystąpić niezgodność w czasie wykonywania, tj. Byłoby to możliwe aby umieścić dowolny typ wartości w mapie skrótów dla danego klucza mieszającego.
Chociaż nie testowałem następujących elementów, teoretycznie mogę uzyskać klucze skrótu z nazw klas w środowisku
classOf
uruchomieniowym, więc klucz skrótu jest nazwą klasy zamiast ciągu (przy użyciu odwrotnych apostrofów Scali do osadzenia ciągu w nazwie klasy).W ten sposób uzyskuje się bezpieczeństwo typu statycznego.
źródło
def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A
. Nie używalibyśmy kolekcji kluczy argumentów, ponieważ typy elementów byłyby podliczane (nieznane w czasie kompilacji) w typie kolekcji.