Rappresentazione di variabili associate con una funzione dagli usi ai leganti


11

Il problema di rappresentare le variabili associate nella sintassi, e in particolare quello della sostituzione che evita la cattura, è ben noto e presenta una serie di soluzioni: variabili nominate con equivalenza alfa, indici di de Bruijn, insensatezza locale, insiemi nominali, ecc.

Ma sembra esserci un altro approccio abbastanza ovvio, che tuttavia non ho visto usato da nessuna parte. Vale a dire, nella sintassi di base abbiamo solo un termine "variabile", scritto dire , e quindi separatamente diamo una funzione che mappa ogni variabile a un legante nel cui ambito si trova. Quindi un -term comeλ

λX.(λy.Xy)

sarebbe scritto λ.(λ.) e la funzione associerebbe il primo al primo λ e il secondo al secondo λ . Quindi è un po 'come gli indici de Bruijn, solo invece di dover "contare λ s" quando si esce dal termine per trovare il raccoglitore corrispondente, si valuta solo una funzione. (Se rappresento questo come una struttura di dati in un'implementazione, penserei di dotare ogni oggetto a termine variabile con un semplice puntatore / riferimento al corrispondente oggetto a termine legante.)

Ovviamente questo non è sensato per la scrittura della sintassi su una pagina da leggere per gli umani, ma nemmeno gli indici de Bruijn. Mi sembra che abbia perfettamente senso matematicamente, e in particolare rende molto semplice la sostituzione che evita la cattura: basta inserire il termine che si sta sostituendo e prendere l'unione delle funzioni di legame. È vero che non ha una nozione di "variabile libera", ma poi (di nuovo) nemmeno gli indici de Bruijn; in entrambi i casi, un termine contenente variabili libere è rappresentato da un termine con un elenco di raccoglitori "contestuali" in primo piano.

Mi sto perdendo qualcosa e c'è qualche motivo per cui questa rappresentazione non funziona? Ci sono problemi che lo rendono molto peggio degli altri che non vale la pena considerare? (L'unico problema a cui riesco a pensare in questo momento è che l'insieme di termini (insieme alle loro funzioni di legame) non è definito induttivamente, ma ciò non sembra insormontabile.) O ci sono effettivamente luoghi in cui è stato usato?


2
Non conosco gli svantaggi. Forse la formalizzazione (ad esempio in un assistente di prova) è più pesante? Non sono sicuro ... Quello che so è che non c'è nulla di tecnicamente sbagliato: questo modo di vedere i termini lambda è quello suggerito dalla loro rappresentazione come reti di prova, quindi le persone consapevoli della rete (come me) lo usano implicitamente tutto il tempo. Ma le persone a conoscenza della rete sono molto rare :-) Quindi forse è davvero una questione di tradizione. PS: ho aggiunto un paio di tag vagamente correlati per rendere la domanda più visibile (si spera).
Damiano Mazza,

Questo approccio non equivale alla sintassi astratta di ordine superiore (ovvero rappresentare i leganti come funzioni nella lingua host)? In un certo senso, l'uso di una funzione come legante stabilisce in modo implicito i puntatori ai leganti, nella rappresentazione delle chiusure.
Rodolphe Lepigre,

2
@RodolpheLepigre Non la penso così. In particolare, la mia comprensione è che HOAS è corretto solo quando la metatematica è abbastanza debole, mentre questo approccio è corretto in una metatematica arbitraria.
Mike Shulman,

3
Bene, quindi ogni raccoglitore usa un nome di variabile univoco (all'interno dell'albero) (il puntatore ad esso è uno automaticamente). Questa è la convenzione di Barendregt. Ma quando si sostituisce, è necessario ricostruire (in C) l'elemento che si sta sostituendo per continuare ad avere nomi univoci. Altrimenti (in generale) stai usando gli stessi puntatori per più sottotitoli e puoi ottenere l'acquisizione variabile. La ricostruzione è la ridenominazione alfa. Presumibilmente succede qualcosa di simile a seconda delle specifiche della codifica degli alberi come set?
Dan Doel,

3
@DanDoel Ah, interessante. Ho pensato che fosse così ovvio da non aver bisogno di menzionare che avresti inserito una copia separata del termine che veniva sostituito ad ogni occorrenza della variabile per la quale è stato sostituito; altrimenti non avresti più un albero di sintassi ! Non mi è venuto in mente di pensare a questa copia come ad una ridenominazione alfa, ma ora che me lo fai notare posso vederlo.
Mike Shulman,

Risposte:


11

Le risposte di Andrej e Łukasz danno buoni punti, ma volevo aggiungere ulteriori commenti.

Per fare eco a ciò che Damiano ha detto, questo modo di rappresentare il legame usando i puntatori è quello suggerito dalle reti di prova, ma il primo posto in cui l'ho visto per termini lambda era in un vecchio saggio di Knuth:

  • Donald Knuth (1970). Esempi di semantica formale. In Symposium on Semantics of Algorithmic Languages , E. Engeler (a cura di), Lecture Notes in Mathematics 188, Springer.

Nella pagina 234, ha disegnato il seguente diagramma (che ha definito una "struttura di informazioni") che rappresenta il termine :(λy.λz.yz)X

Diagramma di Knuth per $ (\ lambda y. \ Lambda z.yz) x $

Questo tipo di rappresentazione grafica dei termini lambda è stata anche studiata in modo indipendente (e più approfondito) in due tesi nei primi anni '70, sia da Christopher Wadsworth (1971, Semantics and Pragmatics of Lambda-Calculus ) che da Richard Statman (1974, Structural Complexity di prove ). Al giorno d'oggi, tali diagrammi sono spesso indicati come "grafici λ" (vedere ad esempio questo documento ).

Osserva che il termine nel diagramma di Knuth è lineare , nel senso che ogni variabile libera o legata ricorre esattamente una volta - come altri hanno già detto, ci sono problemi e scelte non banali nel tentativo di estendere questo tipo di rappresentazione a non -lineari.

D'altra parte, per termini lineari penso che sia fantastico! La linearità preclude la necessità di copiare e quindi si ottiene sia -equivalence che la sostituzione "gratuitamente". Questi sono gli stessi vantaggi di HOAS, e in realtà sono d'accordo con Rodolphe Lepigre che esiste una connessione (se non esattamente un'equivalenza) tra le due forme di rappresentazione: c'è un senso in cui queste strutture grafiche possono essere naturalmente interpretate come diagrammi a corde , che rappresenta gli endomorfismi di un oggetto riflessivo in una bicategoria chiusa compatta (ho dato una breve spiegazione di questo qui ).α


10

Non sono sicuro di come sia rappresentata la tua funzione variabile-legante e per quale scopo ti piacerebbe utilizzarla. Se si utilizzano i puntatori a ritroso, come notato da Andrej, la complessità computazionale della sostituzione non è migliore della classica ridenominazione alfa.

Dal tuo commento sulla risposta di Andrej deduco che in una certa misura sei interessato a condividere. Posso fornire alcuni input qui.

In un tipico calcolo lambda tipizzato, l'indebolimento e la contrazione, contrariamente ad altre regole, non hanno sintassi.

Γ , x 1 : A , x 2 : A t : T

Γt:TΓ,X:UNt:TW
Γ,X1:UN,X2:UNt:TΓ,X:UNt:TC

Aggiungiamo qualche sintassi:

Γ , x 1 : A , x 2 : A t : T

Γt:TΓ,X:UNWX(t):TW
Γ,X1:UN,X2:UNt:TΓ,X:UNCXX1,X2(t):TC

a b , cCun'B,c() sta "utilizzando" la variabile e vincolando le variabili . Ho imparato quell'idea da uno dei " An Interaction Net Implementation of Closed Reduction " di Ian Mackie .un'B,c

Con quella sintassi, ogni variabile viene usata esattamente due volte, una volta dove è legata e una volta dove è usata. Questo ci consente di prendere le distanze da una particolare sintassi e guardare il termine come un grafico in cui variabili e termini sono bordi.

Dalla complessità algoritmica, ora possiamo usare i puntatori non da una variabile a un legante, ma da un legante a una variabile e avere sostituzioni in un tempo costante.

Inoltre, questa riformulazione ci consente di monitorare la cancellazione, la copia e la condivisione con maggiore fedeltà. Si possono scrivere regole che copiano (o cancellano) in modo incrementale un termine mentre si condividono i sottotermini. Ci sono molti modi per farlo. In alcune impostazioni limitate le vittorie sono abbastanza sorprendenti .

Questo si sta avvicinando agli argomenti di reti di interazione, combinatori di interazione, sostituzione esplicita, logica lineare, valutazione ottimale di Lamping, condivisione di grafici, logiche di luce e altro.

Tutti questi argomenti sono molto interessanti per me e sarei lieto di dare riferimenti più specifici, ma non sono sicuro che nulla di tutto ciò sia utile per te e quali siano i tuoi interessi.


6

La struttura dei dati funziona ma non sarà più efficiente di altri approcci perché è necessario copiare ogni argomento su ogni riduzione beta e si devono fare tutte le copie quante sono le occorrenze della variabile associata. In questo modo continui a distruggere la condivisione della memoria tra i sotto-domini. In combinazione con il fatto che stai proponendo una soluzione non pura che prevede manipolazioni dei puntatori ed è quindi molto soggetta a errori, probabilmente non ne vale la pena.

Ma sarei felice di vedere un esperimento! Potresti prenderlo lambdae implementarlo con la tua struttura di dati (OCaml ha puntatori, sono chiamati riferimenti ). Più o meno, devi solo sostituire syntax.mle norm.mlcon le tue versioni. Sono meno di 150 righe di codice.


Grazie! Devo ammettere che non stavo davvero pensando molto alle implementazioni, ma soprattutto alla possibilità di fare prove matematiche senza preoccuparmi né della contabilità di de Bruijn né della ridenominazione alfa. Ma c'è qualche possibilità che un'implementazione possa conservare un po 'di condivisione della memoria non eseguendo copie "fino a quando non è necessario", cioè fino a quando le copie divergerebbero l'una dall'altra?
Mike Shulman,

Certo, potresti fare una cosa del genere copy-on-write, i sistemi operativi che la gente fa da molto tempo. Uno dovrebbe fornire alcune prove del fatto che funzionerà meglio delle soluzioni stabilite. Molto dipenderà dai modelli di utilizzo. Ad esempio, la maggior parte degli argomenti su -redeces vengono duplicati o vengono utilizzati principalmente in modo lineare? In un tipico redex , quale di solito è più grande, o ? A proposito, le sottostazioni esplicite sono anche un modo per fare le cose pigramente. ( λ x . e 1 )βe 1 e 2(λX.e1)e2e1e2
Andrej Bauer,

2
Per quanto riguarda le prove matematiche, ho subito una buona dose di formalizzazione della sintassi di tipo teorico, la mia esperienza è che i vantaggi si ottengono quando generalizziamo il setup e lo rendiamo più astratto, non quando lo rendiamo più concreto. Ad esempio, possiamo parametrizzare la sintassi con "qualsiasi buon modo di trattare l'associazione". Quando lo facciamo, è più difficile fare errori. Ho anche formalizzato la teoria dei tipi con gli indici de Bruijn. Non è troppo terribile, specialmente se ci sono tipi dipendenti che ti impediscono di fare cose insensate.
Andrej Bauer,

2
Per aggiungere, ho lavorato su un'implementazione che utilizzava sostanzialmente questa tecnica (ma con numeri interi e mappe univoci, non puntatori), e non lo consiglierei davvero. Abbiamo sicuramente avuto molti bug in cui ci siamo persi la clonazione corretta delle cose (in minima parte a causa del tentativo di evitarlo quando possibile). Ma penso che ci sia un articolo di alcune persone GHC in cui lo sostengono (hanno usato una funzione hash per generare nomi univoci, credo). Potrebbe dipendere esattamente da cosa stai facendo. Nel mio caso era l'inferenza / il controllo del tipo e lì sembra piuttosto inadatto.
Dan Doel,

@MikeShulman Per gli algoritmi di ragionevole complessità (elementare) (in gran parte quantità di copia e cancellazione), la cosiddetta "parte astratta" della riduzione ottimale di Lamping non sta facendo copie fino al momento necessario. La parte astratta è anche la parte non controversa rispetto all'algoritmo completo che richiede alcune annotazioni che possono dominare il calcolo.
Łukasz Lew,

5

Altre risposte discutono principalmente di problemi di implementazione. Dal momento che menzioni la tua motivazione principale come fare prove matematiche senza troppa contabilità, ecco il problema principale che vedo con quello.

Quando dici "una funzione che associa ogni variabile a un legante nel cui ambito si trova": il tipo di output di questa funzione è sicuramente un po 'più sottile di quello che lo fa suonare! In particolare, la funzione deve assumere valori in qualcosa come "i leganti del termine in esame", ovvero un insieme che varia a seconda del termine (e non è ovviamente un sottoinsieme di un ambiente più ampio in alcun modo utile). Quindi, in sostituzione, non puoi semplicemente "prendere l'unione delle funzioni di legame": devi anche reindicizzare i loro valori, secondo alcune mappe da leganti nei termini originali a leganti nel risultato della sostituzione.

Queste reindicizzazioni dovrebbero sicuramente essere “di routine”, nel senso che potrebbero essere ragionevolmente spazzate sotto il tappeto o confezionate bene in termini di funzionalità o naturalità. Lo stesso vale per la contabilità coinvolta nel lavoro con variabili nominate. Quindi, nel complesso, mi sembra probabile che ci sarebbe almeno tanto contabilità coinvolta in questo approccio quanto in approcci più standard.

A parte questo, però, è un approccio concettualmente molto accattivante e mi piacerebbe vederlo attentamente elaborato: posso ben immaginare che potrebbe gettare una luce diversa su alcuni aspetti della sintassi rispetto agli approcci standard.


tenere traccia dell'ambito di ogni variabile richiede effettivamente la contabilità, ma non saltare alla conclusione che è sempre necessario limitarsi a una sintassi ben definita! Operazioni come la sostituzione e la riduzione della beta possono essere definite anche in termini non mirati, e il mio sospetto è che se si volesse formalizzare questo approccio (che, di nuovo, è proprio l'approccio delle reti di prova / "grafici") in un assistente di prova, si implementano prima le operazioni più generali, quindi si dimostra che preservano la proprietà di essere ben definiti.
Noam Zeilberger,

(D'accordo che vale la pena provare ... anche se non sarei sorpreso se qualcuno ha già nel contesto della formalizzazione di reti di prova / grafici λ.)
Noam Zeilberger,


5

λLazy.t

Nel complesso, penso che sia una rappresentazione interessante, ma comporta una contabilità con puntatori, per evitare di interrompere i collegamenti vincolanti. Immagino che sarebbe possibile cambiare il codice per utilizzare i campi mutabili, ma la codifica in Coq sarebbe quindi meno diretta. Sono ancora convinto che questo sia molto simile a HOAS, sebbene la struttura del puntatore sia resa esplicita. Tuttavia, la presenza di Lazy.timplica che è possibile valutare alcuni codici nel momento sbagliato. Questo non è il caso nel mio codice in quanto solo la sostituzione di una variabile con una variabile può avvenire alla forcevolta (e non la valutazione ad esempio).

(* Representation of a term of the λ-calculus. *)
type term =
  | FVar of string      (* Free variable  *)
  | BVar of bvar        (* Bound variable *)
  | Appl of term * term (* Application    *)
  | Abst of abst        (* Abstraction    *)

(* A bound variable is a pointer to the corresponding binder. *)
and bvar = abst

(* A binder is represented as its body in which the bound variable points to
   the binder itself. Note that we need to use a thunk to be able to work
   underneath a binder (for substitution, evaluation, ...). A name can be
   given for easy printing, but no renaming is done. Only “visual capture”
   can happen since pointers are established the right way, even if names
   can clash. *)
and abst = { body : term Lazy.t ; name : string }

(* Terms can be built with recursive values for abstractions. *)

(* Krivine's notation is used for application (function in parentheses). *)

let id    : term = (* λx.x        *)
  Abst(let rec id = {body = lazy (BVar(id)); name = "x"} in id)

let idid  : term = (* (λx.x) λx.x *)
  Appl(id, id)

let delta : term = (* λx.(x) x *)
  Abst(let rec d = {body = lazy (Appl(BVar(d), BVar(d))); name = "x" } in d)

let weird : term = (* (λx.x) λy.(λx.(x) x) (C) y *)
  Appl(id, Abst(let rec x = {body = lazy (Appl(delta, Appl(FVar("C"),
    BVar(x)))); name = "y"} in x))

let omega : term = (* (λx.(x) x) λx.(x) x *)
  Appl(delta, delta)

(* Printing function is immediate. *)
let rec print : out_channel -> term -> unit = fun oc t ->
  match t with
  | FVar(x)   -> output_string oc x
  | BVar(x)   -> output_string oc x.name
  | Appl(t,u) -> Printf.fprintf oc "(%a) %a" print t print u
  | Abst(f)   -> Printf.fprintf oc "λ%s.%a" f.name print (Lazy.force f.body)

(* Substitution of variable [x] by [v] in the term [t]. Occurences of [x] in
   [t] are identified using physical equality ([BVar] case). The subtle case
   is [Abst], because we need to reestablish the physical link between the
   binder and the variable it binds. *)
let rec subst_var : bvar -> term -> term -> term = fun x t v ->
  match t with
  | FVar(_)   -> t
  | BVar(y)   -> if y == x then v else t
  | Appl(t,u) -> Appl(subst_var x t v, subst_var x u v)
  | Abst(f)   ->
      (* First compute the new body. *)
      let fv = subst_var x (Lazy.force f.body) v in
      (* Reestablish the physical link, using [subst_var] itself again. This
         requires a second traversal of the term. We could probably do both
         at once, but who cares the complexity is linear in [t] anyway. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)

(* Actual substitution function. *)
let subst : abst -> term -> term = fun f v ->
  subst_var f (Lazy.force f.body) v

(* Normalization function (all the way, even under binders). *)
let rec eval : term -> term = fun t ->
  match t with
  | Appl(t,u) ->
      begin
        let v = eval u in
        match eval t with
        | Abst(f) -> eval (subst f v)
        | t       -> Appl(t,v)
      end
  | Abst(f)   ->
      (* Actual computation in the body. *)
      let fv = eval (Lazy.force f.body) in
      (* Here, the physical link is reestablished, but it is important to note
         that the computation of evaluation is done above. So the part below
         only takes a linear time in the size of the normal form of the body
         of the abstraction. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)
  | _         ->
      t

let _ = Printf.printf "id         = %a\n%!" print id
let _ = Printf.printf "eval id    = %a\n%!" print (eval id)

let _ = Printf.printf "idid       = %a\n%!" print idid
let _ = Printf.printf "eval idid  = %a\n%!" print (eval idid)

let _ = Printf.printf "delta      = %a\n%!" print delta
let _ = Printf.printf "eval delta = %a\n%!" print (eval delta)

let _ = Printf.printf "omega      = %a\n%!" print omega
(* The following obviously loops. *)
(*let _ = Printf.printf "eval omega = %a\n%!" print (eval omega)*)

let _ = Printf.printf "weird      = %a\n%!" print weird
let _ = Printf.printf "eval weird = %a\n%!" print (eval weird)

(* Output produced:
id         = λx.x
eval id    = λx.x
idid       = (λx.x) λx.x
eval idid  = λx.x
delta      = λx.(x) x
eval delta = λx.(x) x
omega      = (λx.(x) x) λx.(x) x
weird      = (λx.x) λy.(λx.(x) x) (C) y
eval weird = λy.((C) y) (C) y
*)
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.