Quais são alguns casos de uso atraentes para tipos de methods dependentes?

Os tipos de methods dependentes, que costumavam ser um recurso experimental antes, agora foram habilitados por padrão no tronco , e aparentemente isso parece ter criado algum entusiasmo na comunidade Scala.

À primeira vista, não é imediatamente óbvio para que isso poderia ser útil. Heiko Seeberger postou aqui um exemplo simples de tipos de methods dependentes, os quais, como pode ser visto no comentário, podem ser facilmente reproduzidos com parâmetros de tipo em methods. Então esse não foi um exemplo muito convincente. (Eu poderia estar faltando alguma coisa óbvia. Por favor, corrija-me se assim for.)

Quais são alguns exemplos práticos e úteis de casos de uso para tipos de methods dependentes em que eles são claramente vantajosos em relação às alternativas?

Que coisas interessantes podemos fazer com elas que não eram possíveis / fáceis antes?

O que eles nos compram sobre os resources existentes do sistema de tipos?

Além disso, os tipos de methods dependentes são análogos ou inspirados em qualquer recurso encontrado nos sistemas de tipos de outras linguagens tipificadas avançadas, como Haskell, OCaml?

Mais ou menos qualquer uso de tipos de membros (ou seja, nesteds) pode dar origem a uma necessidade de tipos de methods dependentes. Em particular, eu mantenho que, sem tipos de methods dependentes, o padrão clássico de bolo está mais próximo de ser um antipadrão.

Então qual é o problema? Tipos nesteds no Scala são dependentes de sua instância delimitadora. Consequentemente, na ausência de tipos de methods 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 pesadelosamente rígidos e difíceis de refatorar.

Vou ilustrar isso com um exercício que dou 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 bolo: temos uma família de abstrações que são gradualmente refinadas através de uma hierarquia ( ResourceManager / Resource são refinados pelo FileManager / File que por sua vez são refinados pelo NetworkFileManager / RemoteFile ). É um exemplo de brinquedo, mas o padrão é real: é usado em todo o compilador Scala e foi usado extensivamente 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) 

Observe que a dependência de caminho significa que o compilador garantirá que os methods testDuplicates e testDuplicates no NetworkFileManager possam ser chamados apenas com argumentos que correspondam a ele, isto é. é próprio RemoteFiles e nada mais.

Isso é inegavelmente uma propriedade desejável, mas suponha que desejamos mover esse código de teste para um arquivo de origem diferente? Com tipos de methods dependentes é trivialmente fácil redefinir esses methods fora da hierarquia do ResourceManager ,

 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 methods dependentes aqui: o tipo do segundo argumento ( rm.Resource ) depende do valor do primeiro argumento ( rm ).

É possível fazer isso sem tipos de methods dependentes, mas é extremamente complicado e o mecanismo não é muito intuitivo: estou ensinando este curso há quase dois anos e, nesse período, ninguém apresentou uma solução de trabalho espontânea.

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 curto período de tempo lutando com ele, você provavelmente descobrirá por que eu (ou talvez tenha sido David MacIver, não conseguimos lembrar qual de nós cunhou o termo) chamamos isso de Bakery of Doom.

Edit: consenso é que Bakery of Doom foi coinage de David MacIver ...

Para o bônus: a forma de tipos dependentes do Scala em geral (e tipos de methods dependentes como parte dele) foi inspirada na linguagem de programação Beta ... eles surgem naturalmente da semântica de aninhamento consistente da Beta. Eu não conheço nenhuma outra linguagem de programação mainstream que tenha tipos dependentes nesta forma. Línguas 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, ao contrário do Scala, não têm subtipagem.

 trait Graph { type Node type Edge def end1(e: Edge): Node def end2(e: Edge): Node def nodes: Set[Node] def edges: Set[Edge] } 

Em outro lugar, podemos garantir estaticamente que não estamos misturando nós de dois charts diferentes, por exemplo:

 def shortestPath(g: Graph)(n1: g.Node, n2: g.Node) = ... 

É claro que isso já funcionou se definido dentro do Graph , mas digamos que não podemos modificar o Graph e estamos escrevendo uma extensão “pimp my library” para ele.

Sobre a segunda pergunta: os tipos habilitados por esse recurso são muito mais fracos do que os tipos dependentes completos (veja Dependently Typed Programming em Agda para ter uma ideia disso). Eu não acho que tenha visto uma analogia antes.

Esse novo recurso é necessário quando membros de 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 polymorphism familiar pode ser expressa nas versões mais recentes e antigas do Scala, como no exemplo simplificado a seguir.

 trait C[A] def f[M](a: C[M], b: M) = b class C1 extends C[Int] class C2 extends C[String] f(new C1, 0) res0: Int = 0 f(new C2, "") res1: java.lang.String = f(new C1, "") error: type mismatch; found : C1 required: C[Any] f(new C1, "") ^ 

Estou desenvolvendo um modelo para a interopção de uma forma de programação declarativa com estado ambiental. Os detalhes não são relevantes aqui (por exemplo, detalhes sobre retornos de chamada e semelhança conceitual com o modelo 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 input input immutable arguments que são valores do ambiente, podem chamar outras funções e gravar o estado no ambiente. Mas as funções não podem ler valores do ambiente (portanto, o código interno da function não depende da ordem das mudanças de estado e, portanto, permanece declarativo nesse sentido). Como digitar isso no Scala?

A class de ambiente deve ter um método sobrecarregado que insira tal function para chamar e insira as chaves de hash dos argumentos da function. Portanto, esse método pode chamar a function com os valores necessários do mapa de hash, sem fornecer access público de leitura aos valores (assim, conforme necessário, negando às funções a capacidade de ler valores do ambiente).

Mas se essas chaves hash são seqüências de caracteres ou valores inteiros de hash, a tipagem estática do tipo de elemento hash map subsumes para Any ou AnyRef (código de mapa hash não mostrado abaixo) e, portanto, uma incompatibilidade de tempo de execução pode ocorrer, ou seja, seria possível para colocar qualquer tipo de valor em um mapa de hash para uma determinada chave hash.

 trait Env { ... def callit[A](func: Env => Any => A, arg1key: String): A def callit[A](func: Env => Any => Any => A, arg1key: String, arg2key: String): A } 

Embora eu não tenha testado o seguinte, em teoria eu posso obter as chaves de hash dos nomes das classs em tempo de execução usando classOf , então uma chave hash é um nome de class em vez de uma string (usando os backticks do Scala para embutir uma string em um nome de class) .

 trait DependentHashKey { type ValueType } trait `the hash key string` extends DependentHashKey { type ValueType <: SomeType } 

Assim, a segurança do tipo estático é alcançada.

 def callit[A](arg1key: DependentHashKey)(func: Env => arg1key.ValueType => A): A 
Intereting Posts