Os tipos de métodos dependentes, que antes eram um recurso experimental, agora foram ativados por padrão no porta-malas e, aparentemente, isso parece ter gerado algum entusiasmo na comunidade Scala.
À primeira vista, não é imediatamente óbvio para que isso poderia ser útil. Heiko Seeberger postou um exemplo simples de tipos de métodos dependentes aqui , que, como pode ser visto no comentário, podem ser facilmente reproduzidos com parâmetros de tipo em métodos. Portanto, esse não foi um exemplo muito convincente. (Talvez esteja faltando algo óbvio. Por favor, me corrija.)
Quais são alguns exemplos práticos e úteis de casos de uso para tipos de métodos dependentes, onde eles são claramente vantajosos sobre as alternativas?
Que coisas interessantes podemos fazer com eles que antes não eram possíveis / fáceis?
O que eles nos compram sobre os recursos existentes do sistema de tipos?
Além disso, os tipos de métodos dependentes são análogos ou inspiram-se em quaisquer recursos encontrados nos sistemas de tipos de outras linguagens de tipos avançadas, como Haskell, OCaml?
fonte
Respostas:
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 ,
É 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 porFileManager
/File
que, por sua vez, são refinadas porNetworkFileManager
/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,
Note-se que os meios de dependência caminho que o compilador irá garantir que o
testHash
etestDuplicates
métodos onNetworkFileManager
só pode ser chamado com argumentos que correspondem a ele, ou seja. é próprioRemoteFiles
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,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 ...
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.
fonte
def testHash4[R <: ResourceManager#BasicResource](rm: ResourceManager { type Resource = R }, r: R) = assert(r.hash == "9e47088d")
Suponho que isso possa ser considerado outra forma de tipos dependentes, no entanto.Em outro lugar, podemos garantir estaticamente que não estamos misturando nós de dois gráficos diferentes, por exemplo:
Obviamente, isso já funcionou se definido por dentro
Graph
, mas dizemos que não podemos modificarGraph
e estamos escrevendo uma extensão "pimp my library" para ela.Sobre a segunda pergunta: os tipos ativados por esse recurso são muito mais fracos que os tipos dependentes completos (consulte Programação dependente de tipos no Agda para obter uma amostra disso.) Acho que nunca vi uma analogia antes.
fonte
Esse novo recurso é necessário quando membros do tipo abstrato concreto são usados em vez de parâmetros de tipo . Quando parâmetros de tipo são usados, a dependência do tipo de polimorfismo da família pode ser expressa nas versões mais recentes e mais antigas do Scala, como no exemplo simplificado a seguir.
fonte
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}
etc.Estou desenvolvendo um modelo para a interoperabilidade de uma forma de programação declarativa com o estado ambiental. Os detalhes não são relevantes aqui (por exemplo, detalhes sobre retornos de chamada e semelhança conceitual com o modelo do ator combinado com um serializador).
O problema relevante é que os valores de estado são armazenados em um mapa de hash e referenciados por um valor de chave de hash. As funções inserem argumentos imutáveis que são valores do ambiente, podem chamar outras funções e gravar estado no ambiente. Mas as funções são não permissão para ler valores do ambiente (portanto, o código interno da função não depende da ordem das mudanças de estado e, portanto, permanece declarativo nesse sentido). Como digitar isso no Scala?
A classe de ambiente deve ter um método sobrecarregado que insira uma função para chamar e insira as chaves hash dos argumentos da função. Portanto, esse método pode chamar a função com os valores necessários no mapa de hash, sem fornecer acesso público de leitura aos valores (portanto, conforme necessário, negar às funções a capacidade de ler valores do ambiente).
Mas, se estas chaves de hash são cadeias ou valores hash inteiros, a tipagem estática do hash mapa elemento tipo engloba a qualquer ou AnyRef (código mapa hash não mostrado abaixo), e, assim, um desfasamento de tempo de execução poderia ocorrer, ou seja, seria possível para colocar qualquer tipo de valor em um mapa de hash para uma determinada chave de hash.
Embora eu não tenha testado o seguinte, em teoria eu posso obter as chaves de hash dos nomes das classes em tempo de execução
classOf
, portanto, uma chave de hash é um nome de classe em vez de uma string (usando os backticks de Scala para incorporar uma string em um nome de classe).Assim, a segurança do tipo estático é alcançada.
fonte
def callit[A](argkeys: Tuple[DependentHashKey,DependentHashKey])(func: Env => argkeys._0.ValueType => argkeys._1.ValueType => A): A
. Não usaríamos uma coleção de chaves de argumento, porque os tipos de elemento seriam incluídos (desconhecidos no momento da compilação) no tipo de coleção.