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 ,
trait ResourceManager {
type Resource <: BasicResource
trait BasicResource {
def hash : String
def duplicates(r : Resource) : Boolean
}
def create : Resource
// Test methods: exercise is to move them outside ResourceManager
def testHash(r : Resource) = assert(r.hash == "9e47088d")
def testDuplicates(r : Resource) = assert(r.duplicates(r))
}
trait FileManager extends ResourceManager {
type Resource <: File
trait File extends BasicResource {
def local : Boolean
}
override def create : Resource
}
class NetworkFileManager extends FileManager {
type Resource = RemoteFile
class RemoteFile extends File {
def local = false
def hash = "9e47088d"
def duplicates(r : Resource) = (local == r.local) && (hash == r.hash)
}
override def create : Resource = new RemoteFile
}
To przykład klasycznego wzoru ciasta: mamy rodzinę abstrakcji, które są stopniowo udoskonalane przez hierarchię ( ResourceManager
/ Resource
są udoskonalane przez FileManager
/ File
które z kolei są udoskonalane przez NetworkFileManager
/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,
val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)
Zauważ, że zależność od ścieżki oznacza, że kompilator zagwarantuje, że metody testHash
i testDuplicates
on NetworkFileManager
można wywołać tylko z argumentami, które im odpowiadają, tj. jest własny RemoteFiles
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ą,
def testHash4(rm : ResourceManager)(r : rm.Resource) =
assert(r.hash == "9e47088d")
def testDuplicates4(rm : ResourceManager)(r : rm.Resource) =
assert(r.duplicates(r))
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 ...
// Reimplement the testHash and testDuplicates methods outside
// the ResourceManager hierarchy without using dependent method types
def testHash // TODO ...
def testDuplicates // TODO ...
testHash(rf)
testDuplicates(rf)
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.