Suponha que eu queira escrever uma biblioteca que lide com vetores e matrizes. É possível agrupar as dimensões nos tipos, para que operações de dimensões incompatíveis gerem um erro no tempo de compilação?
Por exemplo, eu gostaria que a assinatura do produto escalar fosse algo como
dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a
onde o d
tipo contém um único valor inteiro (representando a dimensão desses vetores).
Suponho que isso possa ser feito definindo (manualmente) um tipo separado para cada número inteiro e agrupando-os em uma classe de tipo chamada VecDim
. Existe algum mecanismo para "gerar" esses tipos?
Ou talvez alguma maneira melhor / mais simples de conseguir a mesma coisa?
haskell
type-systems
type-safety
mitchus
fonte
fonte
tensor
biblioteca está conseguindo isso muito elegante usando um recursivadata
definição: noaxiom.org/tensor-documentation#ordinalsRespostas:
Para expandir a resposta de @ KarlBielefeldt, aqui está um exemplo completo de como implementar Vectors - listas com um número estaticamente conhecido de elementos - em Haskell. Segure seu chapéu ...
Como você pode ver na longa lista de
LANGUAGE
diretivas, isso funcionará apenas com uma versão recente do GHC.Precisamos de uma maneira de representar comprimentos dentro do sistema de tipos. Por definição, um número natural é zero (
Z
) ou é o sucessor de outro número natural (S n
). Então, por exemplo, o número 3 seria escritoS (S (S Z))
.Com a extensão DataKinds , esta
data
declaração apresenta um tipo chamadoNat
e dois construtores de tipo chamadosS
eZ
- em outras palavras, temos números naturais no nível de tipo . Observe que os tiposS
eZ
não têm nenhum valor de membro - apenas tipos do tipo*
são habitados por valores.Agora, apresentamos um GADT representando vetores com um comprimento conhecido. Observe a assinatura de tipo:
Vec
requer um tipo de tipoNat
(ou seja, umZ
ou umS
tipo) para representar seu comprimento.A definição de vetores é semelhante à de listas vinculadas, com algumas informações extras no nível de tipo sobre seu comprimento. Um vetor é ou
VNil
, nesse caso, tem um comprimento deZ
(ero), ou é umaVCons
célula que adiciona um item a outro vetor; nesse caso, seu comprimento é um a mais que o outro vetor (S n
). Note-se que não há nenhum argumento do construtor do tipon
. É usado apenas no momento da compilação para rastrear comprimentos e será apagado antes que o compilador gere o código da máquina.Definimos um tipo de vetor que carrega conhecimento estático de seu comprimento. Vamos consultar o tipo de alguns
Vec
s para ter uma ideia de como eles funcionam:O produto escalar continua da mesma maneira que faria para uma lista:
vap
, que 'zippily' aplica um vetor de funções a um vetor de argumentos, éVec
o aplicativo<*>
; Eu não o coloquei em umaApplicative
instância porque fica confuso . Observe também que estou usando ofoldr
da instância gerada pelo compilador deFoldable
.Vamos tentar:
Ótimo! Você recebe um erro em tempo de compilação ao tentar
dot
vetores cujos comprimentos não coincidem.Aqui está uma tentativa de uma função para concatenar vetores juntos:
O comprimento do vetor de saída seria a soma dos comprimentos dos dois vetores de entrada. Precisamos ensinar ao verificador de tipos como adicionar
Nat
s juntos. Para isso, usamos uma função de nível de tipo :Esta
type family
declaração introduz uma função nos tipos chamados:+:
- em outras palavras, é uma receita para o verificador de tipos calcular a soma de dois números naturais. É definido recursivamente - sempre que o operando esquerdo for maior queZ
ero, adicionamos um à saída e o reduzimos em um na chamada recursiva. (É um bom exercício escrever uma função de tipo que multiplica doisNat
s.) Agora podemos fazer a+++
compilação:Veja como você o usa:
Até agora, tão simples. E quando queremos fazer o oposto da concatenação e dividir um vetor em dois? Os comprimentos dos vetores de saída dependem do valor de tempo de execução dos argumentos. Gostaríamos de escrever algo como isto:
mas infelizmente Haskell não nos deixa fazer isso. Permitir que o valor do
n
argumento apareça no tipo de retorno (geralmente chamado de função dependente ou tipo pi ) exigiria tipos dependentes de "espectro completo", enquantoDataKinds
apenas nos fornece construtores de tipo promovidos. Em outras palavras, os construtores de tipoS
eZ
não aparecem no nível do valor. Teremos que aceitar valores singleton para uma representação em tempo de execução de um determinadoNat
. *Para um determinado tipo
n
(com tipoNat
), há precisamente um termo do tipoNatty n
. Podemos usar o valor singleton como uma testemunha em tempo de execução paran
: aprender sobre aNatty
nos ensina sobre isson
e vice-versa.Vamos dar uma volta:
No primeiro exemplo, dividimos com êxito um vetor de três elementos na posição 2; obtivemos um erro de tipo quando tentamos dividir um vetor em uma posição após o final. Singletons são a técnica padrão para fazer um tipo depender de um valor em Haskell.
* A
singletons
biblioteca contém alguns auxiliares do Template Haskell para gerar valores singleton comoNatty
para você.Último exemplo. E quando você não conhece estaticamente a dimensionalidade do seu vetor? Por exemplo, e se estivermos tentando criar um vetor a partir de dados de tempo de execução na forma de uma lista? Você precisa do tipo do vetor para depender do comprimento da lista de entrada. Em outras palavras, não podemos usar
foldr VCons VNil
para construir um vetor porque o tipo do vetor de saída muda a cada iteração da dobra. Precisamos manter o comprimento do vetor em segredo do compilador.AVec
é um tipo existencial : a variável typen
não aparece no tipo de retorno doAVec
construtor de dados. Estamos usando-o para simular um par dependente :fromList
não podemos dizer estaticamente o comprimento do vetor, mas ele pode retornar algo com o qual você pode fazer a correspondência de padrões para aprender o comprimento do vetor -Natty n
no primeiro elemento da tupla . Como Conor McBride coloca em uma resposta relacionada , "Você olha para uma coisa e, ao fazê-lo, aprende sobre outra".Essa é uma técnica comum para tipos existencialmente quantificados. Como você não pode realmente fazer nada com dados para os quais você não conhece o tipo - tente escrever uma função de
data Something = forall a. Sth a
- existenciais geralmente são agrupados com evidências do GADT, que permitem recuperar o tipo original executando testes de correspondência de padrões. Outros padrões comuns para existenciais incluem empacotar funções para processar seu tipo (data AWayToGetTo b = forall a. HeresHow a (a -> b)
), que é uma maneira elegante de executar módulos de primeira classe ou criar um dicionário de classe de tipo (data AnOrd = forall a. Ord a => AnOrd a
) que pode ajudar a emular o polimorfismo de subtipo.Pares dependentes são úteis sempre que as propriedades estáticas dos dados dependem de informações dinâmicas não disponíveis no momento da compilação. Aqui está
filter
para vetores:A
dot
doisAVec
s, precisamos provar ao GHC que seus comprimentos são iguais.Data.Type.Equality
define um GADT que só pode ser construído quando seus argumentos de tipo são os mesmos:Quando você combina com padrões
Refl
, o GHC sabe dissoa ~ b
. Existem também algumas funções para ajudá-lo a trabalhar com esse tipo: usaremosgcastWith
para converter entre tipos equivalentes eTestEquality
determinar se doisNatty
s são iguais.Para testar a igualdade de dois
Natty
s, precisaremos fazer uso do fato de que, se dois números forem iguais, seus sucessores também serão iguais (:~:
é congruente ao longoS
):A correspondência de padrões no
Refl
lado esquerdo permite que o GHC saiba disson ~ m
. Com esse conhecimento, é trivial queS n ~ S m
, portanto, o GHC nos permita devolver um novoRefl
imediatamente.Agora podemos escrever uma instância de
TestEquality
por recursão direta. Se ambos os números são zero, eles são iguais. Se ambos os números tiverem predecessores, eles serão iguais se os predecessores forem iguais. (Se não forem iguais, basta retornarNothing
.)Agora podemos juntar as peças em
dot
um par deAVec
s de comprimento desconhecido.Primeiro, a correspondência de padrões no
AVec
construtor para obter uma representação em tempo de execução dos comprimentos dos vetores. Agora usetestEquality
para determinar se esses comprimentos são iguais. Se forem, teremosJust Refl
;gcastWith
usará essa prova de igualdade para garantir quedot u v
seja bem digitada, descarregando suan ~ m
suposição implícita .Observe que, como um vetor sem conhecimento estático de seu comprimento é basicamente uma lista, reimplementamos efetivamente a versão da lista
dot :: Num a => [a] -> [a] -> Maybe a
. A diferença é que esta versão é implementada em termos dos vetoresdot
. Aqui está o ponto: antes que o verificador de tipos permita que você liguedot
, você deve ter testado se as listas de entrada têm o mesmo tamanho usandotestEquality
. Estou propenso a obterif
instruções erradas, mas não em um ambiente de tipo dependente!Não é possível evitar o uso de wrappers existenciais nas bordas do seu sistema, quando você está lidando com dados de tempo de execução, mas pode usar tipos dependentes em qualquer lugar dentro do sistema e manter os wrappers existenciais nas bordas ao executar a validação de entrada.
Como
Nothing
não é muito informativo, você pode refinar ainda mais o tipo dedot'
para retornar uma prova de que os comprimentos não são iguais (na forma de evidência de que sua diferença não é 0) no caso de falha. Isso é bastante semelhante à técnica padrão de Haskell de usarEither String a
para possivelmente retornar uma mensagem de erro, embora um termo de prova seja muito mais útil em termos computacionais do que uma string!Assim, termina esse tour de algumas das técnicas comuns na programação Haskell de tipo dependente. Programar com tipos como este em Haskell é muito legal, mas muito estranho ao mesmo tempo. Dividir todos os seus dados dependentes em várias representações que significam a mesma coisa -
Nat
o tipo,Nat
o tipo,Natty n
o singleton - é realmente bastante complicado, apesar da existência de geradores de código para ajudar no clichê. Atualmente, também existem limitações sobre o que pode ser promovido para o nível de tipo. É tentador embora! A mente desconfia das possibilidades - na literatura há exemplos em Haskell deprintf
interfaces de banco de dados fortemente tipadas , mecanismos de layout de interface do usuário ...Se você quiser ler mais, há um crescente corpo de literatura sobre Haskell de tipo dependente, publicado e em sites como o Stack Overflow. Um bom ponto de partida é o artigo do Hasochism - o artigo passa por esse exemplo (entre outros), discutindo as partes dolorosas com mais detalhes. O artigo de Singletons demonstra a técnica de valores singleton (como
Natty
). Para obter mais informações sobre digitação dependente em geral, o tutorial do Agda é um bom ponto de partida; Além disso, Idris é uma linguagem em desenvolvimento projetada (aproximadamente) para ser "Haskell com tipos dependentes".fonte
Isso é chamado de digitação dependente . Depois de saber o nome, você poderá encontrar mais informações sobre o que você poderia desejar. Há também uma linguagem interessante, semelhante a haskell, chamada Idris, que as usa nativamente. Seu autor fez algumas apresentações realmente boas sobre o tópico que você pode encontrar no youtube.
fonte
newtype Vec2 a = V2 (a,a)
,newtype Vec3 a = V3 (a,a,a)
e assim por diante, mas não é isso que o que faz a pergunta.)Pi (x : A). B
qual é uma função deA
paraB x
ondex
está o argumento da função. Aqui, o tipo de retorno da função depende da expressão fornecida como argumento. No entanto, tudo isso pode ser apagado, é tempo de compilação única