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 xyz @ 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 (amb)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 = { }