x - FoTS

advertisement
Reductiemachine
Functionele talen en dus de -calculus, worden vaak geïmplementeerd
door een reductiemachine. De elementaire stap is een reductie, en de
"machinetaal" bestaat uit reductiestappen. In principe kan men die in
hardware realiseren (LISP machine). Het uit te voeren programma is één
-expressie, die stapsgewijs gereduceerd wordt tot normaalvorm.
De expressie kan als een string symbolen voorgesteld worrden, maar dat
vereist veel kopieerwerk. Daarom gebruiken moderne implementaties een
graph voorstelling. Dat maakt wel het memory-management moeilijker, en
heeft geleid tot de ontwikkeling van allerlei technieken voor garbage
collection.
In de vorm die we totnogtoe gezien hebben zijn -conversie en -reductie
nog te complex om als één stap gezien te worden, we vervangen ze door
meer elementaire stappen.
let en eval
Om het programmeren te vergemakkelijken voert men "syntactic sugar"
toe, zoals de mogelijkheid om hulpfuncties te definiëren (let) en om de
evaluatie expliciet te starten (eval)
let f = E1
let g = E2
eval E3
staat voor:
(f.(g.E3 E2) E1)
Graph voorstelling
Een -expressie wordt voorgesteld door haar parse tree, maar met extra
pijlen die ervoor zorgen dat er cycli kunnen voorkomen. Dit betekent dat
het originele programma gemakkelijk terug gereconstrueerd kan
worden: in principe volstaat een depth-first doortocht van de parse tree.
Soorten knopen:
abstractie:
x
applicatie:
:
combinator:
infix lijstconstructor:
,
operator:
head, tail, cons, +, -, ...
lijst-einde of lege lijst:
[]
indirectie:
@
numerieke waarde:
#
renaming:
{z/x}
variabele:
x,y,z, ...
Y, true, false
Graph voorstelling: voorbeelden
((x.y.(x (y x)) 2) [E,F])
:
,
:
x
2
,
E
y
[]
F
:
:
x
y
x
Graph voorstelling: voorbeelden
:
fact
n
:
E
:
:
1
:
iszero
:
n
*
:
n
let fact = n.(((iszero n) 1) ((mult n) (fact (pred n))))
eval E
:
pred
n
De regels
Voor -conversie en -reductie gebruikten we totnogtoe volgende regels.
-regel:
x.E  z.{z/x}E
-regel:
(x.E Q)  [Q/x]E
Daarnaast zijn er de regels voor lijsten en constante symbolen.
Omdat de expressies willekeurig complex kunnen zijn kan men het
uitvoeren van die substituties niet als een elementaire stap opvatten.
Daarom vervangen we deze regels door nieuwe, die wel elementaire
stappen voorstellen. De regels voor lijsten worden iets sterker; we laten
toe de applicatie van een lijst om te zetten in een lijst van applicaties - dit
leidt tot de zgn. -regels.
Aangepaste regels
Voor -conversie:
1:
{z/x}x  z
2:
{z/x}E  E
3:
{z/x}y.E  y.{z/x}E
4:
{z/x}(E1 E2)  ({z/x}E1 {z/x}E2)
5:
{z/x}[E1 , ... ,En]  [{z/x}E1 , ... , {z/x}En]
als x niet vrij in E
als x, y en z verschillend
De {z/x} wordt dus gewoon doorgeschoven naar binnen in de expressie.
Aangepaste regels
Voor -conversie:
1:
(x.x Q)  Q
2:
(x.E Q)  E
3:
(x.y.E Q)  z.(x.{z/y}E Q)
4:
(x.(E1 E2) Q)  ((x.E1 Q) (x.E2 Q))
als x niet vrij in E
als x verschillend van y, en z
komt niet voor in E of in Q
Voor 3: we willen (x.y.E Q)  [Q/x]y.E maar alleen als y "veilig" is.
Dus beginnen we met y alvast te vervangen door een "verse" z. Omdat
die uiteraard verschillend is van x kunnen we de x naar binnen schuiven,
voorbij de z.
Aangepaste regels: de  - regels
Voor lijsten vervangt men de regel
(x.[E1, ... ,En] Q)  [(x.E1 Q), ... , (x.En Q)]
Door de zgn. -regels:
1: ([E1, ... ,En] Q)  [(E1 Q), ... , (En Q)] en
2: x.[E1, ... ,En]  [ x.E1 , ... , x.En ]
Deze regels, samen met die voor de combinatoren en operatoren,
kunnen nu omgezet worden naar elementaire stappen voor de
reductiemachine.
Stappen: -regels
z/x
z

{z/x}x  z
x
x

{z/x}E  E als x niet vrij in E
E
y
z/x
{z/x}y.E  y.{z/x} E als xyz
@
z/x

z/x
y
y
E
E
E
Stappen: -regels
:
z/x
z/x
:
{z/x}(E1 E2)  ({z/x}E1 {z/x}E2)
z/x
:

E1
E1
E2
E2
,
z/x
z/x
,
{z/x}[E1, ... ,En]  [{z/x}E1, ... ,{z/x}En]
A
,

B
z/x
A
B
Stappen: -regels
@
:
1: (x.x Q)  Q

x
x
Q
Q
x
x
@
:
2: (x.E Q)  E
als x niet vrij in E

x
x
Q
Q
E
E
Stappen: -regels
3: (x.y.E Q)  z.(x.{z/y}E Q)
als x en y verschillen, en z is vers: noch vrij,
noch gebonden in (E Q)
z
:
:

x
x
x
Q
Q
y
y
E
E
z/y
Stappen: -regels
4: (x.((E1 E2) Q)  ((x.E1 Q) (x.E2 Q))
:
:
:

x
Q
x
x
x
:
E1
:
:
E2
E1
E2
Q
Stappen: -regels
1: ([E1, ... ,En] Q)  [(E1 Q), ... , (En Q)]
,
:
:
:

,
,
C
A
B
C
A
B
Stappen: -regels
2: x.[E1, ... ,En]  [ x.E1 , ... , x.En ]
x
,
x

,
A
B
x
,
A
B
Y combinator
In plaats van de cyclische representatie te gebruiken, kunnen we ook de
Y-combinator expliciet voorstellen, en volgende regel gebruiken.
:
:
:

Y
Y
F
F
Het gebruik van de cyclische representatie van recursieve functies blijkt
gewoonlijk efficiënter te zijn.
Opmerkingen
• Ook voor de combinators null, +, head, ... zijn er uiteraard dergelijke
operaties.
• De operaties behouden altijd de knopen aan de linkerkant (er
kunnen nog andere pijlen zijn naar die knopen) en wijzigen enkel van
de root het label. Op die manier is het duidelijk hoe het vervangen
deel van de graph vasthangt aan de rest.
• De operaties voeren nooit nieuwe cycli in; de enige cycli zijn die
veroorzaakt door recursieve definities.
Implementatie
Om de knopen voor te stellen gebruikt men bv. 4 velden :
( code, op1, op2, marker)
(marker voor de garbage collector).
Het herkennen van een patroon in een graph is in het algemeen een
moeilijk probleem. Gelukkig zijn de te herkennen patronen hier erg
klein. Om een -redex te vinden: gebruik een depth-first traversal om
het volgende patroon op te sporen:
: N1
N2 x
N3
Implementatie
Als N2 geen abstractieknoop is, dan is N1 niet de root van een -redex.
Anders, zoek een vrije occurence van x in de boom onder N3. Als er
geen te vinden is, dan hebben we een 2-redex. Als er wel een
gevonden wordt, dan bepaalt het veld code van N3 of we een redex
voor 1, 3 of 4 herkennen. Als N3 noch een variable-, noch een
abstractie-, noch een applicatieknoop is, dan is N1 niet de root van een
-redex.
Het zoeken naar een vrije occurence van x voor herkennen van een 2redex kan uitgevoerd worden in een depth-first doortocht.
Ook voor -redexen kunnen we volstaan met het onderzoeken van 2
knopen, met weer een extra test voor 2.
Evaluatie-orde
Doordat we nu andere regels hebben is normal order
evaluatie niet meer helemaal wat we nodig hebben: bv
((x.y.E P) Q) 
(3)
(z.(x.{z/y}E P) Q) 
(4)
((z.x.{z/y}E Q) (z.P Q)) 
(3)
(v.(z.{v/x}{z/y}E Q) (z.P Q)) 
(4)
((v.z.{v/x}{z/y}E (z.P Q)) (v.Q (z.P Q)))  ...
Afspraak: na 3 verder gaan met het deel na de nieuwe
(meest linkse) .
Evaluatie-orde
Eager, applivative order: argumenten eerst.
Demand - driven: argumenten pas evalueren als nodig.
Lazy: ook de argumenten maar partieel evalueren als dat volstaat.
Normal order: meest linkse redex. In ons systeem komt dat overeen met
lazy.
Lazy evaluatie is niet altijd efficiënt:
(x.((+ x)(pred x)) ((* 5) (succ 3))) 
((x.(+ x) Q) (x.(pred x) Q)) 
(4) met Q = ((* 5) (succ 3))
(4)
(((x.+ Q) (x.x Q)) (x.(pred x) Q))  (1, 2)
((+ ((* 5) (succ 3))) (x.(pred x) Q)) 
((+ 20) (x.(pred x) Q))  ...
(regels voor *, succ)
en Q moet opnieuw geëvalueerd worden.
Graph reductie
De boom van Q = ((* 5) (succ 3)) wordt niet gedupliceerd:
:
:
:
:
+
:
x

:
x
pred
*
x
:
5
succ
3
4
Graph reductie
:
:
:
:
x
x
x
:
:
:
+
:
x
pred
*
x
:
5
succ
3
Opmerkingen
•Deze reductiemachine kan uiteraard geperfectioneerd worden, bv,
door parallellisme te gebruiken.
• Moderne implementaties halen de efficiëntie van een behoorlijke
C compiler.
• I/O, of andere neveneffecten, kan men inbouwen door gebruik te
maken van monads: interpreteer sommige symbolen m als acties; en
bekijk paren (m,a), met m een actie en a een waarde. Er is een
sequencing operatie >>= van type
m a  (amb)m b
Informeel: een actie die een a oplevert kan samengesteld worden met
een functie die op basis van een a een actie geeft die een b oplevert.
Het resultaat is een actie die een b oplevert.
Unificatie
In Haskell wordt pattern matching gebruikt in de definitie van functies, bv.
in qsort: matching qsort [x:xs] met [3,2,4,1,5] resulteert in de binding van
x aan 3 en van xs aan [2,4,1,5]. Ook by typechecking komt een dergelijke
situatie voor: als men een functie f wil toepassen op een argument arg,
dan moet de typedescriptor van f van de vorm td1 td2 zijn, en als arg
typedescriptor tdarg heeft, dan moeten td1 en tdarg "gelijk gemaakt" kunnen
worden, door vereenvoudigingen, maar ook door de vervanging van
typevariabelen door meer concrete typedescriptoren. Dit "gelijk maken"
(en tegelijk variabelen binden) noemt men unificatie
Ook in logische talen speelt unificatie een belangrijke rol.
Unificatie
qsort(cons(x , xs)) en cons(3 , cons(2 , cons(4 , cons(1 , cons(5 , [ ])))))
(in gewone mathematische notatie)
resultaat: x = 3
xs = cons(2 , cons(4 , cons(1 , cons(5 , [ ]))))
mult(add(4 , 5) , minus(y)) en mult(add(x ,y) , z)
resultaat: x = 4
y=5
z = minus(5)
mult(add(4 , 5) , minus(y)) en mult(add(x ,y) , 8)
resultaat: fail
Unificatie: algemeen
Gegeven: twee termen, T1 en T2, opgebouwd uit
- variabelen
- constanten
- functie-applicaties ("Functoren", in logisch programmeren)
Gevraagd: een meest algemene unifier (most general unifier, MGU)
voor T1 en T2
unifier: een substitutie  (afbeelding van de variabelen naar de termen) die voldoet
aan (T1) = (T2)
most general unifier: een unifier met de eigenschap dat, voor elke andere unifier ' ,
geldt dat '(T1) een instantie is van (T1), m.a.w. er is een substitutie  die van
(T1) '(T1) maakt: ((T1)) = '(T1) (er geldt dan ook ((T2)) = '(T2) natuurlijk).
Unificatie: algoritme van Robinson
MGU = { };
WS = { (T1 , T2) }
repeat
verwijder een paar (R1 , R2) uit WS;
case
R1 en R2 zijn twee identieke variabelen of constanten: skip;
R1 is een variabele die niet voorkomt in R2:
vervang, overal in WS en MGU, R1 door R2;
voeg (R1 , R2) toe aan MGU;
R2 is een variabele die niet voorkomt in R1:
vervang, overal in WS en MGU, R1 door R2;
voeg (R1 , R2) toe aan MGU;
R1= f(y1, ... ,yn) en R2= f(z1, ... ,zn):
voeg {(y1,z1), ... ,(yn,zn)} toe aan WS
otherwise return(fail)
end case
until WS = { }
Download