Mais ou menos, qualquer uso de tipos de membros (ou seja, aninhados) pode gerar uma necessidade de tipos de métodos dependentes. Em particular, sustento que, sem tipos de métodos dependentes, o padrão clássico de bolos está mais próximo de ser um anti-padrão.
Então qual é o problema? Os tipos aninhados no Scala dependem de sua instância anexa. Consequentemente, na ausência de tipos de métodos dependentes, as tentativas de usá-los fora dessa instância podem ser frustrantemente difíceis. Isso pode transformar projetos que inicialmente parecem elegantes e atraentes em monstruosidades que são terrivelmente rígidas e difíceis de refatorar.
Ilustrarei isso com um exercício que faço durante meu curso de treinamento 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
}
É um exemplo do padrão clássico de bolos: temos uma família de abstrações que são gradualmente refinadas através de uma hierarquia ( ResourceManager
/ Resource
são refinadas por FileManager
/ File
que, por sua vez, são refinadas por NetworkFileManager
/ RemoteFile
). É um exemplo de brinquedo, mas o padrão é real: é usado em todo o compilador Scala e foi amplamente utilizado no plug-in Scala Eclipse.
Aqui está um exemplo da abstração em uso,
val nfm = new NetworkFileManager
val rf : nfm.Resource = nfm.create
nfm.testHash(rf)
nfm.testDuplicates(rf)
Note-se que os meios de dependência caminho que o compilador irá garantir que o testHash
e testDuplicates
métodos on NetworkFileManager
só pode ser chamado com argumentos que correspondem a ele, ou seja. é próprio RemoteFiles
e nada mais.
É inegavelmente uma propriedade desejável, mas suponha que desejássemos mover esse código de teste para um arquivo de origem diferente? Com tipos de métodos dependentes, é fácil redefinir esses métodos fora da ResourceManager
hierarquia,
def testHash4(rm : ResourceManager)(r : rm.Resource) =
assert(r.hash == "9e47088d")
def testDuplicates4(rm : ResourceManager)(r : rm.Resource) =
assert(r.duplicates(r))
Observe os usos dos tipos de métodos dependentes aqui: o tipo do segundo argumento ( rm.Resource
) depende do valor do primeiro argumento ( rm
).
É possível fazer isso sem tipos de métodos dependentes, mas é extremamente incômodo e o mecanismo é pouco intuitivo: eu ensino este curso há quase dois anos e, nesse período, ninguém encontrou uma solução que funcionasse sem ser solicitada.
Experimente você mesmo ...
// 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)
Depois de um breve período de dificuldade, você provavelmente descobrirá por que eu (ou talvez tenha sido David MacIver, não podemos lembrar qual de nós cunhou o termo) a chamo de Padaria da Perdição.
Edit: consenso é que Bakery of Doom foi cunhagem de David MacIver ...
Para o bônus: a forma de tipos dependentes de Scala em geral (e tipos de métodos dependentes como parte dela) foi inspirada pela linguagem de programação Beta ... elas surgem naturalmente da semântica consistente de nidificação de Beta. Eu não conheço nenhuma outra linguagem de programação, mesmo que levemente convencional, que tenha tipos dependentes neste formulário. Idiomas como Coq, Cayenne, Epigram e Agda têm uma forma diferente de digitação dependente, que de certa forma é mais geral, mas que difere significativamente por fazer parte de sistemas de tipos que, diferentemente do Scala, não possuem subtipo.