Verificação de tipos e tipos recursivos (Escrevendo o combinador Y em Haskell / Ocaml)

21

Ao explicar o combinador Y no contexto de Haskell, geralmente é observado que a implementação direta não verifica o tipo no Haskell devido ao seu tipo recursivo.

Por exemplo, do Rosettacode :

The obvious definition of the Y combinator in Haskell canot be used
because it contains an infinite recursive type (a = a -> b). Defining
a data type (Mu) allows this recursion to be broken.

newtype Mu a = Roll { unroll :: Mu a -> a }

fix :: (a -> a) -> a
fix = \f -> (\x -> f (unroll x x)) $ Roll (\x -> f (unroll x x))

E, de fato, a definição "óbvia" não digita check:

λ> let fix f g = (\x -> \a -> f (x x) a) (\x -> \a -> f (x x) a) g

<interactive>:10:33:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    Expected type: t2 -> t0 -> t1
      Actual type: (t2 -> t0 -> t1) -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a

<interactive>:10:57:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a
(0.01 secs, 1033328 bytes)

A mesma limitação existe em Ocaml:

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
Error: This expression has type 'a -> 'b but an expression was expected of type 'a                                    
       The type variable 'a occurs inside 'a -> 'b

No entanto, no Ocaml, é possível permitir tipos recursivos passando no -rectypescomutador:

   -rectypes
          Allow  arbitrary  recursive  types  during type-checking.  By default, only recursive
          types where the recursion goes through an object type are supported.

Ao usar -rectypes, tudo funciona:

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
val fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
utop # let fact_improver partial n = if n = 0 then 1 else n*partial (n-1);;
val fact_improver : (int -> int) -> int -> int = <fun>
utop # (fix fact_improver) 5;;
- : int = 120

Sendo curioso sobre sistemas de tipos e inferência de tipos, isso levanta algumas questões que ainda não sou capaz de responder.

  • Primeiro, como o verificador de tipos cria o tipo t2 = t2 -> t0 -> t1? Tendo inventado esse tipo, acho que o problema é que o type ( t2) se refere a si mesmo no lado direito?
  • Segundo, e talvez o mais interessante, qual o motivo dos sistemas do tipo Haskell / Ocaml não permitirem isso? Eu acho que não é uma boa razão desde Ocaml também não vai permitir isso por padrão, mesmo se ele pode lidar com tipos recursiva se dado a -rectypeschave.

Se esses são realmente grandes tópicos, eu gostaria de sugestões para literatura relevante.

beta
fonte

Respostas:

16

Primeiro, o erro do GHC,

O GHC está tentando unificar algumas restrições com x, primeiro, nós o usamos como uma função para

x :: a -> b

Em seguida, usamos como um valor para essa função

x :: a

E, finalmente, unificamos com a expressão de argumento original

x :: (a -> b) -> c -> d

Agora x xse torna uma tentativa de unificar t2 -> t1 -> t0, no entanto, não podemos unificar isso, pois isso exigiria a unificação t2, o primeiro argumento de x, com x. Daí a nossa mensagem de erro.

Em seguida, por que não tipos recursivos gerais. Bem, o primeiro ponto que vale a pena notar é a diferença entre os tipos equi e iso recursivo,

  • equi-recursivo é o que você espera mu X . Typeé exatamente equivalente a expandi-lo ou dobrá-lo arbitrariamente.
  • tipos iso-recursivos fornecem um par de operadores folde unfoldque dobram e desdobram as definições recursivas de tipos.

Agora tipos equi-recursivos parecem ideais, mas são absurdamente difíceis de acertar em sistemas de tipos complexos. Na verdade, pode tornar a verificação de tipo indecidível. Não estou familiarizado com todos os detalhes do sistema de tipos do OCaml, mas os tipos totalmente equirecursivos no Haskell podem fazer com que o datilógrafo faça um loop arbitrariamente tentando unificar tipos; por padrão, Haskell garante que a verificação de tipos seja encerrada. Além disso, em Haskell, os sinônimos de tipo são burros, os tipos recursivos mais úteis seriam definidos como type T = T -> (), no entanto, são alinhados quase imediatamente em Haskell, mas você não pode alinhar um tipo recursivo, é infinito! Portanto, os tipos recursivos em Haskell exigiriam uma grande revisão sobre como os sinônimos são manipulados, provavelmente não valendo o esforço de colocar mesmo como uma extensão de idioma.

Tipos iso-recursivos são um pouco trabalhosos, é mais ou menos necessário dizer explicitamente ao verificador de tipos como dobrar e desdobrar seus tipos, tornando seus programas mais complexos de ler e escrever.

No entanto, isso é muito semelhante ao que você está fazendo com seu Mutipo. Rollé dobrado e unrolldesdobrado. Então, na verdade, temos tipos iso-recursivos incorporados. No entanto, tipos equi-recursivos são muito complexos, de modo que sistemas como OCaml e Haskell forçam você a passar recorrências pelos pontos de fixação no nível de tipo.

Agora, se isso lhe interessa, recomendo Tipos e linguagens de programação. Minha cópia está aberta no meu colo enquanto escrevo para garantir a terminologia correta :)

Daniel Gratzer
fonte
Em particular o capítulo 21 fornece uma boa intuição para a indução, coinduction e tipos recursivos
Daniel Gratzer
Obrigado! Isso é realmente fascinante. Atualmente, estou lendo a TAPL e fico feliz em saber que isso será abordado mais adiante neste livro.
beta
@beta Sim, TAPL e seu irmão mais velho, Tópicos Avançados em Tipos e Linguagens de Programação, são recursos maravilhosos.
Daniel Gratzer #
2

No OCaml, você precisa passar -rectypescomo parâmetro para o compilador (ou inserir #rectypes;;no nível superior). Grosso modo, isso desativará "ocorre verificação" durante a unificação. A situação The type variable 'a occurs inside 'a -> 'bnão será mais um problema. O sistema de tipos ainda estará "correto" (som etc.), as infinitas árvores que surgem como tipos são às vezes chamadas de "árvores racionais". O sistema de tipos fica mais fraco, ou seja, torna-se impossível detectar alguns erros do programador.

Veja minha palestra sobre lambda-calculus (começando no slide 27) para obter mais informações sobre operadores de fixpoint com exemplos no OCaml.

lukstafi
fonte