Estou trabalhando em um pequeno compilador de cálculo lambda que possui um sistema de inferência do tipo Hindley-Milner em funcionamento e agora também oferece suporte a vamos recursivos (não no código vinculado), que eu entendo que deve ser suficiente para torná-lo completo .
O problema agora é que não tenho idéia de como fazê-lo, ou se ele já os suporta e só preciso encontrar uma maneira de codificá-los. Eu gostaria de poder defini-los sem precisar adicionar novas regras ao sistema de tipos.
A maneira mais fácil de pensar em uma lista x
é como algo que é null
(ou a lista vazia) ou um par que contém uma x
e uma lista de x
. Mas, para fazer isso, preciso ser capaz de definir pares e ou 's, que acredito serem os tipos de produto e soma.
Parece que eu posso definir pares desta maneira:
pair = λabf.fab
first = λp.p(λab.a)
second = λp.p(λab.b)
Uma vez pair
que teria o tipo a -> (b -> ((a -> (b -> x)) -> x))
, depois de passar, digamos, an int
e a string
, produziria algo com o tipo (int -> (string -> x)) -> x
, que seria a representação de um par de int
e string
. O que me incomoda aqui é que, se isso representa um par, por que isso não é logicamente equivalente ou implica a proposição int and string
? No entanto, é equivalente a (((int and string) -> x) -> x)
, como se eu pudesse ter apenas tipos de produtos como parâmetros para funções. Esta respostaparece resolver esse problema, mas não tenho idéia do significado dos símbolos que ele usa. Além disso, se isso realmente não codifica um tipo de produto, há algo que eu possa fazer com os tipos de produtos que eu não poderia fazer com minha definição de pares acima (considerando que eu também posso definir n-tuplas da mesma maneira)? Caso contrário, isso não contradiz o fato de que você não pode expressar a conjunção (AFAIK) usando apenas implicação?
Além disso, e o tipo de soma? De alguma forma, posso codificá-lo usando apenas o tipo de função? Em caso afirmativo, isso seria suficiente para definir listas? Ou então, existe outra maneira de definir listas sem precisar estender meu sistema de tipos? E, se não, que mudanças eu precisaria fazer para manter o mais simples possível?
Lembre-se de que sou um programador de computadores, mas não um cientista da computação, nem um matemático e muito ruim em ler notações matemáticas.
Edit: Não tenho certeza de qual é o nome técnico do que implementei até agora, mas tudo o que tenho é basicamente o código que vinculei acima, que é um algoritmo de geração de restrição que usa as regras para aplicativos, abstrações e variáveis obtidas do algoritmo Hinley-Milner e, em seguida, um algoritmo de unificação que obtém o tipo principal. Por exemplo, a expressão \a.a
produzirá o tipo a -> a
e a expressão \a.(a a)
lançará um erro de verificação de ocorrência. Além disso, não existe exatamente uma let
regra, mas uma função que parece ter o mesmo efeito que permite definir funções globais recursivas como este pseudo-código:
GetTypeOfGlobalFunction(term, globalScope, nameOfFunction)
{
// Here 'globalScope' contains a list of name-value pair where every value is of class 'ClosedType',
// meaning their type will be cloned before unified in the unification algorithm so that they can be used polymorphically
tempType = new TypeVariable() // Assign a dummy type to `tempType`, say, type 'x'.
// The next line creates an scope with everything in 'globalScope' plus the 'nameOfFunction = tempType' name-value pair
tempScope = new Scope(globalScope, nameOfFunction, tempType)
type = TypeOfTerm(term, tempScope) // Calculate the type of the term
Unify(tempType, type)
return type
// After returning, the code outside will create a 'ClosedType' using the returned type and add it to the global scope.
}
O código basicamente obtém o tipo do termo como de costume, mas antes da unificação, ele adiciona o nome da função que está sendo definida com um tipo fictício no escopo do tipo, para que possa ser usado recursivamente dentro de si.
Edição 2: Acabei de perceber que também precisaria de tipos recursivos, que não tenho, para definir uma lista como eu quero.
let func = \x -> (func x)
), você obtém o que tenho.Respostas:
Pares
Essa codificação é a codificação da Igreja de pares. Técnicas semelhantes podem codificar booleanos, números inteiros, listas e outras estruturas de dados.
x:a; y:b
pair x y
(a -> b -> t) -> t
a
b
t
pair
t
pair
é um construtor para o tipo par efirst
esecond
são destruidores. (Essas são as mesmas palavras usadas na programação orientada a objetos; aqui as palavras têm um significado que está relacionado à interpretação lógica dos tipos e termos que não abordarei aqui.) Intuitivamente, os destruidores permitem acessar o que é no objeto e nos construtores pavimentam o caminho para o destruidor, assumindo como argumento uma função que eles aplicam às partes do objeto. Este princípio pode ser aplicado a outros tipos.Soma
A codificação da Igreja de uma união discriminada é essencialmente dual à codificação da Igreja de um par. Onde um par tem duas partes que devem ser reunidas e você pode optar por extrair uma ou outra, você pode construir a união de duas maneiras e, quando usá-la, precisa permitir as duas maneiras. Portanto, existem dois construtores e um único destruidor que recebe dois argumentos.
Deixe-me abreviar o tipo
(a->t) -> (b->t) -> t
comoSUM(a,b)(t)
. Então, os tipos de destruidores e construtores são:portanto
Listas
Para uma lista, aplique novamente o mesmo princípio. Uma lista cujos elementos têm o tipo
a
pode ser construída de duas maneiras: pode ser uma lista vazia ou pode ser um elemento (a cabeça) mais uma lista (a cauda). Comparado aos pares, há uma pequena reviravolta no que diz respeito aos destruidores: você não pode ter dois destruidores separadoshead
etail
porque eles funcionariam apenas em listas não vazias. Você precisa de um único destruidor, com dois argumentos, um dos quais é uma função de 0 argumentos (ou seja, um valor) para o caso nulo e o outro uma função de 2 argumentos para o caso de contras. Funções comois_empty
,head
etail
podem ser derivadas disso. Como no caso de somas, a lista é diretamente sua própria função destruidora.cons
cons
cons
Como você supõe, se você deseja definir um tipo que contém apenas listas homogêneas, precisa de tipos recursivos. Por quê? Vejamos o tipo de uma lista. Uma lista é codificada como uma função que recebe dois argumentos: o valor para retornar em listas vazias e a função para calcular o valor para retornar em uma célula de contras. Seja
a
o tipo de elemento,b
o tipo da lista ec
o tipo retornado pelo destruidor. O tipo de uma lista éTornar a lista homogênea é dizer que, se é uma célula contrária, a cauda deve ter o mesmo tipo que o todo, ou seja, acrescenta a restrição
O sistema do tipo Hindley-Milner pode ser estendido com esses tipos recursivos e, de fato, linguagens de programação práticas fazem isso. Linguagens de programação práticas tendem a desaprovar essas equações "nuas" e requerem um construtor de dados, mas esse não é um requisito intrínseco da teoria subjacente. A exigência de um construtor de dados simplifica a inferência de tipo e, na prática, tende a evitar a aceitação de funções que são realmente defeituosas, mas podem ser tipificadas com alguma restrição não intencional que causa um erro de tipo difícil de entender quando a função é usada. É por isso que, por exemplo, o OCaml aceita tipos recursivos não protegidos apenas com a
-rectypes
opção de compilador não padrão . Aqui estão as definições acima na sintaxe do OCaml, juntamente com uma definição de tipo para listas homogêneas usando a notação paraalias tipos recursiva :type_expression as 'a
significa que o tipotype_expression
é unificado com a variável'a
.Folds
Olhando para isso de forma um pouco mais geral, qual é a função que representa a estrutura de dados?
Em termos gerais, a estrutura de dados é representada como sua função de dobra . Este é um conceito geral para estruturas de dados: uma função fold é uma função de ordem superior que atravessa a estrutura de dados. Existe um sentido técnico em que a dobra é universal : todos os percursos "genéricos" da estrutura de dados podem ser expressos em termos de dobra. Que a estrutura de dados pode ser representada como sua função fold mostra isso: tudo o que você precisa saber sobre uma estrutura de dados é como percorrê-la, o restante é um detalhe de implementação.
fonte
t
e ignorar o argumento que deveria ser utilizado (a
eb
é exatamente o que(a and b) or t
está dizendo). E parece que eu teria o mesmo tipo de problema com somas. E também, sem tipos recursivos, não terei lista homogênea. Então, em poucas palavras, você está dizendo que devo adicionar regras de soma, produto e tipo recursivo para obter listas homogêneas?case (right y) f g → g y
no final da sua seção Sums ?Você pode representar tipos de soma como tipos de produto com tags e valores. Nesse caso, podemos trapacear um pouco e usar uma tag para representar nulo ou não, com a segunda tag representando o par cabeça / cauda.
Definimos booleanos da maneira usual:
Uma lista é então um par com o primeiro elemento como um booleano e o segundo elemento como um par de cabeça / cauda. Algumas funções básicas da lista:
fonte