290 likes | 460 Views
-calculus. C. Functionele talen. basisidee: programmeren = functies bouwen Het uitrekenen van expressies gebeurt door substitutie. een programma definieert een functie (input output) samenstellen van complexer functies uit eenvoudiger delen: opbouwen van functionele termen
E N D
C. Functionele talen • basisidee: programmeren = functies bouwen • Het uitrekenen van expressies gebeurt door substitutie • een programma definieert een functie (input output) • samenstellen van complexer functies uit eenvoudiger • delen: opbouwen van functionele termen • eenvoudige datastructuren: lijsten (2 + 3) * (23 - 17) 5 * (23 - 17) 5 * 6 30 Niet meer verder te “reduceren”: normaalvorm
Haskell: quicksort f [] = [] f (x:xs) = f ys ++ [x] ++ f zs where ys = [a | a xs, a x] zs = [b | b xs, b > x] Bevat variabelen, lijsten, operators, pattern matching, list comprehension, … 3
-calculus • Ingevoerd door Alonzo Church en Stephen Kleene, begin 30-er jaren, als algemeen berekeningsmodel • In 1936 gebruikt om een negatief antwoord te geven op het Entscheidungsprobleem: Equivalentie van willekeurige -expressies (equivalent = zelfde functie) is onbeslisbaar • Formele basis voor functioneel programmeren: de mechanismen van “echte” functionele talen, zoals Haskell, zijn er “bovenop” gebouwd
-calculus • Basis voor denotationele semantiek van programmeertalen (Scott - Strachey): definieer de precieze betekenis (semantiek) van programma’s en programmadelen door middel van een partiële functie die het input-output gedrag beschrijft. • Naïeve, ongetypeerde -calculus leidt tot inconsistenties, net zoals de naïeve verzamelingenleer. Dat werd opgelost door de getypeerde-calculus, waarbij elke term een type krijgt. Dit legde de basis voor de studie van typesystemen in het algemeen.
-calculus • Syntax: variabelen, -abstractie, functie-applicatie • De fundamentele stap: - reductie • Gebonden en vrije variabelen, - conversie • Combinators, gesloten termen • Booleans in de - calculus • Integers in de - calculus • Recursie, de fixpunt - combinator Y • Reductiestrategieën, normal order en applicative order
Syntax De syntax van een -expressie is extreem eenvoudig: er zijn enkel namen (variabelen), functies (abstracties) en applicaties. < expression > ::= < name > | < function > | < application > < name > ::= x | y | z | ... < function > ::= < name > . < body > < body > ::= < expression > < application > ::= ( < expression > < expression > )
Expressies De algemene idee is dat een < function> een functiedefinitie voorstelt, met < name > als (enige) formele parameter, en dat in een < application > een functie wordt toegepast op een argument. • voorbeelden: • x.x (de identiteitsfunctie ) fun.arg.(fun arg) (apply) x.y.y (select second, false) y.(x.(y (x x)) x.(y (x x))) (Y combinator)
Opmerkingen • Strict genomen moeten we ook constanten invoeren. Formeel kunnen we ze behandelen als namen die niet onder een mogen voorkomen. Voorlopig laten we constanten gewoon buiten beschouwing. • Elke expressie (-term) stelt een functie voor.
Opmerkingen • Elke expressie (-term) stelt een functie voor. • De kijk op functies is extensioneel: twee functies worden beschouwd als gelijk als ze dezelfde extensie hebben, m.a.w. als ze voor elk argument dezelfde waarde geven. Over de manier waarop die waarde berekend wordt, dus met welk algoritme, zegt dat niets. In de context van programmatransformaties speekt men ook van semantische equivalentie. • De vraag of twee termen (extensioneel) met dezeldde functie overeenkomen, is onbeslisbaar (cfr. equivalentieprobleem voor Turing machines).
- reductie Idee: applicatie van functie x. <body> op <argument-expressie> = De variabele x speelt dus de rol van formele parameter. Een -expressie wordt uitgewerkt (geëvalueerd) door dit herhaaldelijk te doen, tot er niets meer uit te werken valt: dus tot er geen "redex" (<name>. <body> <arg>) meer te vinden is. expressie bekomen door in <body> de naam x te vervangen door <argument-expressie>.
- converteerbaarheid 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.
- reductie: voorbeelden ( (x.y.x z.z) t) ( y.z.z t ) z.z (select first) (x.( (f. arg.(f arg) z.z) x) t) ( (f. arg.(f arg) z.z) t) ( arg.(z.z arg) t) ( z.z t) t (identity_2) Normal order: eerst de functiedefinitie uitwerken
Opmerkingen • Er is dus in principe maar één reductieregel nodig! • Expressies worden uitgewerkt door deze reductieregel toe te passen tot er geen geschikt deel van de vorm (x.<body> <argument>) meer gevonden kan worden; de expressie is dan in normaalvorm gebracht. • Je zou kunnen verwachten dat het resultaat van een -reductie korter is dan de expressie waarvan je vertrekt, zoals in ( y.( z.w y ) ( x.(x x) x.(x x) ) ) ( z.w ( x.(x x) x.(x x) ) ) w
maar dat hoeft niet zo te zijn: ( x.(x x) x.(x x) ) ( x.(x x) x.(x x) ) ( x.(x x) x.(x x) ) … Er zijn dus oneindige reducties mogelijk. We willen dat twee expressies die enkel verschillen doordat de namen na de verschillen, als gelijk beschouwd worden.
-calculus en concurrency Het zal blijken dat de volgorde waarin delen van een term (redexes) vervangen worden, er niet toe doet. I.h.b. kunnen verschillende, disjuncte delen van een term dus tegelijk, onafhankelijk van elkaar vervangen worden. Dit berekeningsmodel is daarom geschikt voor het invoeren van parallellisme. Het is mogelijk speciale hardware te bouwen om dit exploiteren, en dat is ook gebeurd (LISP - machine).
- conversie: het probleem Bekijk de functieapply== fun.arg.(fun arg). Bedoeling is dat die het eerste argument toepast op het tweede. De namen van de "formele parameters" fun en arg hebben geen belang, dus ook x.y.(x y) stelt apply voor. Maar: ( (apply arg) z) == ( (fun.arg.(fun arg) arg) z) (arg.(arg arg) z) (z z) wat niet de bedoeling is
wel goed gaat: ( (apply arg) z) == ( (x.y.(x y) arg) z) (y.(arg y) z) (arg z) . De reden waarom het bij ( (fun.arg.(fun arg) arg) z) fout gaat is dat in de eerste stap, waarin arg (de "actuele parameter")gesubstitueerd wordt voor fun, die arg verward wordt met de arg van arg. Als we apply definiëren als x.y.(x y) doet die moeilijkheid zich niet voor. Besluit: we moeten, voor we een functie toepassen, de "formele parameters" eerst vervangen door nieuwe, onschadelijke namen. Deze operatie noemt men een -conversie.
Vrije en gebonden namen Op een bepaalde plaats in een expressie is een naam x gebonden als hij daar voorkomt in een subterm van de vorm x.P. Anders is x daar vrij. y.(x.(xy) y.(yx)) gebonden ((x y.(y (x y.(y x.(yx))))) x) vrij x. ((x y.(y (x y.(y x.(yx))))) x)
- 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.
Reductie, met - conversie Bij het uitwerken van een functie-applicatie (x.P Q): voor elke naam y die vrij voorkomt in Q, en die gebonden voorkomt in P, vervang voor het uitwerken de gebonden occurrences van y overal in P door een nieuwe naam y'. bv. in ( (fun.arg.(fun arg) arg) z) : vervang arg door een nieuwe, "ongevaarlijke" naam. Resultaat: ( (fun.arg'.(fun arg') arg) z)
Rekenen in de - calculus: combinators Een combinator is een -expressie met enkel gebonden variabelen. Het gedrag van een combinator is daarom onafhankelijk van de context waarin hij optreedt. Constanten (True, 5, AND, +, ...) kunnen voorgesteld worden door combinatoren. x.y.x select_first e1. e2. c.((c e1) e2) cond
Booleans true == x.y.x select_first false == x.y.y select_second cond== e1. e2. c.((c e1) e2) toegepast op <exp1> en <exp2> geeft: wat, toegepast op true == x.y.x geeft: ((e1. e2. c.((c e1) e2) <exp1>) <exp2>) (e2. c.((c <exp1>) e2) <exp2>) c.((c <exp1>) <exp2>) (c.((c <exp1>) <exp2>)x.y.x) ((x.y.x <exp1>) <exp2>) (y.<exp1> <exp2>) <exp1>
Booleans en analoog leidt een toepassing op false tot <exp2>. not kan nu gedefinieerd worden als: not == x.((cond false) true) x) wantals het derde argument van cond (hier x dus) true is, selecteert cond het eerste van zijn twee argumenten, dus false. Analoog, als het derde argument false is, selecteert het true. x.(((cond false) true) x) kan vereenvoudigd worden: en dus kunnen we gebruiken: not == x.((x false) true) (((e1. e2. c.((c e1) e2) false) true) x) ((e2. c.((c false) e2) false) true) x) (c.((c false) true) x) ((x false) true)
AND de functie X and Y kan gedefinieerd worden als and == x.y.(((cond y) false) x) want als x true is, dan neemt men de waarde van y over, en als x false is, dan selecteert men false. Het deel (((cond y) false) x) kan weer vereenvoudigd worden, nu tot ((x y) false). We gebruiken dus: and == x.y. ((x y) false) (en or == x.y. ((x true) y) ). bv. voor true and false: ((and true) false) == ((x.y. ((x y) false) true) false) (y. ((true y) false) false) ((true false) false) ... false
Gehele getallen: Church numerals • We definieren combinatoren die 0,1,2, ... voorstellen: • 0 ==f.x.x • 1 == f.x.(f x) • 2 == f.x.(f (f x)) • 3 == f.x.(f (f (f x))) • enzovoort. De successor-functie kan dan worden: • succ == n.f.x.(f ((n f) x)) • bv: (succ 1) == (n.f.x.(f ((n f) x)) f.x.(f x)) f.x.(f ((f.x.(f x) f) x)) f.x.(f (x.(f x) x)) f.x.(f (f x)) == 2
Andere bewerkingen plus == m.n.f.x.((m f) ((n f) x)) mult == m.n.f.(m (n f))
Andere bewerkingen plus == m.n.f.x.((m f) ((n f) x)) mult == m.n.f.(m (n f)) sommige zijn behoorlijk complex - ook Church zelf raakte ervan overtuigd dat de predecessor - functie niet voorgesteld kon worden, tot Kleene vond: pred == n.(((n p.z.((z (succ (p true))) (p true))) z.((z 0) 0)) false) In gewone notatie: pred(n) = n -1, als n > 0 0, als n = 0
predecessor - vervolg Deze definitie is gebaseerd op het volgende. z.((z a) b) stelt het geordende paar [a,b] voor, en heeft de eigenschap (z.((z a) b) true) a en (z.((z a) b) ) false) b Dus stelt next == p. z.((z (succ (p true))) (p true)) de functie voor die [n+1,n] berekent uit [n,n-1]: als we voor p het paar [n,n-1] invullen, selecteert (p true) daaruit het eerste element n, en vervolgens vormt men een paar met (succ (p true)) en (p true). De predecessor-functie verkrijgt men door next n keer toe te passen, vertrekkend van het paar [0,0]: z.((z 0) 0) en dan het tweede element te selecteren door de verkregen functie op false toe te passen.