INF2810: Funksjonell Programmering Omgivelsesmodeller og destruktive listeoperasjoner Stephan Oepen & Erik Velldal Universitetet i Oslo 15. mars 2013
Tema 2 Forrige uke Representasjon av mengder Sorterte lister og binære trær Lokal tilstand og assignment Prosedyrebasert objektorientering I dag The whole truth: omgivelser Destruktive listeoperasjoner Data-identitet vs. -ekvivalens
Representasjon av mengder: Binærtrær 3 Enda mer effektivt: {1, 3, 5, 7, 9, 11} som binærtre med ordnede noder. Hver node lagrer et element en venstre-gren med mindre elementer en høyre-gren med større elementer Ved søk etter et gitt element x: Hvis x er mindre enn node-oppslaget, søk til venstre. Hvis x er større, søk til høyre. Kan halvere søkerommet i hvert trinn: Logaritmisk vekst; O(log n). Må være litt obs på å holde treet (noenlunde) balansert.
Trær som lister og mengdeoperasjoner for binærtrær 4 To abstraksjonsbarrier (abstrakte datatyper) som bygger på hverandre: (define (make-tree entry left right) (list entry left right)) (define (tree-entry tree) (car tree)) (define (tree-left-branch tree) (cadr tree)) (define (tree-right-branch tree) (caddr tree)) (define (element-of-set? x set) (cond ((null? set) #f) ((= x (tree-entry set)) #t) ((< x (tree-entry set)) (element-of-set? x (tree-left-branch set))) ((> x (entry set)) (element-of-set? x (tree-right-branch set)))))
Tid og tilstand 5 Så langt ren funskjonell programmering: prosedyrer som funksjonelle (dvs. matematiske) transformasjoner av data; statisk over tid. Semantikken til et uttrykk er uavhengig av hvor og når det brukes. Gir stor grad av gjennomsiktighet (referential transparency). Fra kapittel 3 i SICP tar vi høyde for at uttrykk (og objekter ) kan inngå i en omgivelse som forandrer seg over tid. Sentrale konsepter: tilstand (state) og tilordning (assignment). Gir oss en form for (prosedyre-basert) objekt-orientering. Viktig teknikk for å organisere programmer: vi kan opprette objekter i programmene våre som tilsvarer entiteter i verden vi ønsker å modellere. Atferden avhenger av egenskaper som kan forandre seg over tid.
For eksempel: bankkonto 6 (define (make-withdraw initial) (let ((balance initial)) (lambda (amount) (cond ((>= balance amount) (set! balance (- balance amount)) balance) (else "Insufficient funds")))))? (make-withdraw 100) #<procedure>? (define w1 (make-withdraw 100))? (define w2 (make-withdraw 200))? (w1 50) 50? (w2 50) 150 Vi representerer kontoobjekter som prosedyrer. make-withdraw: En prosedyre for å generere nye konto-objekter. Returnerer en anonym prosedyre som innkapsler sin egen saldo-variabel: w1 og w2 er distinkte objekter, med distinkte lokale tilstandsvariabler. innkapsling kalles gjerne for closure utenfor SICP.
Destruktive operasjoner 7 I kapittel 3 i SICP tok vi et stort skritt: Med set! bryter vi med ren funksjonell programmering og introduserer elementer fra imperativ programmering: Verditilordning og tilstandsendring som modifiserer objekter. Kalles i noen sammenhenger fra et funksjonelt perspektiv for destruktive operasjoner. Når vi endrer tilstanden til et objekt destruerer vi det siden det ikke lenger representerer samme verdi. Til nå har vi kunnet tenke på variabler som navn på verdier. Fra nå må vi tenke på variabler som steder man kan lagre verdier. Bankkonto er en muterbar objekt, med både identitet og tilstand.
Prosedyre-basert objektorientering og message passing 8 (define (make-account) (let ((current 0)) (define (deposit amount) (set! current (+ current amount))) (define (withdraw amount) (deposit (- amount))) (define (balance) current) (define (dispatch message) (cond ((eq? message deposit) deposit) ((eq? message withdraw) withdraw) ((eq? message balance) balance))) dispatch))? (define erik (make-account))? (define oe (make-account))? ((oe deposit) 100)? ((oe balance)) 100? ((erik balance)) 0 Hvorfor kan vi ikke uten videre forenkle til f.eks. (oe deposit 100)?
På sidespor: dotted pairs vs. lister 9 Hvordan Scheme viser cons-celler:? (cons 1 2) (1. 2)? (cons 1 (cons 2 ())) (1 2)? (cons (list 1 2) 3) ((1 2). 3) I bunnen er (1 2 3) en cons-kjede (1. (2. (3. ()))); Er cdr-verdien et par sløyfer Scheme punktum og parentes:. (.? (1. (2. (3. ()))) (1 2 3) Dermed kan vi skrive prosedyrer som tar et variabel antall argumenter: (define (map fn. lists)...)
Polyadiske prosedyrer 10 (define (n-ary-append. lists) (define (recurse lists) (if (null? lists) () (append (car lists) (recurse (cdr lists))))) (recurse lists))? (n-ary-append (1 2) (3 4) (5 6)) (1 2 3 4 5 6) Men hvorfor trengs det egentlig den indre rekursive prosedyren? (define (n-ary-append. lists) (if (null? lists) () (append (car lists) (n-ary-append (cdr lists))))) Hva er verdien til lists-parameteret i første og andre oppkalling?
På en annen sidespor: identitet vs. ekvivalens 11? (define foo foo)? (define bar foo)? (eq? foo bar) #t? (eq? foo bar) #f? (define baz (1 2 3))? (eq? baz (1 2 3)) #f? (equal? baz (1 2 3)) #t? (equal? baz (1 (2) 3)) #f eq? tester identitet, som i Scheme tolkes som samme sted (i minne). equal? tester strukturell ekvivalens mellom (muligens nøstete) par; equal? kan lett implementeres som en polymorf rekursiv prosedyre.? (define mee baz)? (eq? mee baz) #t Flere (kalle)navn kan brukes for én og det samme objektet aliasing.
Hvordan teste for ekvivalens? (define (equal foo bar) (cond ((and (number? foo) (number? bar)) (= foo bar)) ((and (pair? foo) (pair? bar)) (and (equal (car foo) (car bar)) (equal (cdr foo) (cdr bar)))) (else (eq? foo bar)))) Men hva med identitet vs. ekvivalens mellom to prosedyrer?? (define erik (make-account))? (define oe (make-account))? (define liv oe)? (eq? erik oe) #f? (equal? erik oe) #f? (eq? liv oe) #t? ((oe withdraw) 100)? (eq? liv oe) #t 12
En gang i tid: substitusjonsmodellen 13 Prosedyrer evalueres på samme måte som beskrevet og praktisert tidligere. Først evalueres uttrykkene i argumentposisjon, deretter anvendes funksjonen. Vi kan tenke oss at funksjonsuttrykk skrives om på følgende måte: Hele uttrykket erstattes av prosedyrekroppen i definisjonen. Argumentnavnene i definisjonen erstattes av verdiene til uttrykkene i argumentposisjon. Dette kalles substitusjonsmodellen. Eksempel (define square (lambda (x) (* x x))) (square (+ 2 1)) (square 3) (* 3 3) 9
Substitusjonsmodellen passer ikke med muterbar data 14 (define (make-withdraw balance) (lambda (amount) (set! balance (- balance amount)) balance)) Vi må ta hensyn til at første argumentet til set! er spesielt; men ellers kan vi jo bare prøve å bruke den samme oppskriften: ((make-withdraw 25) 20) ((lambda (amount) (set! balance (- 25 amount)) 25) 20) (set! balance (- 25 20)) 25 (set! balance 5) 25 Den lokale variablen balance brukes tre ganger i prosedyrekroppen. Substitueres 2. og 3. bruk av balance samtidig blir resultatet feil.
Omgivelsesmodellen kan forklare alt 15 En omgivelse (environment) er en samling av rammer (frames); Hver ramme inneholder bindinger (bindings), dvs. navn, verdi -par; I tilleg har hver ramme en peker til sin omsluttende omgivelse; Prosedyreobjekter er par: prosedyrekoden koblet til en omgivelse.
Noen kommentarer til omgivelsesmodellen 16 SICP bruker ramme og omgivelse om hverandre noen steder. Evaluering av uttrykk kan nå defineres i forhold til en omgivelse. define og set! brukes for å etablere eller endre bindinger. Prosedyreoppkalling (og dermed også let) etablerer nye rammer. Rammer nøstes innenfor hverandre og danner så omsluttende rammer. Når et symbol evalueres finnes den nærmeste omsluttende bindingen. Nøstete bindinger for samme navnet overskygger (shadow) hverandre.
Repetisjon: omgivelsesmodellen og let? (define foo 42)? (let ((x foo) (y 1)) (list x y)) (42 1)? (let ((x foo) (y foo)) (list x y)) (42 42)? (let ((x foo) (y x)) (list x y)) error: x undefined? (let ((foo 7) (y foo)) (list foo y)) (7 42)? (let ((foo 7)) (let ((y foo)) (list foo y))) (7 7)? (let* ((foo 7) (y foo)) (list foo y)) (7 7) 17
Tilbake til innkapsling (1/3) 18 Innkapsling kan nå avmystifiseres på grunnlag av omgivelsesmodellen: (define (make-withdraw balance) (lambda (amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient Funds"))) (define w1 (make-withdraw 100)) Kroppen til make-withdraw returnerer et prosedyreobjekt (dvs. par); Oppkallingen av make-withdraw etablerer en ny ramme, som så blir omgivelsen til den anonyme prosedyren som lambda returnerer.
Tilbake til innkapsling (2/3) 19 Innkapsling: balance er kun tilgjengelig i omgivelsen E1, ikke globalt.
Tilbake til innkapsling (3/3) 20 Oppkalling av prosedyreobjektet w1 etablerer nok en ny ramme; som nøstes innenfor omgivelsen til w1 (med innkapsulert balance)