Normalizacja przez ocenę

W semantyce języka programowania normalizacja przez ocenę (NBE) to styl uzyskiwania normalnej postaci terminów w rachunku λ poprzez odwoływanie się do ich semantyki denotacyjnej . Termin jest najpierw interpretowany jako denotacyjny model struktury λ-terminowej, a następnie reprezentatywna reprezentacja kanoniczna (β-normalna i η-długa) jest wyodrębniana przez reifikację oznaczenie. Takie zasadniczo semantyczne, wolne od redukcji podejście różni się od bardziej tradycyjnego składniowego, opartego na redukcji opisu normalizacji jako redukcji w systemie przepisywania terminów , w którym β-redukcje są dozwolone głęboko w λ-termach.

NBE został po raz pierwszy opisany dla prostego rachunku lambda . Od tego czasu został rozszerzony zarówno na słabsze systemy typów , takie jak nieokreślony rachunek lambda przy użyciu podejścia opartego na teorii domeny , jak i na bogatsze systemy typów, takie jak kilka wariantów teorii typów Martina-Löfa .

Zarys

Rozważmy po prostu wpisany rachunek lambda , gdzie typy τ mogą być typami podstawowymi (α), typami funkcji (→) lub iloczynami (×), określonymi przez następującą gramatykę postaci Backusa-Naura (→ kojarzenie w prawo, jak zwykle):

(Typy) τ ::= α | τ 1 → τ 2 | τ 1 × τ 2

Można je zaimplementować jako typ danych w metajęzyku; na przykład dla Standard ML możemy użyć:

      
                  
                 typ danych  ty  =  Podstawowy  ciąg  znaków  |  Strzałka  ty  *  ty  |  _  Produkt  ty  *  ty  _   

Terminy definiowane są na dwóch poziomach. Niższy syntaktyczny (czasami nazywany poziomem dynamicznym ) to reprezentacja, którą zamierza się znormalizować.

(Warunki składniowe) s , t ,… ::= var x | lam ( x , t ) | aplikacja ( s , t ) | para ( s , t ) | pierwszy t | snd t

Tutaj lam / app (odp. pair / fst , snd ) to formy intro / elim dla → (odp. ×), a x to zmienne . Terminy te mają być zaimplementowane jako pierwszego rzędu w metajęzyku:

      
                        
                           typ  danych  tm  =  zmienna  łańcucha  |  lam  łańcucha  *  tm  |  _  aplikacja  tm  *  tm  |  _  para  tm  *  tm  |  _  pierwszy  z  tm  |  snd  z  tm 

Semantyka denotacyjna terminów (zamkniętych) w metajęzyku interpretuje konstrukcje składni w kategoriach cech metajęzyka; zatem lam jest interpretowane jako abstrakcja, app jako aplikacja itp. Konstruowane obiekty semantyczne są następujące:

(Warunki semantyczne) S , T ,… ::= LAM x . S x ) | PARA ( S , T ) | SYN t

Zauważ, że w semantyce nie ma zmiennych ani form eliminacji; są one reprezentowane po prostu jako składnia. Te obiekty semantyczne są reprezentowane przez następujący typ danych:

        
                   
                  typ danych  sem  =  LAM  z  (  sem  ->  sem  )  |  PARA  sem  *  sem  |  _  SYN  z  tm 

Istnieje para funkcji indeksowanych typu, które poruszają się tam iz powrotem między warstwą składniową i semantyczną. Pierwsza funkcja, zwykle zapisywana ↑ τ , odzwierciedla składnię terminu w semantyce, podczas gdy druga reifikuje semantykę jako termin syntaktyczny (zapisany jako ↓ τ ). Ich definicje są wzajemnie rekurencyjne w następujący sposób:

Definicje te można łatwo zaimplementować w metajęzyku:

 
     
     
           
          

 
       
         (* zmienna_świeża : jednostka -> łańcuch *)  val  zmienna_zmienna  =  ref  ~1  zabawa  zmienna_świeża  ()  =  (  zmienna_zmienna  :=  1  +  !zmienna_zmienna  ;  "v"  ^  Int  .  toString  (  !zmienna_zmienna  ))  (* reflect : ty -> tm -> sem *)  zabawa  odbijać  (  Strzałka  (  a  ,  b  ))  t  =  LAM  (  fn          
         
               
        
         S  =>  odzwierciedlać  b  (  app  (  t  ,  (  reify  a  S  ))))  |  odbicie  (  Prod  (  a  ,  b  ))  t  =  PARA  (  odbicie  a  (  pierwsze  t  ),  odbicie  b  (  snd  t  ))  |  odzwierciedlać  (  Podstawowe  _)  t  =  SYN  t 

 
        
             
                 
       
        (* reify : ty -> sem -> tm *)  i  reify  (  Strzałka  (  a  ,  b  ))  (  LAM  S  )  =  niech  val  x  =  fresh_var  ()  in  lam  (  x  ,  reify  b  (  S  (  odzwierciedla  a  (  var  x  ))))  koniec  |  reify  (  Prod  (  a  ,  b     
             
           ))  (  PARA  (  S  ,  T  ))  =  para  (  reify  a  S  ,  reify  b  T  )  |  reify  (  Podstawowy  _)  (  SYN  t  )  =  t 

Z indukcji struktury typów wynika, że ​​jeśli obiekt semantyczny S denotuje dobrze wpisany termin s typu τ, to reifikacja obiektu (tj. ↓ τ S) daje β-normalną η-długą postać s . Pozostaje więc tylko skonstruować wyjściową interpretację semantyczną S z terminu syntaktycznego s . Operacja ta, zapisana ∥ s Γ , gdzie Γ jest kontekstem powiązań, przebiega przez indukcję wyłącznie po strukturze terminowej:

W realizacji:

     
                     

 
         
                

 
   typ danych  ctx  =  pusty  |  add  of  ctx  *  (  string  *  sem  )  (* wyszukiwanie : ctx -> string -> sem *)  zabawne  wyszukiwanie  (  add  (  remdr  ,  (  y  ,  value  )))  x  =  jeśli  x  =  y  następnie  wartość  else  szukaj  remdr  x  (* znaczenie: ctx -> tm -> sem *)  zabawne  znaczenie    
         
              
                     
              G  t  =  przypadek  t  z  var  x  =>  wyszukiwanie  G  x  |  lam  (  x  ,  s  )  =>  LAM  (  fn  S  =>  znaczenie  (  dodaj  (  G  ,  (  x  ,  S  )))  s  )  |  aplikacja  (  s  ,  t  )  =>  (  wielkość liter  ma znaczenie    
                                
                  
               
                          G  s  z  LAM  S  =>  S  (  co oznacza  G  t  ))  |  para  (  s  ,  t  )  =>  PARA  (  co oznacza  G  s  ,  co oznacza  G  t  )  |  fst  s  =>  (  wielkość liter  oznacza  G  s  z  PARY  (  S  ,  T  )  =>  S  ) 
               
                          |  snd  t  =>  (  wielkość liter  oznacza  G  t  z  PARY  (  S  ,  T  )  =>  T  ) 

Należy zauważyć, że istnieje wiele niewyczerpujących przypadków; jednak w przypadku zastosowania do zamkniętego , dobrze wpisanego terminu żaden z tych brakujących przypadków nigdy nie zostanie napotkany. Operacja NBE na zasadach zamkniętych to zatem:

 
           (* nbe : ty -> tm -> tm *)  fun  nbe  a  t  =  reify  a  (  co oznacza  puste  t  ) 

Jako przykład jego użycia rozważ termin składniowy SKK zdefiniowany poniżej:

         
                   val  K  =  lam  (  "x"  ,  lam  (  "y"  ,  var  "x"  ))  val  S  =  lam  (  "x"  ,  lam  (  "y"  ,  lam  (  "z"  ,  app  (  app  (  var  "x"  ,  var  „z”  ),  aplikacja  (  var  „y”   
         ,  var  "z"  )))))  val  SKK  =  app  (  app  (  S  ,  K  ),  K  ) 

Jest to dobrze znane kodowanie funkcji tożsamości w logice kombinatorycznej . Normalizowanie go w typie tożsamości daje:

        
         -  nbe  (  strzałka  (  podstawowe  „a”  ,  podstawowe  „a”  ))  SKK  ;  val  it  =  lam  (  "v0"  ,  var  "v0"  )  :  tm 

Wynik jest w rzeczywistości w formie η-długiej, co można łatwo zobaczyć, normalizując go przy innym typie tożsamości:

              
          -  nbe  (  Strzałka  (  Strzałka  (  Podstawa  „a”  ,  Podstawa  „b”)  ),  Strzałka  (  Podstawa  „a”  ,  Podstawa  „b”  )))  SKK  ;  val  it  =  lam  (  "v1"  ,  lam  (  "v2"  ,  app  (  var  "v1"  ,  var  "v2"  )))    :  tm 

Warianty

Używanie poziomów de Bruijna zamiast nazw w składni rezydualnej sprawia, że ​​reify jest czysto funkcjonalne, ponieważ nie ma potrzeby fresh_var .

Typ danych terminów rezydualnych może być również typem danych terminów rezydualnych w postaci normalnej . Typ reify (a zatem nbe ) wyjaśnia następnie, że wynik jest znormalizowany. A jeśli typ danych normalnych form jest wpisany, typ reify (a zatem nbe ) wyjaśnia, że ​​normalizacja zachowuje typ.

Normalizacja przez ocenę jest również skalowana do prostego rachunku lambda z sumami ( + ), przy użyciu rozdzielonych operatorów sterujących shift i reset .

Zobacz też