Memoisering. I de følgende memoiseringeksemplene brukes tabeller, og vi tar derfor først en repetisjon av dette.

Like dokumenter
Memoisering. I de følgende memoiseringeksemplene brukes tabeller, og vi tar derfor først en repetisjon av dette.

INF2810: Funksjonell Programmering. En Scheme-evaluator i Scheme, del 2

INF2810: Funksjonell Programmering. En Scheme-evaluator i Scheme, del 2

Memoisering, utsatt evaluering og strømmer

Memoisering, utsatt evaluering og strømmer

INF2810: Funksjonell Programmering. En Scheme-evaluator i Scheme, del 2

INF2810: Funksjonell Programmering. En metasirkulær evaluator, del 2

INF2810: Funksjonell Programmering. En metasirkulær evaluator, del 2

INF2810: Funksjonell Programmering. Strømmer og utsatt evaluering

Gjennomgåelse av eksamensoppgaven i HUMIT2710 fra våren 2004

INF2810: Funksjonell Programmering. Strømmer og utsatt evaluering

INF2810: Funksjonell Programmering. Mer om strømmer

INF2810: Funksjonell Programmering. Mer om strømmer

INF2810: Funksjonell Programmering. Mer om verditilordning og muterbare data.

INF2810: Funksjonell Programmering. Mer om verditilordning. Tabeller. Og strømmer.

INF2810: Funksjonell Programmering. Mer om verditilordning og muterbare data.

INF2810: Funksjonell Programmering. Kommentarer til prøveeksamen

(define (naer-nok-kuberot? y x) (< (abs (- (kube y) x)) 0.001)) (define (naermere-kuberot y x) (/ (+ (* y 2) (/ x (kvadrat y))) 3))

INF2810: Funksjonell Programmering. Mer om verditilordning. Tabeller. Og strømmer.

INF2810: Funksjonell Programmering. Utsatt evaluering og strømmer

INF2810: Funksjonell Programmering. En metasirkulær evaluator

INF2810: Funksjonell Programmering. Utsatt evaluering og strømmer

INF2810: Funksjonell Programmering. En metasirkulær evaluator

INF2810: Funksjonell Programmering

Eksamen i SLI230, vår 2003.

UNIVERSITETET I OSLO

INF2810: Funksjonell Programmering. En Scheme-evaluator i Scheme

INF2810: Funksjonell Programmering. Utsatt evaluering og strømmer

UNIVERSITETET I OSLO

INF2810: Funksjonell Programmering

INF2810: Funksjonell Programmering. Utsatt evaluering og strømmer

INF2810: Funksjonell Programmering

INF2810: Funksjonell Programmering

Høyere-ordens prosedyrer

Rekursjon og lister. Stephan Oepen & Erik Velldal. 1. februar, Universitetet i Oslo

INF2810: Funksjonell Programmering. En Scheme-evaluator i Scheme

INF2810: Funksjonell programmering: Mer om Scheme. Rekursjon og iterasjon.

INF2810: Funksjonell Programmering. Dataabstraksjon og Trerekursjon

Eksamen i HUMIT 2710, Funksjonell programmering, våren Ingen hjelpemidler er tillatt. <resten av forsiden> Side 1 av 7

INF2810: Funksjonell Programmering. Lister og høyereordens prosedyrer

INF2810: Funksjonell Programmering. Lokale variabler. Og trær.

INF2810: Funksjonell Programmering. Lister og høyereordens prosedyrer

INF2810: Funksjonell Programmering. Lokale variabler. Og trær.

Innlevering 2b i INF2810, vår 2017

INF2810: Funksjonell Programmering. Eksamensforberedelser

UNIVERSITETET I OSLO

INF2810: Funksjonell Programmering. Tilstand og verditilordning

INF2810: Funksjonell Programmering. Strømmer

INF2810: Funksjonell Programmering

INF2810: Funksjonell Programmering. Strømmer

INF2810: Funksjonell Programmering

Par og Lister (først et par sider fra forrige uke) Par er byggestener for lister og trær og sammensatte datatyper.

Side 1. Oppgave 1. Prosedyrer 1.1. Prosedyrene f og g skal begge returnere prosedyrer. a. Skriv f slik at ((f a) b) returnerer summen av a og b.

INF2810: Funksjonell Programmering. Køer, tabeller, og (litt om) parallelitet

INF2810: Funksjonell Programmering. Tilstand og verditilordning

INF2810: Funksjonell Programmering. Tilstand og verditilordning

UNIVERSITETET I OSLO

INF2810: Funksjonell Programmering. Køer, tabeller, og (litt om) parallelitet

Appendiks A Kontinuasjoner

Innlevering 2a i INF2810, vår 2017

INF2810: Funksjonell Programmering. Tilstand og verditilordning

INF2810: Funksjonell Programmering. Mengder og lokal tilstand

INF2810: Funksjonell Programmering. Trær og mengder

INF2810: Funksjonell Programmering. Trær og mengder

LISP PVV-kurs 25. oktober 2012

INF2810: Funksjonell Programmering. Trær og mengder

INF2810: Funksjonell Programmering. Muterbare data

Vi skal se på lambda-uttrykk. Følgende er definerte og vil bli brukt gjennom oppgaven

Kap. 4 del I Top Down Parsering INF5110 v2006. Stein Krogdahl Ifi, UiO

INF2810: Funksjonell programmering: Introduksjon

Det er ikke tillatt med andre hjelpemidler enn de to sidene som er vedlagt oppgavesettet. Følgende funksjoner er definert og brukes i oppgaven:

Anatomien til en kompilator - I

Anatomien til en kompilator - I

INF2810: Funksjonell Programmering. Mer om Scheme. Rekursjon og iterasjon.

Kap. 4: Ovenfra-ned (top-down) parsering

Lisp 2: Lister og funksjoner

Destruktuktive listeoperasjoner

Destruktuktive listeoperasjoner

Par og Lister (først et par sider fra forrige uke) Par er byggestener for lister og trær og sammensatte datatyper.

Plan: Parameter-overføring Alias Typer (Ghezzi&Jazayeri kap.3 frem til 3.3.1) IN 211 Programmeringsspråk

Oppgave 1 Minimum edit distance

INF2810: Funksjonell Programmering. Mer om Scheme. Rekursjon og iterasjon.

Notat 2, ST Sammensatte uttrykk. 27. januar 2006

Kap.4 del I Top Down Parsering INF5110 v2005. Arne Maus Ifi, UiO

INF2810: Funksjonell Programmering. Oppsummering og eksamensforberedelser

Python: Løkker. TDT4110 IT Grunnkurs Professor Guttorm Sindre

INF2810: Funksjonell Programmering. Oppsummering og eksamensforberedelser

Hjemmeeksamen 2 i INF3110/4110

INF5110 V2012 Kapittel 4: Parsering ovenfra-ned

Obligatorisk oppgave 1 INF1020 h2005

INF2810: Funksjonell Programmering. Mer om Scheme. Rekursjon og iterasjon.

INF5110 V2013 Stoff som i boka står i kap 4, men som er generelt stoff om grammatikker

TDT Øvingsforelesning 1. Tuesday, August 28, 12

INF2810: Funksjonell Programmering. Oppsummering og eksamensforberedelser

INF2810: Funksjonell programmering: Introduksjon

INF2810: Funksjonell Programmering. Oppsummering og eksamensforberedelser

Oppsummering fra sist

Notat 2, ST januar 2005

INF2810: Funksjonell Programmering. Oppsummering og eksamensforberedelser

INF2810: Funksjonell Programmering. Huffman-koding

Transkript:

Memoisering I de følgende memoiseringeksemplene brukes tabeller, og vi tar derfor først en repetisjon av dette. Vi definere en allmenn tabelltype ved en prosedyre med - tabellen som en lokal tilstandsvariabel, - lokale rutiner for å hente frem og legge inn data, samt - en meldingsbehandlingsprosedyre. (define (make-table) (let ((table (list *table-head*))) (define (lookup line-key col-key)...) (define (insert! line-key col-key value)...) (define (dispatch m) (cond ((eq? m 'lookup) lookup) ((eq? m 'insert!) insert!) (else (error "Unknown operation -- TABLE" m)))) dispatch)) 455

(define (lookup line-key col-key) (let ((line (assoc line-key (cdr table)))) (and line ; andre term evalueres bare hvis line #f. (let ((entry (assoc col-key (cdr line)))) (and entry (cdr entry)))))) ; andre term evalueres bare hvis entry #f. (define (insert! line-key col-key value) (let ((line (assoc line-key (cdr table)))) (if line (let ((entry (assoc col-key (cdr line)))) (if entry (set-cdr! entry value) (set-cdr! line ; fant ikke kolonnenøkkelen, (cons (cons col-key value) ; så vi legger inn en ny entry (cdr line))))) ; først på linjen (set-cdr! table ; fant ikke linjenøkkelen, (cons (list line-key ; en ny linje (cons col-key value)) ; med én enkelt entry (cdr table)))))))) ; først i tabellen 456

Ordet memoisere betegner det å høres ut som memorisere og minner om memorere, lagre resultatet av en regneoperasjon, slik at neste gang samme operasjon evt. skal utføres, så kan det allerede utregnede resultatet ganske enkelt hentes frem. Det må da dreie seg om - en bestemt type operasjoner, - og en tilsvarende prosedyre - som selv holder rede på de resultater som allerede foreligger. Exercise 3,27 (s. 272) i SICP dreier seg om memoisering av resultater fra beregning av fibonacci-tall, men vi tar en kort motiverende omvei der vi ser først på memoisering av resultater fra multiplikasjoner. Vi tenker oss et system for håndtering av valutatransaksjoner som - utfører en rekke divisjoner med et Liten vits i å gjøre dette hvis det er - begrenset utvalg av faktorer bestående av ulike valutakurser. en stor mengde mulige argumenter 457

For memoiseringen benytter vi en todimensjonal tabell. (define (memoize-div) (let ((table (make-table))) (lambda (x y) (let ((known-result ((table 'lookup) x y))) (or known-result ; enten fant vi det tidligere beregnede resultatet og returnerer det, (let ((result (/ x y))) ; eller så regner vi det ut nå, og ((table 'insert!) x y result); legger det inn i tabellen, result)))))) ; før vi returnerer det (define div (memoize-div)) (div 18 3) 6 (div 18 3) 6 Her ser vi ikke hva som foregår, men, om vi, for illustrasjonens skyld, bytter ut første term i or-uttrykket med (and known-result (cons 'memoized known-result)) og returverdien fra let-uttrykket med (cons 'computed known-result) får vi (div 18 3) (computed. 6) (div 18 3) (memoized. 6) NB! Dette er ikke det vi vil ha. Selv om memoisering innebærer tilstandsendring bak kulissene, fastholder vi det funksjonelle paradigmet og kravet om at samme argument skal gi samme resultat svekkes ikke. 458

Så til memoiseringen av fibonacci-tallene: Utgangspunktet er bokas opprinnelige rekursive, eksponensielt voksende, utgave av fibonacci-funksjonen. (define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) De blå numrene angir kallrekkefølgen. 459

For å memoisere denne bruker vi en endimensjonal tabell med følgende (globale) prosedyrer: (define (make-table) (list '*table*)) (define (lookup key table) (let ((record (assoc key (cdr table)))) ; Verdien til record blir enten det vi evt. fant, eller #f. (and record (cdr record)))) ; (if record (cdr record) #f) (define (insert! key value table) (let ((record (assoc key (cdr table)))) (if record ; Fant vi en tidligere utregning av denne (set-cdr! record value) ; Erstatt tidligere med aktuell verdi * (set-cdr! table ; Ingen record her, så (cons (cons key value) ; sett inn ny med aktuell verdi (cdr table)))))) Vi er ikke interessert i noen returverdi fra insert! bare at tabelllen blir endret. * Denne hører med i den generelle tabellen, men er ikke relevant for dette eksempelet. Her kaller vi insert! bare for å sette inn nye verdier ikke for å endre ekstisterende. Set neste side. 460

En memoiseringsprosedyre for en hvilken som helst ett-arguments-prosedyre f kan skrives slik: (define (memoize f ) ; Det er resultatene av eventuelle kall på f som skal memoiseres. (let ((table (make-table))) ; Vi lager en omgivelse der både tabellen og f inngår. (lambda (x) (let ((result (lookup x table))) (or result ; Enten fant vi et tidligere utregnet resultat, og returnerer dette, * (let ((result (f x))) ; eller så må vi regne ut resultatet, og (insert! x result table) ; legge det i tabellen, result)))) ; før vi returnerer det. )) Et kall på memoize gir oss en prosedyre hvis lokale omgivelser inneholder funksjonen f og tabellen table. Det er denne prosedyren som returneres fra kallet på memoize i definisjonen av memo-fib på neste side. * Angående or-uttrykket over: (or a b) gir samme resultat som (if a a b). 461

Vi bruker memoize til å definere en prosedyre memo-fib, og som argument til memoize sender vi en variant av fibonacci-prosedyren der de rekursive kallene går via kall på memo-fib slik: (define memo-fib (let ((fib (lambda (n) ; fib = f i memoize (if (> n 2) n (+ (memo-fib (- n 1)) (memo-fib (- n 2))))) (memoize fib))) Merk at koden til prosedyreobjektet memo-fib ikke er fib, men derimot lambdauttrykket i memoize. De lokale omgivelsene til memo-fib er således memoize, der prosedyrevariabelen f bindes til fib ved kallet (memoize fib). Det rekursive kallet på memo-fib går dermed via kallet på f dvs fib i memoize. mens det rekursive kallet på fib går via kallet på memo-fib i fib. 462

463

Så gjenstår det bare å forklare hva som skjer når vi kaller memo-fib f.eks. slik: (memo-fib 5) Vi ser en gang til på rekursjonsmønstret for den opprinnelige versjonen: Det vi vil med memoiseringen er å utnytte det faktum at vi her har 15 rekursive kall med bare 6 ulike argumenter. Vi ser at kallkjeden går nedover langs venstregrenene fib(4), fib(3), fib(2), fib(1), og i og med høyre bladnode i nederst venstre subtre, fib(0) er alle de gjenværende kallene i treet alt uført. Merke at for hvert tidligere utført kall, kuttes hele subtreet. 464

Utsatt evaluering Skillet i Scheme mellom ordinære uttrykk og spesialformer angår bl.a. om, og evt. når, deler av sammensatte uttrykk evalueres. Regelen for applikativ evaluering sier at - alle deluttrykk i et sammensatt uttrykk må evalueres - før hele uttrykket kan evalueres (ved at første term anvendes på de øvrige), Men dette gjelder ikke for spesialformene, dvs. uttrykk som innledes med if, cond, and eller or. 465

Vi kan få til noe som ligner på spesialformer, ganske enkelt ved å pakke inn deluttrykk i spesialformen lambda uten argumenter. (define (pakket? exp) (procedure? exp)) Vi pakker ut et innpakket uttrykk ved å kalle det. (define (pakk-ut exp) (if (pakket? exp) (exp) ; pakk det ut, dvs. kall det. exp)) ; returnér det som det er Noen språk som Algol og Simula (som har bl.a. Algol som basis) har syntaks for såkalt navnoverføring av argumenter (call by name). Dette innebærer nettopp det vi snakker om her. Det uttrykket som (om nødvendig) må evalueres, for at et argument skal få sin verdi, kalles da en thunk. (pakk-ut (lambda () (+ 2 3))) 5 (pakk-ut (+ 2 3)) 5 (define pakket-sum (lambda () (+ 2 3))) pakket-sum #<procedure:pakket-sum> (pakket-sum) 5 (pakk-ut pakket-sum) 5 466

(define (test a b) (if (= (pakk-ut a) 0) 'a=zero (pakk-ut b))) (define a 2) (test a (/ 1 a)) ; ==> 1/2 (test (lambda () a) (lambda () (/ 1 a))) ; ==> 1/2 Nå kaller vi test og med et argumentet som garantert gir kjøreavbrudd, dersom det blir evaluert. (define a 0) (test a (/ 1 a)) ; ==> /: division by zero Men når vi pakker inn uttrykket, evaluerer selve pakken til en prosedyre, vi kommer inn i prosedyren, og siden a = 0, blir ikke uttrykket pakket ut (test (lambda () a) (lambda () (/ 1 a))) ; ==> a=zero 467

Vår utpakkingsprosedyre pakk-ut, skiller seg fra Schemes standardprosedyre for utpakking (som er den vi skal bruke etter hvert). For å kunne demonstrere test-prosedyren, har vi definert vår utpakkingsprosedyre slik at den aksepteres både uinpakkede og innpakkede argumenter, men som vi straks skal se: standardprosedyren for utpakking i Scheme aksepterer kun innpakkede argumenter. (Og dette er vel i og for seg rimelig. Det kunne ha virket forvirrende om f.eks. et argument av typen heltall skulle gi samme resultat som et argument av typen prosedyre.) 468

Vi kan ikke ha noen egen prosedyre for innpakking, ettersom "innpakkingen" da i seg selv ville ha forårsaket en evaluering, og dermed ødelagt hele poenget. Gitt en definisjon som (define (pakk-inn exp) (lambda () exp)) ville vi ved kallet (pakk-inn (+ 2 3)) først ha fått evaluert argumentet (+ 2 3) 5, slik at exp i kroppen til pakk-inn ble bundet til 5 og ikke til uttrykket (+ 2 3). 469

Men nå finnes det en spesialform for innpakking i Scheme, nemlig delay. Assossiert til denne er metaforparet løftegivning og -avtvinging. Vi sier at resultatet av en utsatt delayed evaluering, er et løfte om en fremtidig evaluering, og at dette løftet, om nødvendig, kan avtvinges. (define løfte (delay (+ 2 3))) løfte #<struct:promise> (promise? løfte) #t En annen utsett-fremtving-relasjon finner vi mellom quote og eval. (eval (quote (+ 2 3))) 5 (eval '(+ 2 3)) 5 Det er altså klare likheter mellom relasjonene quote eval, lambda kall og delay force, men det er også viktige forskjeller mellom dem. (force løfte) 5 (løfte) procedure application: expected procedure, given: #<struct:promise> (no arguments) Siste linje viser at delay, i motsetning til innpakking vha lambda, ikke returner en prosedyre. NB! dette gjelder Racket. Det finnes Scheme implementasjoner der et løfte ikke er en egen datatype men ganske enkelt er et lambda-objekt. 470

Primitiven force tar et løfte, laget av delay, som argument og avtvinger innfrielsenav dette om det da ikke alt var innfridd. Et løfte kan sees som et memoiserende prosedyreobjekt. Når uttrykket i løftet evalueres første gang, lagres resultatet i løftets omgivelser, slik at det gankske enkelt kan hentes frem ved eventuell senere bruk av force. Dette kan vi illustrere ved å legge et ekstra lag rundt uttrykket i form av en utskrift. Utskriften kommer bare ved evalueringen altså første gang løftet avtvinges. (define løfte (delay (begin (display 'yo) (+ 2 3)))) (force løfte) yo5 Her vises utskriften fra REPL. (force løfte) 5 Merk at display-setningen ikke bidrar til resultatet. Slike bieffekter (side effects) kan vi fremkalle i illustrasjonsøyemed, men i et program, ville vi aldri finne på å gjøre noe slikt i en sammenheng som denne. 471

En datatype der utsatt evaluering er helt essensielt, er en egen form for par der - car-delen alltid er umiddelbart tilgjengelig, mens - cdr-delen kun foreligger som et løfte om å produsere noe. Til dette formålet kan car virke som for vanlige par, mens vi trenger en cdr-selektor som tvinger cdr-delen til å innfri sitt løfte. (define (force-cdr pair) (force (cdr pair))) Det å utstyre et programmeringsspråk med mekanismer for utsatt evaluering, som call by name, se tekstboksen på s. 410, kan bl.a. begrunnes ut fra et ønske om å kunne sende potensielt tunge beregninger, som ikke nødvendigvis skal utføres, som argumenter til prosedyrer. Men det finnes også problemer for hvilke utsatt evaluering er inherent i løsningen, som f.eks. løsning av differensialligninger der vi må utsette evalueringen av integranden for overhodet å komme i gang. NB! Disse navnene er midlertidige. Vi skal etter hvert knytte denne typen par til strømmer, og der er navnene hhv. stream-car og stream-cdr. 472

Vi kan lage slike par etter to litt ulike prinsipper. Legg nøye merke til hva som er kall her: (define (gjenta) (cons 'jada (delay (gjenta)))) ; (1) gjenta ; ==> #<procedure:gjenta> (2) (gjenta) ; ==> (jada. #<struct:promise>) (3) (car (gjenta)) ; ==> jada (4) (car (force-cdr (gjenta))) ; ==> jada (5) (car (force-cdr (force-cdr (gjenta)))) ; ==> jada (6) I (4) ber vi om første element i det paret som ble generert av et kall på gjenta. I (5) ber vi om første element i det paret som ble generert av et gjentatt kall på gjenta. I (6) ber vi om første element i det paret som ble generert av andre gangs gjentatte kall på gjenta. 473

Se så på disse setningene. (define repetér (cons 'jada (delay repetér))) ; (7) repetér ; ==> (jada. #<struct:promise>) (8) (car repetér) ; ==> jada (9) (car (force-cdr repetér)) ; ==> jada (10) (car (force-cdr (force-cdr repetér))); ==> jada (11) Mens gjenta i (1) ble definert som en prosedyre som genererer et par, blir det tilsvarende objektet i (7) definert ganske enkelt som et par, men vel å merke, et par hvis cdr-del inneholder et løfte om gjentagelse. Vi kaller det siste en implisitt gjentaglse. Følgende, som er ekvivalent med (7), viser noe av det som foregår (define repetér (cons 'jada (12) (delay (cons 'jada (delay repetér))))) 474

I (9) ber vi ganske enkelt om første element i paret repetér. (car repetér) ; ==> jada (9) I (10) ber vi om første element i det paret som ble generert ved innfrielsen av løftet i andre del av paret repetér. (car (force-cdr repetér)) ; ==> jada (10) I (11) ber vi om første element i det paret som ble generert ved innfrielsen av løftet i andre del av innfrielsen av løftet i andre del av paret repetér. (car (force-cdr (force-cdr repetér))); ==> jada (11) Vi setter inn definisjonen av repetér i (10), etter at den utsatte evalueringen er utført: (car (cdr (cons 'jada (cons 'jada (delay repetér))))) (13) Tilsvarende kan vi, for å forstå hva som skjer i (11) gjøre enda en substitusjon. (car (cdr (cons 'jada (cons 'jada (cons 'jada (delay repetér)))))) (14) 475

Sammenlign (3) og (8) (gjenta) ; ==> (jada. #<struct:promise>) (3) repetér ; ==> (jada. #<struct:promise>) (8) Hvorfor trenger vi den eksplisitte formen når den ekslisitte og den implisitte formen her gir samme resultat? Poenget er at prosedyren i den eksplisitte formen gir mulighet for mer enn rene gjentagelser. Her er et eksempel der vi sender et argument til en gjentagelsesproduserende prosedyre og bruker dette i et regnestykke som gir en ny verdi til neste løfte. (define (heltall fra) (cons fra (delay (heltall (+ fra 1))))) ; (15) (car (heltall 1)) ; 1 (16) (car (force-cdr (heltall 1))) ; 2 (17) (car (force-cdr (force-cdr (heltall 1)))) ; 3 (18) 476

I neste eksempel ser vi på en liste av gjentatte gjentagelser. Vi kan samle opp et gitt antall gjentagelsene i en liste slik (define (gjenta->liste gjentagelse n) (if (= n 0) '() (cons (car gjentagelse) (gjenta->liste (force-cdr gjentagelse) (- n 1))))) (gjenta->liste (gjenta) 3) ; (jada jada jada) (gjenta->liste repetér 3) ; (jada jada jada) (gjenta->liste (heltall 1) 3) ; (1 2 3) Det er fullt mulig å definere en prosedyre som genererer et endelig antall gjentagelser. SICP inneholder en definisjon av en prosedyre som konverterer en strøm av gentagelser til en liste, uten å telle antall elementer. I stedet tester prosedyren for nil (vi skal straks se hva dette betyr). En slik prosedyre kan bare brukes på en endelig strøm. 477

Den indirekte (implisitte) formen er ikke særlig interessant alene, men den kan gi input til andre mer interessante gjentagelsesstrømmer. (define enere (cons 1 (delay enere))) (define toere (cons 2 (delay toere))) (define (tell n enheter) (cons n (delay (tell (+ n (car enheter)) (force-cdr enheter))))) (gjenta->liste (tell 1 enere) 10) ; (1 2 3 4 5 6 7 8 9 10) (gjenta->liste (tell 1 toere) 10) ; (1 3 5 7 9 11 13 15 17 19) (gjenta->liste (tell 2 toere) 10) ; (2 4 6 8 10 12 14 16 18 20) Prosedyren tell synes å måtte gi en evig løkke, men pga. delay, produserer den bare ett nytt par for hver gang den blir kalt, og dette gjør den vha. første gjenværende ledd i strømmen enheter. 478

Egendefinerte spesialformer Det som mangler i vår implementasjon, er en konstruktor, men vi kan ikke definere noen prosedyre som tar som argument noe som (i første omgang) ikke skal evalueres. Det vi trenger er vår egen spesialform, og Scheme gir oss da også muligheten for å definere en slik. Vi kunne kalle vår egendefinerte spesialform cons-and-delay, men for å slippe å bytte navn i neste omgang kaller vi den cons-stream (som vi straks skal bruke til å lage strømmer). 479

En definisjon av en spesialform - innledes med ordet define-syntax, - fulgt av navnet på den formen vi skal definere, heretter kalt nøkkelordet. - Deretter følger reglene for transformasjon av vår spesialform til basisformer i Scheme. - Transformasjonsreglene inngår i en liste som - innledes med ordet syntax-rules - fulgt av en liste med eventuelle reserverte ord, - fulgt av en liste med én eller flere transformasjonsregler. En transformasjonsregel har formen (mønster utførelse). Mønsteret det evaluatoren skal gjenkjenne er - en liste med nøkkelordet, fulgt av - ingen, ett eller flere ord som er variabler i transformasjonsregelen. Utførelsen kan bektraktes som regelens kropp. Den består av - en eller flere Scheme-setninger der eventuelle variabler i mønsteret inngår. 480

Vi definerer formen hverken-eller (define-syntax hverken-eller ; Navnet på spesialformen (syntax-rules () ; Intet reservert ord ((hverken-eller test1 test2) ; Mønstret som skal gjenkjennes (not (or test1 test2))))) ; Malen (kroppen) som skal utføres (hverken-eller (= (+ 2 2) 4) (= (+ 2 2) 4)) ==> #f (hverken-eller (= (+ 2 2) 4) (= (+ 2 2) 5)) ==> #f (hverken-eller (= (+ 2 2) 3) (= (+ 2 2) 4)) ==> #f (hverken-eller (= (+ 2 2) 3) (= (+ 2 2) 5)) ==> #t begge er sanne det første er sant og det andre er usant det første er usant og det andre er sant begge er usanne I de to første uttrykkene evaluer den første termen til #t, og dermed er det ikke nødvendig å evaluere den andre. Vi legger inn en effekt og ser hva REPL gir (hverken-eller (display "hverken ") (display "eller ")) gir hverken #f (hverken-eller (not (display "hverken ")) (display "eller ")) gir hverken eller #f Returverdien fra display og andre effektprosedyrer er uspesifisert i R5RS, men den kan aldri være #f. 481

Følgende definisjon av en eksklusiv eller, gir ingen besparelse: (define-syntax enten-eller ; Navnet på spesialformen (syntax-rules () ; Intet reservert ord ((enten-eller test1 test2) ; Mønstret som skal gjenkjennes (or (and test1 (not test2)) ; Malen (kroppen) som skal utføres (and (not test1) test2))))) (enten-eller (= (+ 2 2) 4) 'månen-er-en-gul-ost) #f (enten-eller (= (+ 2 2) 3) 'månen-er-en-gul-ost) 'månen-er-en-gul-ost Uansett om første term evaluerer til #t eller #f, må den andre termen også evalueres, for å få sjekket om den evaluerer til det motsatte. 482

Mulig, men kanskje ikke særlig pent Ved å bruke et reservert ord så som eller kunne vi ha laget formene (hverken a eller b) og (enten a eller b). (define-syntax hverken ; Navnet på spesialformen (syntax-rules (eller) ; Reservert ord ((hverken test1 eller test2) ; Mønstret som skal gjenkjennes (not (or test1 test2))))) ; Malen (kroppen) som skal utføres (hverken #f eller #f) #t Her er eller en ren dekorasjon. Ordet dukker opp mellom to signifikante termer men er i og for seg uten betydning, og dermed får vi ikke noe S-uttrykk, der alle ledd evalueres og første ledd anvendes på de øvrige. 483

Definisjonen av cons-stream følger samme mønster. (define-syntax cons-stream ; Navnet på spesialformen (syntax-rules () ; Ingen andre reservert ord ((cons-stream obj stream) ; Mønstret som skal gjenkjennes (cons obj (delay stream))))) ; Malen (kroppen) som skal utføres Ved hjelp av cons-stream kan vi skrive om (1), (5) og (11) slik: (define (gjenta) (cons-stream 'jada (gjenta))) ; (1') (define repetér (cons-stream 'jada repetér)) ; (6') (define (tell fra) (cons-stream fra (tell (+ fra 1)))) ; (13') (define enere (cons-stream 1 enere)) Dette er strømmer, som har selektorene stream-car og stream-cdr. (define (stream-car obj) (car obj)) (define (stream-cdr obj) (force (cdr obj))) Med disse kan vi definere tellestrømmen slik: (define (tell n enheter) (cons-stream n (tell (+ n (stream-car enheter)) (stream-cdr enheter)))))) 484

Prosedyre for memoisering av prosedyrer Vi minner om syntaks-definisjonen av spesilaformen cons-stream over: (define-syntax cons-stream (syntax-rules () ((cons-stream obj stream) (cons obj (delay stream))))) Spesilaformen delay skal være definert i Scheme (i hht. R5RS), men om den ikke var det, kunne vi ha definert den selv slik: (define-syntax delay (syntax-rules () ((delay expression) (memo-proc ; se under (lambda () expression))))) 485

Prosedyren memo-proc (se under) tar som argument en argumentløs prosedyre, en"lambda-pakke", med et eller annet uttrykk som kropp og returnerer en prosedyre som tar ingen, ett eller flere argumenter, og som har to lokale variabler: - én for lagring av et eventuelt ferdig beregnet resultat fra det innpakkede uttrykket og - én for å holde rede på om uttrykket er evaluert eller ikke. Første gang det innpakkede uttrykket evalueres, lagres resultatet resultat-variabelen, før det returneres. Alle eventuelle etterfølgende ganger returneres det lagrede resultatet. Legg for øvrig merke til følgende: (define p (cons 1 (delay 2))) p ==> (1. #<promise:xxx>) (force (cdr p)) ==> 2 p ==> (1. #<promise:2>) Et avtvunget løftet er fortsatt et løfte. 486

Neste gang prosedyren kalles, returneres ganske enkelt det tildligere beregnede resulatet. (define (memo-proc lambda-wrapped-expression) (let ((already-run? #f) ; begge disse verdiene gjelder bare frem til første kall, (result #f)) ; bortsett fra at det faktiske resultatet kan være #f, og ; det er derfor vi trenger et eget flagg for om evalueringen alt er gjort (lambda () (if already-run? result Merk forskjellen mellom (begin (set! already-run? #t) (set! result (lambda-wrapped-expression))); utfør det innpakede uttrykket result))) - den argumentløse prosedyren som sendes til memo-proc, og - det uttrykket som er pakket inn i denne prosedyren. Det siste kan godt være et kall på en prosedyre som tar argumenter. 487

I SICP Exercise 3.52 og 3.53 dreier det seg om å se hva som foregår, når en strøm produseres, og det kan da være greit å kunne slå memoiseringen av og på. For å få til dette definerer vi vår egen utgave av delay sammen med en hjelpeprosedyre og en global variabel: (define *memoize* #t) (define (set-memoize! on/off)(set! *memoize* on/off)) (define-syntax delay (syntax-rules () ((delay expression) (if *memoize* (memo-proc (lambda () expression)) (lambda () expression))))) Som vi ser, er *memoize* et globalt flagg som bestemmer om delay skal memoisere eller ikke, og som vi kan heise og fire vha set-memoize! 488

NB! delay skaper et objekt av typen promise som er den eneste typen force aksepterer, så når vi definerer vår egen delay, må vi også definere vår egen force. (define (force expression) (expression)) ; utfør kallet Her er et eksempel på hvordan dette virker: (define (vis-og-returner noe) (display " ") (display noe) noe) (define (effekt-tall fra) (cons-stream (vis-og-returner fra) (effekt-tall (+ fra 1)))) (define noen-tall (effekt-tall 1)) 489

Følgende kall gir følgende effekter og returverdier (stream->list noen-tall 5) (1 2 3 4 5) ; Fra REPL: 1 2 3 4 5(1 2 3 4 5) (stream->list noen-tall 5) (1 2 3 4 5) ; Fra REPL: (1 2 3 4 5) Vi merker oss at vi bare får effekt første gang de tre løftene etter første element i noen-tall avkreves. Så slår vi av memoiseringen og ser hva som skjer. (set-memoize! #f) (define noen-tall (effekt-tall 1)) (stream->list noen-tall 5) (1 2 3 4 5) ; Fra REPL: 1 2 3 4 5(1 2 3 4 5) (stream->list noen-tall 5) (1 2 3 4 5) ; Fra REPL: 1 2 3 4 5(1 2 3 4 5) 490

Strømmer Mens en liste er en sekvens av et gitt antall objekter, er en strøm både en sekvens og en prosess., og mens en gitt liste til enhver tid har et gitt, endelig, antall elementer, har en strøm har til enhver tid enda ett element. Dette gjelder uendelige strømmer. Det går også an å lage endelige strømmer. Poenget med en strøm er at dens elementer produseres ettersom vi aksesserer dem. Altså, mens en liste foreligger i sin helhet med et endelig antall elementer, foreligger en strøm bare element for element, men med et i prinsippet uendelig antall elementer. Og når vi på fullt alvor kan snakke om strømmer i datamaskinprogrammer som uendelige størrelser, så er det nettopp fordi en strøm aldri realiseres i sin helhet. 491

Sekvensierte versus sammenpakkede prosesser Vi har tidligere sett hvordan vi kan løse sammensatte problemer ved hjelp av en sekvens av prosesser. F. eks. for å summere kvadratene av alle primtall i et gitt intervall, kan vi - generere sekvensen av alle tallene i intervallet, - filtrere ut ikke-primtallene fra denne sekvensen, - kvadrere de filtrerte tallene, og til slutt - summere de kvadrerte tallene. Dette gir greie og oversiktlige løsninger, men arbeidsmengden i en slik sekvens lett blir større enn om man hadde slått sammen flest mulig prosesser i en og samme iterasjon. Ikke minst ville man på den måten kunne unngå å generere en masse verdier som i siste instans allikevel ikke ville bli brukt. 492

Følgende eksempel illustrerer dette. Komprimert løsning: (define (sum-kvadrerte-primtall a b) (define (iter n sum) (cond ((> n b) sum) ((prime? n) (iter (+ n 1) (+ (kvadrat n) sum))) (else (iter (+ n 1) sum)))) (iter a 0)) Sekvens av sekvensielle prosesser: (define (sum-kvadrerte-primtall a b) (accumulate + 0 (map kvadrat (filter prime? (enumerer a b))))) Et, for visse formål, vesentlig aspekt ved den siste er at den i langt større grad enn den første likner en rent fysisk signalstrøm. 493

Mens den komprimerte løsningen klarer seg med telleren og summen (siden iter er halerekursiv, får vi ingen rekursjonsstack), må liste-til-liste-løsningen generere hele heltallssekvensen, før den kan begynne å lete etter heltall. Deretter produserer filtreringsprosedyren en ny sekvens og mappingprosedryen enda en, før akkumulatoren kan beregne summen. Ved hjelp av strømmer kan vi slå to fluer i en smekk, idet vi beholder sekvensen av atskilte prosesser, samtidig som vi ikke utfører flere beregninger enn det vi ville ha gjort med en hvilken som helst kompakt løsning. 494

Utsatt evaluering og memoisering Det magiske løsen her er utsatt evaluering og memoisering. Vi tenker oss at vi skal plukke ut ett og ett - primtallet fra listen av heltallene fra 10 til 1 000 000. Slik lister er operasjonalisert i Scheme, måtte alle listens 999991 elementer genereres, før vi kunne gå videre i prosessen, uansett hvor mange eller få primtall vi ønsket. Her plukker vi ut det andre primtallet 10 (altså 13). (car (cdr (filter prime? (enumerate 10 1000000)))) I læreboken startes det på 1 000, men vi starter før, for å kunne kjenne igjen primtallene. 495

Fra strømmen av heltall fra 10 til 1 000 000 kan jeg imidlertid plukke ut det andre primtallet, uten at det genereres mer enn fire tall. Ved hjelp av konstruktoren cons-stream og selektoren stream-car og stream-cdr kan vi definere ekvivalenter til alle de typiske listeoperasjonene som seleksjon mapping, filtrering, akkumulering, etc, slik at strømmer og lister, fra en funksjonell betraktning, er ekvivalente. (stream-car (stream-cdr (stream-filter prime? (stream-enumerate 10 10000)))) Dette er helt parallelt til det liste-baserte uttrykket over. 496

Vi minner om at - første gjenværende elementet i en strøm alltid er tilgjengelig vha. stream-car, mens det - for andre gjenværende elementet bare foreligger et løfte som vi må avtvinge, for å få tak i elementet. Med dette for øye kan vi se hva som foregår bak kulissene under evalueringen av uttrykket over. - Kallet (stream-cdr (stream-filter...)), tvinger stream-filter til å produsere sitt andre tall, men før dette kan skje, må stream-filter ha fått tilstrekkelig mange tall fra stream-enumerate. - For å kunne produsere sitt andre tall, må stream-filter - først hente (ikke tvinge) første tall fra stream-enumerate og - deretter tvinge stream-enumerate til å produsere sitt andre tall. 497

Dette gir hhv. tallene 10 og 11, hvorav stream-filter vraker det første og aksepterer det andre som det dermed kan levere fra seg som sitt første tall. - Nå er stream-filter klar til å produsere sitt andre tall, som det får ved først å avtvinge stream-enumerate dens neste tall, 12, som, vrakes, og deretter 13, som aksepteres, slik at stream-filter kan levere det fra seg som sitt andre tall. - Dermed mottar stream-cdr resten av strømmen fra og med andre primtall mellom 10 og 10000 fra stream-filter og levere det første av disse, 13, til stream-car. 498