360 likes | 462 Views
Lijsten in de -calculus. Een lijst [E 1 , E 2 , ... , E n ] kan in de -calculus voorgesteld worden als z.((z E 1 ) z.((z E 2 ) ... z.((z E n ) nil)...)
E N D
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 uitgebreid worden: {z/x} [E1, ... ,En] [{z/x}E1, ... , {z/x}En] (x.[E1, ... ,En] Q) [(x.E1 Q), ... , (x.En Q)]
Functies voor lijstmanipulatie: append append moet 2 lijsten samenvoegen: ( ( append [A1, …, Am] ) [B1,…,Bn] ) = [A1, …, Am,B1,…,Bn] En dus, recursief, ((append x) y) = if (null x) then y else (((cons (head x)) (append (tail x)) y)) of in zuivere -notatie: append = x. y.(((null x)y)(((cons (head x)) (append (tail x)) y))) en met de Y-combinator: append = (Yf.x. y.(((null x)y)(((cons (head x)) (f (tail x) y))))
Functies voor lijstmanipulatie: map map past een fuctie toe op alle elementen van een lijst: ( (map f) [E1,…,En] ) = [ (f E1) ,…, (f En) ] En dus, recursief, ((map f) x) = if (null x) then [ ] else ((cons (f (head x))) ((map f) (tail x))) of in zuivere -notatie: map = m.x.y.(((null x) [ ]) ((cons (f (head x))) ((m f) (tail x)))) en met de Y-combinator: map = (Y m.x.y.(((null x) [ ]) ((cons (f (head x))) ((m f) (tail x)))))
Lijsten: tail recursion De som van de elementen van een lijst kan berekend worden met een polyadische uitbreiding van de gewone som - functie. In -calculus kan men werken met één argument, een lijst. Recursief: (sum x) = if (null x) then 0 else ((+ (head x)) (sum (tail x))) Deze vorm van recursie lijkt op die van map, maar er is een belangrijk verschil: daar kan men f toepassen op het eerste element zonder de rest van de recursie eerst te moeten uitwerken, maar hier kan de binaire + maar voor het eerst toegepast worden op de laatste 2 elementen van de lijst. Het eerste geval (zoals map dus) noemen we staart-recursie (tail recursion). Het tweede geval is minder wenselijk omdat er plaats moet voorzien worden om al de "pending" oproepen van + bij te houden. We kunnen wel een staart-recursieve definitie van sum geven:
Lijsten: tail recursion We voeren een accumulator a in: ((sum a) x) = if (null x) then a else ((sum ((+ a) (head x)) (tail x)). Het tweede argument van sum is nu (tail x), wat al korter is dan x. Op voorwaarde dat het eerste argument van sum wordt geëvalueerd voor het tweede, hebben we weer staart-recursie. Bij een oproep moet a natuurlijk geïnitializeerd worden op 0: ((sum 0) x) berekent de som van de elementen van x. Bij het rekenen met oneindige lijsten is staartrecursie uiteraard noodzakelijk.
Oneindige lijsten • Oneindige, of op zijn minst "open ended" lijsten komen veel voor: getallen, woordenlijsten, streams, ... zijn vaak geproduceerd door andere programma’s. • Doorgeven ervan (bv. als functieargument) stelt mogelijk problemen: (f arg) met arg een oneindige lijst • Oplossing: demand-driven evaluatie: onderbreek de evaluatie van een argument op het moment dat de toegepaste functie f verder uitgewerkt kan worden zonder het argument verder te evalueren. • Komt overeen met normal order evaluatie, van bv. • ((cons E1) ((cons E2) ... ((cons En) nil)...). • Dat lukt niet altijd: als f staat voor null, head, tail, dan kan het, maar niet als f staat voor, bv, sum.
Oneindige lijsten: zeros Stel dat we head willen toepassen op een oneindige lijst nullen Recursieve definitie van die lijst: zeros = ((cons 0) zeros) met de Y combinator: zeros = (Y z. ((cons 0) z)) wat inderdaad reduceert tot (z. ((cons 0) z) (Y z. ((cons 0) z))) en dan tot ((cons 0) (Y z. ((cons 0) z))). Probleem: dit is niet van de vorm [E1, ... ,En] die we nodig hebben om de regel (head [E1, ... ,En]) E1 toe te passen
Lazy rules voor de lijstoperatoren Het probleem kan opgelost worden door nieuwe regels te introduceren voor de lijstoperatoren, zodat ze kunnen werken op slechts gedeeltelijk geëvalueerde lijsten. ((cons E) R) [E,R (null [E,R ) false (head [E,R ) E (tail [E,R ) R (1 [E,R ) E (k [E,R ) ((pred k) R) voor k =2,3,... Deze regels stellen de lazy evaluatie van de lijstoperatoren voor. Het is nu geen probleem meer om bv. (5 zeros) te evalueren.
Strictness Een -expressie die een oneindige lijst voorstelt is, bekeken als functie, ongedefinieerd: de uitwerking ervan is oneindig. Toelaten dat een functie f, toegepast op een dergelijke expressie, toch een resultaat geeft, komt neer op het toelaten van niet-stricte functies: Een functie is strict als ze alleen gedefinieerd is wanneer al haar argumenten gedefinieerd zijn Voorbeelden van niet-stricte functies: conditionele expressie if C then A else B multiply (altijd 0 als eerste argument 0 is)
Voorbeelden Om een oneindige lijst te kunnen bouwen hebben we een functie nodig om telkens het volgende element te berekenen. De lijst van natuurlijke getallen ontstaat bv. door de functie succ te itereren. De lijst [ x , (f x) , (f (f x)) , ... ] kan gedefinieerd worden als ((iterate f) x) = ((cons x) ((iterate f) (f x)) en dus expliciet als iterate = (Y i.f.x.((cons x) ((i f) (f x))). De lijst van alle natuurlijke getallen is dus numbers = ((iterate succ) 0). Analoog kan de lijst [ 1, 1, 2, 3, 5, 8, 13, ... ] van Fibonacci getallen kan gedefinieerd worden als ((build x) y) = ((cons x) ((build y) ((+ x) y))) dus build = (Y b.x.y.((cons x) ((b y) ((+ x) y)))) en fibonacci = ((build 1) 1).
Zeef van Erathosthenes Hulpfunctie filter om veelvouden van n uit een lijst x te filteren: ((filter n) x) = if (null x) then [ ] else if (iszero ((mod (head x)) n)) then ((filter n) (tail x)) else (((cons (head x)) ((filter n) (tail x))) sieve kan nu gedefinieerd worden als (sieve x) = ((cons (head x)) (sieve ((filter (head x)) (tail x)))) en de lijst van priemgetallen kan dus uitgedrukt worden als: primes = (sieve ((iterate succ) 2)).
De getypeerde -calculus In deze versie van de -calculus wordt aan elke -expressie een type toegekend. Op die manier kan men de inconsistentie van de ongetypeerde calculus vermijden, en tegelijk het programmeren gemakkelijker maken door een type-checking systeem in te voeren.Types worden opgebouwd met type-constructoren, vertrekkende van een vaste set van ground types zoals int, boolean, real. Definitie (T1): de set Typ van beschikbare types is opgebouwd als volgt. (1) Alle ground types behoren tot Typ (2) Als en tot Typ behoren, dan ook . Neem nu aan dat elke variabele een type heeft. Dan kunnen we voor elk type een aparte set van -expressies definiëren. Alleen -expressies die een welbepaald type hebben beschouwen we nog als geldig.
De getypeerde -calculus Neem nu aan dat elke variabele een type heeft. Dan kunnen we de set van -expressies van type definiëren als volgt: Definitie (T2): Voor elk type is de set van -expressies van type gedefinieerd door: (1) Elke variabele van type is in (2) Als x tot behoort en E behoort tot , dan behoort x.E tot (3) als P tot behoort en Q behoort tot , dan behoort (P Q) tot . De set van alle getypeerde -expressies is de unie van alle . Er zijn nu dus minder expressies: als P tot behoort en Q behoort niet tot , dan is (P Q) geen geldige expressie.
Uitbreiding: types voor lijsten Definitie T1 breiden we uit met de regel (3) Als 1, ... , n tot Typ behoren, dan ook [1, ... , n]. en Definitie T2 met twee regels: (4) Als de Ei van type i zijn, dan is [E1, ... , En] van type [1, ... , n]. (5) Als P van type [ 1 , ... , n ] is en Q van type , dan is (P Q) van type [1, ... , n]. Dus x.[E1, ... , En] is van type [1, ... , n] en [x.E1, ... , x.En] is van type [ 1 , ... , n ]. Applicatie van beide op een expressie van type geeft een resultaat van type[1, ... , n].
-reductie in getypeerde -calculus Een -reductie (x.P Q) {Q/x}P is nu alleen mogelijk als x en Q hetzelfde type hebben. Als gevolg daarvan is -reductie en dus ook gelijkheid consistent met de typering: -expressies die in mekaar omgezet kunnen worden hebben hetzelfde type.
Typechecking We hebben oneindig veel lijst-types, dus lijstoperatoren als head, tail, map enz. zijn overloaded. Ook een aantal constanten als + en * zijn overloaded. De regels van definitie T2 geven te weinig informatie om statische typechecking te kunnen doen voor, bv, overloaded functies als mult of add. Ze zeggen niets over relaties tussen types, zoals het feit dat een integer geïnterpreteerd kan worden als een real. Met behulp van de regels van T2 kunnen we aan type inferentie of typechecking doen: uitzoeken wat precies het type is van een expressie waarin overloaded symbolen voorkomen. Een expressie als ((mult 3.14) x) kan alleen het type real hebben, de informatie over het type van x is daarvoor niet nodig - dat moet real zijn. Het is helaas zeer moeilijk uit te maken voor welke variabelen het type zo kan bepaald worden.
Typechecking Het type van een -expressie hangt af van zijn componenten. Het bepalen van het type van een -expressie gebeurt dus "inside out", in tegenstelling tot normal order evaluatie. Om typechecking uit te voeren gaan we voor een expressie eerst een typedescriptor bouwen en die dan vereenvoudigen. Dit laatste is enigszins analoog aan het evalueren van de -expressie zelf. Er zijn wel verschillen, zoals het feit dat er niet in normal order kan geëvalueerd worden.
Typedescriptoren: syntax <type-descr> ::= <ground-type> | <abstraction-type> | <application-type> | <list-type> | <operator-type> | <union-type> <ground-type> ::= int | real | bool <abstraction-type> ::= <type-descr> <type-descr> <application-type> ::= (<type-descr> <type-descr>) <list-type> ::= [ ] | [<type-descr> <tail-type> <tail-type> ::= ] | <type-descr> <tail-type> <operator-type> ::= + | - | * | / | < | = | > | ≠ | ≤ | ≥ | head | tail | cons <union-type> ::= <type-descr> <type-descr> en worden weggelaten als er geen dubbelzinnigheid is
Typedescriptoren Van essentieel belang is hier uiteraard de regel voor applicatie: <application-type> ::= (<type-descr> <type-descr>) Hettypechecking proces moet nagaan of de twee typedescriptoren in de applicatie compatibel zijn. Dat komt neer op de statische typecheck die men ook vindt in andere programmeertalen.
Toekenning van typedescriptoren Definitie (T3): de typedescriptor TD{E} van een -expressie E is inductief bepaald door volgende regels. = int = int ... = int int = bool bool bool TD{0} TD{1} TD{succ} TD{and} TD{null} = gl bool TD{x} = als x een variabele van type is TD{x.P} = TD{x} TD(P) TD{(P Q)} = (TD{P} TD{Q}) TD{[E1, ... ,En]} = [ TD{E1}, ... , TD{En} ] TD{} = voor elke typedescriptor Door de laatste regel is de descriptor van een operator de operator zelf. Hij impliceert ook TD{ [ ] } = [ ]. gl is een generiek list-type.
Vereenvoudigen van typedescriptoren Dit gebeurt met volgende regels: • voor alle typedescriptoren en [1 , ... , n] voor alle typedescriptoren en 1 , ... , n • voor alle typedescriptoren • voor alle typedescriptoren en 1 , ... , n • int • • [ ] • 1 ( ) = ([ 1 , ... , n ] ) = (gl [ ]) = (gl [1 , ... , n]) = ((+ int) int) = ... ((bool ) ) = (head [ ]) = (head [1 , ... , n]) =
Vereenvoudigen van typedescriptoren (head [1 , ... , n] [1 , ... , n]) = (tail [ ]) = (tail [1 , ... , n]) = (tail gl) = ((cons ) [ ]) = ((cons ) [1 , ... , n ]) = (int [1 , ... , n ]) = 11 [ ] [2 , ... , n] gl [] [, 1 , ... , n ] [1 ... n] en verder ook nog de gewone regels voor de unie
Typechecking Deze regels vereisen soms heel wat pattern-matching: bv. de bij een toepassing van de regel ( ) = kan complex zijn. Indien er na de vereenvoudiging nog een applicatie-type blijft staan, dan is er een typefout opgetreden. Aangezien verschilt vanvoor elk type , is er geen zelf-applicatie mogelijk en kunnen we de Y combinator niet gebruiken. Wat wel gaat is voor elk type een aparte combinator Y invoeren, met reductieregel (Y E) (E (Y E)) voor elke -expressie E, en type-inferentieregel (Y ) = voor elk type .
Voorbeeld De recursieve definitie van fact is: fact = n.(((iszero n) 1) ((mult n) (fact (pred n)))) Dit leidt tot de type-vergelijking = (((intbool int) int) (int(intint) int) ( (intint int))) en na vereenvoudiging = int ((bool int) (intint ( int))) Een oplossing is = intint , zoals verwacht. Het bestaan van een oplossing voor de type-vergelijking impliceert natuurlijk niet dat er een welgedefinieerde oplossing is voor de recursieve definitie: als we bv. in de definitie van fact de pred vervangen door succ, dan verandert er aan de typering niets.
Resultaten Wanneer we geen fixpuntcombinatoren invoeren zijn er in de getypeerde -calculus geen oneindige reducties, en dus geldt: Stelling: Elke getypeerde -expressie heeft een normaalvorm. (Doordat het aantal pijltjes nooit stijgt tijdens de type-vereenvoudiging kan men bewijzen dat er in de getypeerde -calculus geen oneindige reducties voorkomen) We kunnen nu dus de normaalvorm zien als de betekenis of semantiek van een expressie.
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 worden, 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. 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 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 wordt omgezet naar de -expressie (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: variabele: x,y,z, ... combinator: Y, true, false operator: head, tail, cons, +, -, ... indirectie: @ renaming: {z/x} abstractie: x applicatie:: infix lijstconstructor: , lijst-einde of lege lijst: [ ] numerieke waarde: #
+ Graph voorstelling: voorbeelden ((+ A) B) : : B A
, , [ ] , Graph voorstelling: voorbeelden [A1, A2, ... , An] A1 ... A2 An
2 # : : : x x , , : y y [ ] Graph voorstelling: voorbeelden ((x.y.(x (y x)) 2) [E,F]) E F
n * 1 : n : n : : : : : : n Graph voorstelling: voorbeelden fact zero pred let fact = n.(((iszero n) 1) ((mult n) (fact (pred n))))
Aangepaste regels Op een analoge manier kan men een reeks wederzijds recursieve definities en oneindige lijsten voorstellen. Om -conversie te definiëren gebruikt men normaal inductie. Omdat de expressies willekeurig complex kunnen zijn kan men dat niet als een elementaire stap opvatten. Daarom elimineren we de -conversie door ze "in te bouwen" in de -reductie. Dat geeft aanleiding tot de volgende regels (de -regels kunnen nog wat verder vereenvoudigd worden, zie”reductiemachine”) -regel: x.E z.(x.E z) -regels: (x.x Q) Q (x.y Q) y als x en y verschillen (x.x.E Q) x.E (x.y.E Q) y.(x.E Q) als x en y verschillen, en x is niet vrij in E of y is niet vrij in Q (x.(E1 E2) Q) ((x.E1 Q) (x.E2 Q))