- congruentie -regel: x.E z.{z/x}E, voor elke z die niet voorkomt in E. {z/x}E stelt hier het resultaat voor van de substitutie van z voor x in E. -congruentie: Twee expressies M en N zijn -congruent als ofwel M N, of M N, of N wordt verkregen uit M door een subexpressie S ervan te vervangen door een expressie T zo dat S T, of er is een expressie R zo dat M -congruent is met R en R is -congruent met N.
- converteerbaarheid -regel: (x.P Q) [Q/x]P waar [Q/x]P het resultaat is van de substitutie van Q voor x in P, rekening houdend met het feit dat er eventueel nieuwe gebonden namen ingevoerd worden. Een expressie M is -converteerbaar (gelijk) aan een expressie N, N = M, als N -congruent (zie verder) is met M, of M N, of N M, of er is een -expressie E zodat M = E en E = N. De zo verkregen relatie = is uiteraard een equivalentierelatie. staat voor 0 of meer reductiestappen ( of ).
Church - Rosser stelling In het algemeen zijn er in een -expressie meerdere redexen, d.w.z. subtermen van de vorm (x.P Q) die in aanmerking komen voor -reductie. Dus stelt zich de vraag of het kan dat een -expressie meer dan één normaalvorm heeft: schematisch -expressie ... ... normaalvorm_1 normaalvorm_2
Church - Rosser stelling Dat is natuurlijk niet wenselijk, tenminste als de 2 normaalvormen echt verschillen. De Church-Rosser stellingen drukken uit dat het fenomeen zich niet voordoet. Stelling (Church-Rosser 1): Voor -expressies A, B en C: als A B en A C dan bestaat er een -expressie D zo dat A één stap B C "Diamond property" evt. meerdere stappen D Gevolg: Als A B en A C, en B en C zijn beide in normaalvorm, dan zijn B en C -congruent.
Church - Rosser stelling Stelling (Church-Rosser 2): Als A en B -converteerbaar zijn in elkaar, dan is er een D met A D en B D. Er bestaat dan een "zigzag-lijn" van reducties en -stappen, van A naar B A B Gevolg: Als A = B (-converteerbaar) en B is in normaalvorm, dan A B Als A = B, dan hebben ze ofwel beide dezelfde normaalvorm, ofwel hebben ze er geen van beide een.
Church - Rosser stelling Helaas lost dit niet alle problemen ivm de evaluatievolgorde op: er zijn ook nog de -expressies die tot een oneindige reeks reducties leiden, zoals (x.(x x) x.(x x)): (x.(x x) x.(x x)) (x.(x x) x.(x x)) ...
((a.b.b (x.(x x) x.(x x))) x.x) (b.b x.x) x.x Reductiestrategieën Als men bv een dergelijke expressie als argument gebruikt van een applicatie krijgt men een situatie waarin er eventueel wel een normaalvorm is, maar ook oneindige reducties: ((a.b.b (x.(x x) x.(x x))) x.x) (b.b x.x) x.x maar eerst het argument (x.(x x) x.(x x)) van a.b.b evalueren levert een oneindige reeks reducties op, zoals gezien.
Normal order - Applicative order Bij de evaluatie van een functieapplicatie f(arg) gebruikt men in imperatieve talen gewoonlijk de applicatie orde: eerst wordt het argument uitgewerkt, en daarna wordt f erop toegepast (call-by-value). In de -calculus, en de functionele talen die erop gebaseerd zijn, gebruikt men gewoonlijk de normal order evaluatie: bij het evalueren van (f arg) beginnen we met de applicatie, dus als f = x.B is dat de reductie (x.B arg) [arg/x]B (waar [arg/x]B staat voor de versie van B na de substitutie van arg voor x). Het doet er niet toe dat er binnen B eventueel nog redexes zijn.
Normal order evaluatie In het algemeen reduceert men bij normal order evaluatie telkens de meest linkse redex: die met het meest linkse beginsymbool (. bv: (z and (select_second t)) toegepast op z = true en t = false (z.(t.((x.y.((x y) false) z) (x.y.y t)) true) false) (t.((x.y.((x y) false) false) (x.y.y t)) true) ((x.y.((x y) false) false) (x.y.y true)) 4 mogelijkheden Dit noemt men ook lazy evaluation: je begint maar aan het argument als het echt nodig is.
Normal order evaluatie Stelling (standardization): Als een -expressie een normaalvorm heeft, dan is haar normal order evaluatie eindig. Nadeel: normal order evaluatie kan erg inefficiënt zijn, omdat een gebonden variabele meerdere keren kan voorkomen in een -expressie. (x.((plus x) x) (succ 5)) ((plus (succ 5)) (succ 5)) (succ 5) wordt 2 keer geëvalueerd
Combinators Zoals we al zagen by de numerals, booleans enz. hebben combinators, -expressies zonder vrije variabelen (ook wel gesloten -expressies genoemd), de eigenschap dat hun betekenis niet afhangt van de context, dwz van de grotere expressie waarvan ze eventueel een subexpressie zijn. Als er daarentegen wel vrije variabelen zijn worden die evt. bij applicatie vervangen door een argument, dat dan op zijn beurt kan interageren met de rest van de expressie. Men kan nu proberen combinators te vinden die van speciaal belang zijn bij de implementatie van de -calculus. Het gaat dan om combinators die hogere orde - functies voorstellen.
Combinators Identiteit: I x.x Functiecompositie: compose f.g.x.(f (g x)) Functieapplicatie: apply f.x.(f x) Compose is een voorbeeld van een zgn curried operator: om een functie van 3 argumenten voor te stellen worden functies van één argument in elkaar genest. Je kan met behulp van de gewone reductieregel nagaan dat compose inderdaad associatief is, dat dus geldt: ((compose ((compose f) g)) h) = ((compose f) ((compose g) h))
Combinators Speciale symbolen die staan voor combinators, zoals I en compose, kunnen op twee manieren gebruikt worden: gewoon als afkorting van hun definitie; we moeten dan niets veranderen aan de reductieregel(s), of als nieuwe atomen, dus constante symbolen, die niet verder opgesplitst worden. Dan kunnen we er alleen mee werken als we er specifieke, nieuwe reductieregels voor invoeren. Dit kan de efficiëntie sterk verbeteren, en het is bv wat we doen voor de combinators die getallen voorstellen. Nieuwe regels voor I, compose en apply: (I)E E (((compose F) G) E) (F (G E)) ((apply A) B) (A B)
Combinators: uitgebreide syntax Om het gewone rekenen met getallen en booleans in te voeren breiden we de syntax uit als volgt: < expression > ::= < constant > | < name > | < function > | < application > ... < constant > ::= < number > | < operator > | < combinator > < number > ::= < integer > | < real number > < operator > ::= < arithmetic operator > | < relational operator > | < boolean operator > | < predicate > < arithmetic operator > ::= + | - | ... < predicate > ::= zero < combinator > ::= true | false | I | compose
Recursie: de Y-combinator Recursieve definitie van faculteit, in gewone notatie: fac(n) == if n = 0 then 1 else n * fac(n-1) in -notatie: (fac n) = (((iszero n) 1) ((mult n) (fac (pred n)))) en dus fac = n.(((iszero n) 1) ((mult n) (fac (pred n)))) en fac = (f.n.(((iszero n) 1) ((mult n) (f (pred n)))) fac), wat van de vorm f = (E f) is. Bijgevolg komt fac bepalen erop neer een fixpunt te zoeken van de functie voorgesteld door f.n.(((iszero n) 1) ((mult n) (f (pred n)))).
iszero De test voor nul, iszero, kan voorgesteld worden als n.((n (true false)) true). Toepassing op 0 = f.x.x : (n.((n (true false)) true) f.x.x) ((f.x.x (true false)) true) (x.x true) true
iszero Toepassing op 2 = f.x.(f (f x)): (n.((n (true false)) true) f.x.(f (f x))) ((f.x. (f (f x)) (true false)) true) (x.((true false) ((true false) x)) true) ((true false) ((true false) true)) == ((x.y.x x.y.y) ((true false) true)) ((z.y.z x.y.y) ((true false) true)) (-conversie) (y.x.y.y ((true false) true)) (z.x.y.y ((true false) true)) (-conversie) x.y.y == false
(y.(x.(y (x x)) x.(y (x x))) E) (x.(E (x x)) x.(E (x x))) De Y combinator Terug naar recursie. We zoeken een fixpunt van een functie E, die zelf op functies werkt. Dat blijkt te kunnen door toepassing van de combinator Y, ook wel fixpuntcombinator of paradoxale combinator genoemd: Y == y.(x.(y (x x)) x.(y (x x))) Dit is een aangepaste versie van de (x.(x x) x.(x x)) van voordien, alleen wordt er bij elke iteratie een applicatie van y op (x x) neergezet: (y.(x.(y (x x)) x.(y (x x))) E) (x.(E (x x)) x.(E (x x))) (E (x.(E (x x)) x.(E (x x)))) (E (E (x.(E (x x)) x.(E (x x))))) enzovoort
de Y combinator (Y E) reduceert dus wel degelijk tot (E (Y E)). De body van de recursieve definitie wordt een willekeurig groot aantal keer herhaald. Als er in E geen basisgeval voorkomt (zoals het geval n=0 bij fac) dan levert ook de Y combinator uiteraard geen praktische oplossing: bv. x = 3x - 10 we zoeken een functie g bepaald door g = (x.(minus ((mult 3) x) 10) g) met de Y combinator: g = (Y f.x.(minus ((mult 3) x) 10)). De rechterkant, toegepast op een argument, geeft aanleiding tot een oneindige reductie. Als er een basisgeval is, dan stopt de reductie wel.
Y combinator : eindige recursie bv. fac: de n samen met de iszero zorgen ervoor dat de reductie stopt. (fac 1) == ((Y E) f.x.(f x)) waar E == f.n.(((iszero n) 1) ((mult n) (f (pred n)))) ((Y E) f.x.(f x)) == ((y.(x.(y (x x)) x.(y (x x))) E) 1) ((x.(E (x x)) x.(E (x x))) 1) ((E (x.(E (x x)) x.(E (x x))) 1) == ((f.n.(...) (x.(E (x x)) x.(E (x x))) 1) (n.(((iszero n) 1) ((mult n) ((x.(E (x x)) x.(E (x x))) (pred n)))) 1) (((iszero 1) 1) ((mult 1) ((x.(E (x x)) x.(E (x x))) (pred 1)))) (iszero 1) geeft false, we gaan verder met ((mult 1)... (pred 1) ... ((mult 1) (n.(...) ) 0) (iszero n) geeft nu true en de recursie stopt.
Curry's paradox De gewone logische eigenschappen van implicatie zijn inconsistent met de interpretatie van -reductie als gelijkheid De logische formule (§): (P (P Q)) (P Q) is waar. Verder geldt de regel Modus Ponens: als P en P Q waar zijn, dan is ook Q waar. Bekijk nu de -expressie voor (§): imp is de combinator voor implicatie ((imp ((imp P) ((imp P) Q))) ((imp P) Q)) en stel N == x.((imp x) ((imp x) Q)) voor willekeurige Q (x (x Q)) en P == (Y N) dus P is het fixpunt van N, dwz P = (N P) dan geldt P = (N P) = ((imp P) ((imp P) Q) en substitutie in (§) geeft ((imp ((imp P) ((imp P) Q))) ((imp P) Q)) = ((imp P) ((imp P) Q) = (N P) = P . Dus (§) = P.
Curry's paradox Aangezien (§) waar is, is P = true. Maar P staat voor (P (P Q)), dus zijn ook (P Q) en dus Q waar. Maar Q was willekeurig, dus we hebben een contradictie. De oorzaak van de inconsistentie ligt in het zonder beperking toelaten van zelf-applicatie. Dit kan opgelost worden door het toevoegen van typering: aan elke -expressie wordt een type toegekend. Als de types T en TT verschillen, dan kan een functie van type TT niet op zichzelf toegepast worden.Deze idee leidde tot de getypeerde - calculus.
De -regel Abstractie is een partieel inverse van applicatie: er geldt (x.P x) P maar, met abstractie en applicatie in de andere volgorde, is x.(P x) niet reduceerbaar tot P. Applicatie hiervan op een term Q kan ertoe leiden dat er vrije voorkomens van x in P onder de x terecht komen, en daarom reduceert (x.(P x) Q) tot ([Q/x]P Q), wat in het algemeen niet hetzelfde is als (P Q). Nochtans lukt het wel als we voor de abstractie een verse variabele gebruiken: voor elke twee termen P,Q en variabele x, met x niet vrij in P, geldt (x.(P x) Q) (P Q). Dus zijn x.(P x) en P extensioneel equivalent (als functie dus) terwijl ze toch een verschillende normaalvorm hebben. Om de -calculus extensioneel compleet te maken (dwz als termen extensioneel equivalent zijn dan hebben ze dezelfde normaalvorm) voegen we een reductieregel toe.
De -regel <naam> . (<expressie> <naam>) mag vereenvoudigd worden tot <expressie> , m.a.w. we nemen als nieuwe, extra regel: ( <naam> . (<expressie> <naam>) <argument> ) (<expressie> <argument>)
Standaard combinators Het is mogelijk om elke -expressie te vervangen door een equivalente expressie die enkel bestaat uit combinators, constanten, en vrije variabelen die op mekaar toegepast zijn. Als, bv, u en v de enige vrije variabelen in P zijn, dan geldt voor de combinator u.v.P dat ((u.v.P u) v) = P. Het blijkt mogelijk te zijn om helemaal van de abstracties (en dus van de gebonden variabelen) af te komen en een versie van de -calculus te maken met slechts twee combinatoren: de standaard combinatoren S en K. (Schönfinkel, Curry)
Standaard combinators S en K zijn gedefinieerd als volgt: S: x.y.z.((x z) (y z)) K: x.y.x (= True) Als shorthand kunnen we de identiteit I: x.x gebruiken, ze is niet echt nodig want ((S K) K) = I Een expressie met enkel de standaard combinators heet een standaard combinator expressie. Syntax: < comb-expression > ::= < atom > | < application > < atom > ::= < variable > | < constant > | < combinator > < application > ::= (< comb-expression > < comb-expression >) < combinator > ::= S | K
Standaard combinators Stelling: Voor elke -expressie E kan men een standaard combinator expressie F construeren met F = E. Deze constructie staat bekend als “bracket abstraction”. Combinator expressies staan dichter bij het practische functionele programmeren dan de -calculus: je vertrekt daar immers ook met een aantal vaste basisfuncties, en de nieuwe regels zijn ook efficiënter dan de -reductie. Er zijn naast S en K nog een heel aantal vergelijkbare combinatoren ingevoerd. Omgekeerd kunnen die combinatoren steeds terug vertaald worden in -calculus, zodat de theoretische basis aanwezig blijft.
Lijsten in de -calculus Een lijst [E1, E2, ... , En] kan in de -calculus voorgesteld worden als z.((z E1) z.((z E2) ... z.((z En) nil)...) met nil een constant symbool dat de lege lijst voorstelt. Men neemt dan head == x.(x true), tail ==x.(x false) en cons == x.y.z.((z x) y). Men moet dan geen extra notatie invoeren. Een beknopter alternatief bestaat erin de syntax uit te breiden met lijstexpressies van de vorm [E1, E2, ... , En], met nieuwe symbolen voor combinatoren als head, tail en cons, en daar gepaste reductieregels voor te geven.
Lijsten: syntax De syntax voor -expressies wordt als volgt uitgebreid: < -expression > ::= < variable> | … | < list > < list > ::= [ < -expression > < list-tail > | [ ] < list-tail > ::= , < -expression > < list-tail > | ] … < list-operator > ::= head | tail | cons We kunnen nu expressies schrijven als bv. (q.[p,q,r] A) , (x.[(x y),(y x)] B) of (x.(y.[x,y] a) [b,c,d]), en we verwachten dat die reduceren tot respectievelijk [p,A,r] , [(B y),(y B)] en [[b,c,d],a] .
Lijsten: reductieregels (head [ ]) [ ] (head [E1, ... ,En]) E1 (tail [ ]) [ ] (tail [E1, ... ,En]) [E2, ... ,En] ((cons A) [ ]) [A] ((cons A) [E1, ... ,En]) [A,E1, ... ,En ] (null [ ]) true (null [E1, ... ,En ]) false (1 [E1, ... ,En ]) E1 (k [E1, ... ,En ]) ((pred k) [E2, ... ,En ])
Lijsten: reductieregels Ook de regels voor -conversie en -reductie moeten dus uitgebreid worden: {z/x} [E1, ... ,En] [{z/x}E1, ... , {z/x}En] (x.[E1, ... ,En] Q) [(x.E1 Q), ... , (x.En Q)]
Wederzijdse recursie F = (Y g.[r1, ... , rn]) Met behulp van lijsten kunnen we ook hiervoor de Y combinator gebruiken. Stel, we hebben een rij functiedefinities f1 = e1, ... , fn = en. Stel dat de fi overal in de ej mogen voorkomen. De waarde van een expressie E die de fi gebruikt kan niet zomaar verkregen worden door reductie van (f1.( ... (fn.E en) ... )) want een vrij voorkomen van fi in ej wordt dan maar vervangen als i < j. We gebruiken een F die de lijst [f1, ... , fn] voorstelt. Meer precies, laat Ri de expressie zijn bekomen uit ei door de vrije voorkomens van de fk in ei te vervangen door (k F), d.w.z. de selectie van het k-de element uit F. Dan F = [R1, ... , Rn] Dus F = (g.[r1, ... , rn] F) waar de ri bekomen zijn uit de Ri door F te vervangen door g. We hebben nu weer F = (Y g.[r1, ... , rn])