Tipi di controllo e tipi ricorsivi (Scrivere il combinatore Y in Haskell / Ocaml)


21

Quando si spiega il combinatore Y nel contesto di Haskell, di solito si nota che l'implementazione diretta non controllerà il tipo in Haskell a causa del suo tipo ricorsivo.

Ad esempio, da 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 in effetti, la definizione "ovvia" non 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)

La stessa limitazione esiste in 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

Tuttavia, in Ocaml, si possono consentire tipi ricorsivi passando nello -rectypesswitch:

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

Usando -rectypes, tutto funziona:

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

Essere curiosi di sistemi di tipo e inferenza di tipo, questo solleva alcune domande a cui non sono ancora in grado di rispondere.

  • Innanzitutto, come viene eseguito il controllo del tipo con il tipo t2 = t2 -> t0 -> t1? Avendo escogitato quel tipo, immagino che il problema sia che type ( t2) si riferisce a se stesso sul lato destro?
  • Secondo, e forse il più interessante, qual è la ragione per cui i sistemi di tipo Haskell / Ocaml non lo consentono? Immagino che ci sia una buona ragione dal momento che anche Ocaml non lo consentirà per impostazione predefinita anche se può gestire tipi ricorsivi se gli viene dato lo -rectypesswitch.

Se questi sono argomenti davvero importanti, apprezzerei i suggerimenti per la letteratura pertinente.

Risposte:


16

Innanzitutto, l'errore GHC,

GHC sta tentando di unificare alcuni vincoli con x, in primo luogo, lo usiamo come una funzione

x :: a -> b

Successivamente lo usiamo come valore per quella funzione

x :: a

E infine lo uniamo con l'espressione dell'argomento originale così

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

Ora x xdiventa un tentativo di unificare t2 -> t1 -> t0, tuttavia, non possiamo unificare questo poiché richiederebbe l'unificazione t2, il primo argomento di x, con x. Da qui il nostro messaggio di errore.

Quindi, perché non tipi ricorsivi generali. Bene, il primo punto degno di nota è la differenza tra tipi equi e iso ricorsivi,

  • equi-ricorsivi sono quelli che ti aspetteresti mu X . Typeequivale esattamente ad espanderli o piegarli arbitrariamente.
  • i tipi iso-ricorsivi forniscono una coppia di operatori folde unfoldche piegano e spiegano le definizioni ricorsive dei tipi.

Ora i tipi equi ricorsivi sembrano ideali, ma sono assurdamente difficili da ottenere proprio nei sistemi di tipi complessi. Può effettivamente rendere indecidibile il controllo del tipo. Non ho familiarità con ogni dettaglio del sistema di tipi di OCaml, ma i tipi completamente equirecursivi in ​​Haskell possono far sì che il typechecker esegua un ciclo arbitrario nel tentativo di unificare i tipi, per impostazione predefinita, Haskell si assicura che il controllo del tipo termina. Inoltre, in Haskell, i sinonimi dei tipi sono stupidi, i tipi ricorsivi più utili verrebbero definiti come type T = T -> (), tuttavia vengono incorporati quasi immediatamente in Haskell, ma non è possibile incorporare un tipo ricorsivo, è infinito! Pertanto, i tipi ricorsivi in ​​Haskell richiederebbero un'enorme revisione del modo in cui vengono gestiti i sinonimi, probabilmente non vale la pena metterlo anche come estensione del linguaggio.

I tipi iso-ricorsivi sono un po 'una seccatura da usare, più o meno devi dire esplicitamente al controllo del tipo come piegare e aprire i tuoi tipi, rendendo i tuoi programmi più complessi da leggere e scrivere.

Tuttavia, questo è molto simile a quello che stai facendo con il tuo Mutipo. Rollè fold ed unrollè spiegato. Quindi, in realtà, abbiamo tipi iso-ricorsivi integrati. Tuttavia, i tipi equi-ricorsivi sono semplicemente troppo complessi, quindi sistemi come OCaml e Haskell ti costringono a passare ricorrenze attraverso punti fissi a livello di tipo.

Ora, se questo ti interessa, consiglierei Tipi e Linguaggi di programmazione. La mia copia è aperta in grembo mentre scrivo questo per essere sicuro di avere la terminologia giusta :)


In particolare il capitolo 21 fornisce una buona intuizione per l'induzione, la coinduzione e i tipi ricorsivi
Daniel Gratzer,

Grazie! Questo è davvero affascinante. Attualmente sto leggendo TAPL e sono felice di sapere che questo sarà trattato più avanti nel libro.
beta

@beta Sì, TAPL ed il suo fratello maggiore, Argomenti avanzati in Tipi e Linguaggi di programmazione, sono risorse meravigliose.
Daniel Gratzer,

2

In OCaml, devi passare -rectypescome parametro al compilatore (o inserire #rectypes;;il livello superiore). In parole povere, questo disattiverà "verifica verifica" durante l'unificazione. La situazione The type variable 'a occurs inside 'a -> 'bnon sarà più un problema. Il sistema dei tipi sarà comunque "corretto" (suono, ecc.), Gli alberi infiniti che sorgono mentre i tipi sono talvolta chiamati "alberi razionali". Il sistema dei tipi si indebolisce, cioè diventa impossibile rilevare alcuni errori del programmatore.

Vedi la mia lezione su lambda-calcolo (a partire dalla diapositiva 27) per ulteriori informazioni sugli operatori del fixpoint con esempi in OCaml.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.