0INF2810 — Funksjonell programmering — 1. forelesning. 18.01.2012 Først i denne forelesningen en presentasjon av funksjonell programmering, med en masse mer eller mindre forståelige ord. I løpet av noen uker vil dere ha forstått det meste og ved kursets slutt vil dere forstå alt (vi tar en sjekk ved repetisjonen). ——————————————————— Deretter en introduksjon til språket Scheme: grensesnitt—dvs. programmeringsomgivelser— grunnlegende begreper og kodeeksempler. 1 Begreper lambda calculus first class objekts currying closure lazy evaluation delayed evaluation streams continuations concurrency Konvensjon: A ==> B betyr A evaluerer til B (B er resultatet av evalueringen av A). 2 lambda calculus også skrevet calculus et -uttrykk evaluerer til en prosedyre x . x+x variabel la ==> en prosedyre som tar ett argument og legger dette til seg selv. kropp f = x. x + x. Da evaluerer f(3) til 6, men det gjør også ( x. x + x)(3) . Her har vi erstattet f med definisjonen av f og anvendt resultatet av evalueringen av definisjonen på argumentet 3. 3 first class objects, Med -kalkyle får vi prosedyrer som opptrer som egne objekter på linje med andre objekter som tall, strenger, lister, etc. Vi sier at prosedyrer er førsteklasses objekter Prosedyren p tar 3 argumenter hvorav det første er en prosedyre, og anvender prosedyren på de to etterfølgende argumentene p(+, 7, 4) ==> 11 p(–, 7, 4) ==> 3 p(*, 7, 4) ==> 28 p(( x y. x2 + y2), 7, 4) ==> 63 Dette er i praksis for enkelt til å være nyttig, men det illustrerer poenget. Etterhvert skal vi skrive prosedyrer der nytten av å kunne bruke prosedyreargumenter er helt åpenbar. 4 currying fra én flerargumentprosedyre til et nøste av énargumentsprosedyrer og vice versa. f(x, y) kan skrives om til f(x)(y) — der f(x) evaluerer til en prosedyre. I -notasjon definerer vi f slik: f = x. y. x + y. La g = f(2) da får vi g ==> y. 2 + y. og g(3) ==> 5 som er det samme som f(2)(3) ==> 5 5 en prosedyre som tar ett argument og legger 2 til dette closure — innpakking av én prosedyre i en annen. Forfatterne av SICP mener at denne bruken av begrepet closure er uheldig. Begrepet tilhører abstrakt algebra, der vi f.eks. sier at tallene er lukket under addisjon fordi summen av to tall selv er et tall (se fotnote SICP, s. 98). En closure er en førsteklasses prosedyre med frie variabler som er bundet i prosedyrens omgivelse. Et eksempel y. x + y, (1) Her er x fri og y bundet. f = x. y. x + y (2) Her er både x og y bundet g = f(2). (3) Prosedyreobjektet etter utførelsen av (3) er g et objekt bestående av en omgivelse Omgivelse x=2 der x er bundet til 2. og prosedyrekoden med variabel og kropp, ———————————————————————————————— Closure brukes bl.a. for å skjule tilstandsinformasjon i et funksjonelt program. Closure gir også en god semantikk for objektorientert programmering 6 variabel y. kropp x + y lazy evaluation beregninger utføres bare hvis de er nødvendige —hvis A, utfør B, og hvis ikke, utfør C. I de fleste språk evalueres valgsetninger som if A then B else C på denne måten. Dette gjelder også Scheme, der det ser slik ut (if A B C), men i Scheme kan vi i tillegg definere egne syntaktiske former som også evalueres på denne måten. delayed evaluation (også en form for lazy evaluation) beregninger utsettes til de, om noensinne, blir nødvendige, og dermed unngår vi unødvendige beregninger. 7 uendelig verdisekvenser også kalt strømmer definering av uendelige sekvenser og realisering de enkelte elementene i disse ved utsatt evaluering—etter behov. Dette får vi til vha. closure og delayed evaluation. Merk forskjellen mellom strømmer på den ene siden og arrays og lister på den andre. De siste vil alltid ha en endelig størrelse. En array er typisk statisk og deklareres med en fast størrelse. En liste er typisk dynamisk, og kan vokse og avta under programmets gang, men den vil alltid ha en ende som vi er nødt til å teste for når vi opererer på listen. En strøm er typisk uendelig og Det finnes også endelige strømmer, vi trenger aldri å teste for om strømmen er slutt. men det er en annen sak. 8 continuations (se vedlegg) concurrency To regneprosesser kan utføres samtidig uten på noen måte å affektere hverandre, men hvis to regneprosesser opererer på samme foranderlige objekt, kreves tilstandsprogrammering (se side 11). Læreboka har et avsnitt om dette, men det har skjedd en utvikling på denne fronten etter at dette ble skrevet, ikke minst ved utviklingen av nye funksjonelle språk som har mekanismer for å håndtere dette funksjonelt, —eller mer presist—innenfor det funksjonelle paradigmet. Dette tas ikke opp i INF2810. 9 Programmeringsparadigmer - imperativ / sekvensiell programmering med tilstandsendringer versus - funksjonell / eksplorativ programmering uten tilstandsendringer Imperativ programmering er karakterisert ved verditilordning, dvs. at programmets objekter — tall, strenger og sammensatte objekter — får sine verdier /attributter endret under programmets gang. Hver verditilordning innebærer en endring i programmets tilstand. Hver beregning er bestemt av forutgående tilstandendringer. Sekvensen i utførelsen av instruksjoner og operasjoner er av betydning. 10 I eksplorativ (undersøkende) programmereing definerer vi en statisk verden vha. predikater, funksjoner og regler, og bruker så disse til å undersøke verden uten å endrer verden våre undersøkelser har ingen bieffekter (side-effects) Kall på en gitt prosedyre gir alltid samme resultat med samme argumenter, uavhengig av når kallet utføres i forhold til andre prosedyrekall, dvs. Sekvensen i utførelsen av de ulike delene av en beregning er uten betydning. 11 Funksjoner og prosedyrer Vi sier tildels det samme om prosedyrer som om funksjoner: En prosedyre tar ett eller flere argumenter og gir en verdi. En prosedyre skal alltid gi samme resultat med samme argumenter. Men en prosedyre må også være effektiv, i den forstand at den utfører en beregning og returnerer et konkret resultat når den kalles. 12 Implementasjon Gitt en funksjon f(x) og en prosedyre p(x), slik at for alle x så er returverdien fra kallet p(x) = f(x). Da sier vi at p er en implementasjon av f. En implementasjon gjøres i henhold til en fremgangsmåte—en algoritme. Gitt en prosedyre q med en annen kropp en p, men slik at for alle x så er returverdien fra kallet q(x) = f(x). Da er p og q forskjellige prosedyrer, men begge er implementasjoner av f. 13 Noen høynivåspråk FORTRAN 1958 primitivt, ustrukturert og rendyrket imperativt LISP 1959 primært funksjonelt, men også med imperative mekanismer ALGOL 1960 strukturert overveiende imperativt SIMULA 1962 ALGOL-basert—alle objekorienterte språks mor (Dahl og Nygård) PASCAL 1970 enkelt, pedagogisk, rent, strengt, imperativt C 1972 funksjonelt / instruksjonelt — maskinnært og overbærende (tillater det meste) C++ 1986 SIMULA-inspirert videreutvikling av C, men langt strengere enn C. Java 1995 arkitekturuavhengig, biblioteksbasert, syntaktisk viderutviking av C++ HASKELL 1990 et rent funksjonelt språk. <legg til Scala> SCHEME er både - funksjonelt, - imperativt og - objektorientert. men ikke på samme måte som bl.a. JAVA, der objektorienteringen er integrert i språkets syntaks 14 Programutførelse — interpretering vs. kompilering + eksekvering Klassiske programmeringsomgivelse - kildekodeeditor - kompilator og eventuelt linker og bygger - operativsystemet. Kompilatoren - sjekker det skrevne programmets syntaks og indre konsistens, og når alt er ok, - oversetter kildekodeversjonen av programmet til en maskinkodeversjon. Linkeren - sjekker at alle variabler og kodesekvenser er på plass, f.eks. at en prosedyre som kalles både er deklarert og definert. Byggeren - bygger disse sammen til et helhetlig program. Operativsystemet - kjører programmet ved å overlate kontrollen av maskinen til programmets entry point —typisk første setning i hovedprogrammet (main), i programmer skrevet i imperative språk som Java og C. 15 Moderne programmeringsomgivelser relativt til perioden fra 1950-tallet til et stykke inn på 1980-tallet IDE — Integrated Development Envirnoment - integererer kompilering, lenking og bygging - formidle mellom operativsystemet og programmet ved å legge inn ad hoc entry-points for utførleser av deler av programmet - inspisere programmets tilstander på angitt break-points - mm 16 Interpretere - Leser og tolker kildekoden og utfører det intenderte programmet i én og samme sveip. - Er typisk ikke bundet til hovedprogrammets entry-point. I Scheme kan en hvilken som helst funksjon sendes direkte til interpreteren. - Eventuelle syntaksfeil og inkonsistenser rapporteres når de dukker opp (og fører da selsvagt til kjøreavbrudd). 17 Effektivitet Forhåndskompilering skal i prinsippet være mer effektivt enn interpretering, men med dagens prosessorhastigheter her dette mindre betydning, og med JIT-kompilering (se neste side) er forskjellen nærmest opphevet. Racket, som er den Scheme-implementasjonen som anbefales i INF2810, har en JIT-kompilator, og jeg tror de fleste vil oppleve denne som mer enn rask nok. I tillegg gir Racket mulighet for forhåndskompilering, fra kildekode til stand-alone applikasjoner. 18 Just In Time (fra http://en.wikipedia.org/wiki/JIT_compiler—noe bearbeidet) In computing, just-in-time compilation (JIT), also known as dynamic translation, is a method to improve the runtime performance of computer programs. Traditionally, computer programs had two modes of runtime operation, - interpreted, or - static (ahead-of-time) compilation. ... JIT compilers represent a hybrid approach, - with translation occurring continuously, as with interpreters, but - with caching of translated code to minimize performance degradation. ... Several modern runtime environments, such as Microsoft's .NET Framework and most implementations of Java, rely on JIT compilation for high-speed code execution. 19 Scheme er i utgangspunket et svært enkelt språk, med syntaks for bindinger (av variabler til verdier), bl.a. vha. definisjoner, spesialformer for bl.a. tester og valg: if, and, or, etc, og prosedyreobjekter: lambda prosedyrekall (p a1 ...) der p er en prosedyre og a1 ... er argumentene til p. primitiver, dvs. forhåndsdefinerte prosedyrer som +, /,=, max, abs, etc. Det er mere ved Scheme enn dette, men dette er essensen. Alle uttrykk i Scheme, bortsett fra slikt som tall og tegn, er parenteser. Kodingen dreier seg dermed mye om å matche venstre og høyreparenteser, men her får man god hjelp av editoren—i alle fall hvis man bruker DrRacket. 20 Scheme har vært revidert flere ganger siden den første standarden ble definert i 1973. Revisjonene opp til og med R5RS Revised5 Report on the Algorithmic Language Scheme svarer i det store og hele til det vi kan kalle SICP-Scheme. I PLT-Scheme (nå omdøpt til Racket), fra og med versjon 4, er det gjort en endring som gjør det umulig å endre strukturer som er bygget opp av lister. Her skiller PLT-Scheme seg fra SICP-Scheme. Mer om det i siste halvdel av kurset. R6RS, som ble vedtatt i 2007, har med noen flere standard mekanismer, noen semantiske endringer og et større prosedyrebibliotek 21 Grensesnitt / Programmeringsomgivelser (i DrRacket) REPL—the Read-Eval-Print-Loop (les input fra bruker, evaluer input, skriv resultatet til skjermen) I REPL skrives input og output på separate linjer Her vises de på samme linje av plasshensyn > 123 123 > (+ 2 2) 4 > (2 + 2) procedure application: expected procedure, given: 2; arguments were: #<primitive:+> 2 > (substring "hallo" 1 3) "al" > (/ (string->number "56") 3) 18 2/3 > (exact->inexact (/ 56 3)) 18.666666666666668 22 > (list 1 2 3) (1 2 3) > (car (list 1 2 3)) 1 > (cdr (list 1 2 3)) (2 3) > (= (+ 2 2) 4) #t > (= (+ 2 2) 5) #f > (= 1 "yo") =: expects type <number> as 2nd argument, given: yo; other arguments were: 1 > (equal? 1 "yo") #f > (equal? 'yo (car '(yo doh doodle))) #t > (define en-to-tre (list 1 2 3)) > en-to-tre (1 2 3) > + #<procedure:+> > (apply + en-to-tre) 6 23 Uttrykk (Expressions) Selvevaluerende atomære uttrykk > 324 ; number — nærmere bestemt integer 324 > 3.24 ; number — nærmere bestemt real 3.24 > 3/24 ; number — nærmere bestemt rational 1/8 > #\A ; char(acter) #A > #t ; boolean #t > "hallo" ; string "hallo" —med flere 24 Literale (bokstavelige) uttrykk (latin: litera, engelsk letter, norsk: bokstav) Alle uttrykkene over er literale, Mer om quotations siden. Her nøyer vi oss med å konstatere at vi ved hjelp av en innledende enkel apostrof kan innføre bokstavelige uttrykk ustraffet men det er også følgende > 'hei ; quoted symbol hei —i motsetning til dette: > hei reference to undefined identifier: hei Sammensatte uttrykk (Compositions) — operasjoner > (+ 2 3) 5 ; bruk av prosedyren + > (- 2 3) -1 ; bruk av prosedyren - > (* 2 3 4) 24 ; bruk av prosedyren * > (/ 5 2) ; bruk av prosedyren / 25 Syntaks Prosedyrekall generelt: (<prosedyre> <ingen, én eller flere argumenter>) Prefix Alt er prefikset—også bruk av operatorer (regneuttrykk) Og strengt tatt er det ikke noe skille mellom operatorer og prosedyrer. (abs (- 5 8)) 3 Infix Pascal, C, C++, Java, m.m. Mest vanlig for aritmetiske og logiske operatorer. kombinert med prefixede funksjonskall (prosedyrekall). abs(5 - 8) 3 26 For de som måtte lure på om følgende var mulig: Bl.a. i FORTH — et stakk-orientert programmeringsspråk Postix 8 5 - abs 3 Her trenger vi ikke parenteser, gitt at operatorenes ariteteter (ant. operander) er fixert. 2 3 4 5 + / * 6 virker bare hvis alle tre operatorer er strengt binære. Vi tenker oss operandene på en stakk med siste operand øverst. 5 4 3 2 + opererer på de to øverste, 5 og 4, og legger resultatet 9 tilbake på stakken. 9 3 2 / opererer på de to som nå er øverst, 9 og 3, og legger resultatet 3 tilbake på stakken. 3 2 * opererer på de to som nå er igjen, 3 og 2, og legger resultatet 6 tilbake på stakken. 6 Tilsvarende i prefix: (* (/ (+ 5 4) 3) 2) 6 Men her kunne vi hatt: (* (/ (+ 4 3 2) 3) 2) 6 Som i postfix tilsvares av: 2 3 2 3 4 + + / * 6 ——————————————————————————————————— 27 Prefix muliggjør vilkårlig mange argumenter (+ 1 2 3) 10 (- 1 2 3) -4 ; NB! (- (- 1 2) 3) = (- -1 3) = -4 ; ikke (- 1 (- 2 3)) = (- 1 (- 1)) = 0 (* 2 3 5) (/ 84 2 7) = (- 1 (+ 2 3)) 30 6 ; NB! = (/ (/ 84 2) 7) = (/ 42 7) ; ikke = (/ 84 (/ 2 7)) = (/ 84 2/7)) = 294 Prefix muliggjør nøstede kombinasjoner (med det gjør selvsagt også infix) (+ (* 3 5) (- 10 6)) 6 19 (+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6)) 57 De fleste Scheme-editorer har innrykksmekanismer for bedre leselighet (+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6)) Innrykkene fremkommer automatisk ved trykk på Enter (også vha TAB). 28 = (/ 84 (* 2 7)) Lisps parentesbaserte prefixnotasjon overflødiggjør regler for operatorpresedens Ser vi bort fra slike regler, gir uttrykket 1 + 2 / 3 * 4 - 5 følgende 14 tolkningsmuligheter: Infix Prefix (3 + (8 / (2 * (3 – 2)))) (+ 3 (/ 8 (* 2 (- 3 2)))) 8 (3 + (8 / ((2 * 3) – 2))) (+ 3 (/ 8 (- (* 2 3) 2))) 6 (3 + ((8 / 2) * (3 – 2))) (+ 3 (* (/ 8 2) (- 3 2))) 8 (3 + ((8 / (2 * 3)) – 2)) (+ 3 (- (/ 8 (* 2 3)) 2)) 10/3 (3 + (((8 / 2) * 3) – 2)) (+ 3 (- (* (/ 8 2) 3) 2)) 14 ((3 + 8) / (2 * (3 – 2))) (/ (+ 3 8) (* 2 (- 3 2))) 6 ((3 + 8) / ((2 * 3) – 2)) (/ (+ 3 8) (- (* 2 3) 2)) 3 ((3 + (8 / 2)) * (3 – 2)) (* (+ 3 (/ 8 2)) (- 3 2)) 8 (((3 + 8) / 2) * (3 – 2)) (* (/ (+ 3 8) 2) (- 3 2)) 6 ((3 + (8 / (2 * 3))) – 2) (- (+ 3 (/ 8 (* 2 3))) 2) 10/3 ((3 + ((8 / 2) * 3)) – 2) (- (+ 3 (* (/ 8 2) 3)) 2) 14 (((3 + 8) / (2 * 3)) – 2) (- (/ (+ 3 8) (* 2 3)) 2) 0 (((3 + (8 / 2)) * 3) – 2) (- (* (+ 3 (/ 8 2)) 3) 2) 22 ((((3 + 8) / 2) * 3) – 2) (- (* (/ (+ 3 8) 2) 3) 2) 16 29 Utfordrende Øvelse (Ukeoppgave senere i kurset) Skriv et Scheme-program for beregning av antall mulige parenteskombinasjoner generelt for et gitt antall operander, når vi ser bort fra operatorpresedenser. (Merk at den siste betingelsen innebærer at alle operasjoner må være binære.) operander kombinasjoner 1 1: (o1) 2 1: (o1 o2) 3 2: (o1 (o2 o3)), ((o1 o2) o3) 4 5: (o1 (o2 (o3 o4)), (o1 ((o2 o3) o4)), ((o1 o2) (o3 o4)), ((o1 (o2 o3)) o4), (((o1 o2) o3) o4) 5 14: (o1 (o2 (o3 (o4 o5)))) … ((((o1 o2) o3) o4) o5) 30 Navn (identifiers) Variabler > (define x 5) > x 5 Variabeldefinering generelt: (define <variabel> <verdi>) > (define pi 3.14159) > (define radius 10) > (* pi radius) 31.4159 > (define gyldne-snitt (/ (+ 1 (sqrt 5)) 2)) > gyldne-snitt 1.618033988749895 31 ; se SICP s.38 Prosedyrer > (define (navle-høyde høyde) (/ høyde gyldne-snitt)) > navle-høyde #<procedure:navle-høyde> > (navle-høyde 185) 114.33628791873055 Prosedyredefinering generelt: (define (<prosedyrenavn> <formelle parametre>) <prosedyrekopp>) > (define (gjennomsnitt x y ) (/ (+ x y) 2)) Prosedyrekall / -bruk / -anvendelse (application) generelt: (<prosedyrenavn> <aktuelle parametre>) > (gjennomsnitt 2 3 ) > (gjennomsnitt 2 3.0 ) 2.5 I tråd med SICP vil vi heretter bruke termen argument i stedet for actual parameter. 32 Omgivelse (Environment) Tabell(er) med kobling mellom navn og verdier (mer om dette siden) NB! dette dreier seg om - omgivelsen til en prosedyre under programutførelsen som er noe helt annet enn - en programmeringsomgivelse. I vanlig norsk snakker vi stort sett om omgivelser i flertall, mens man i engelsk snakker om environment i entall Eks: nye omgivelser a new environment Nå vi snakker om programmering velger vi imidlertid den engelske formen, for tydelighets skyld, f.eks. når vi snakker om den innerste omgivelsen i et nøste eller hierarki av omgivelser. ——————————————————————————————— Kommentarer legges sist på linjen eller på en linje for seg selv og skilles fra koden med et semikolon. 33 Definisjoner, binding og leksikalsk — statisk — skop > (define (f x y) (* (/ x y) 2)) ; f defineres globalt, mens x og y ; bare defineres lokalt i kroppen til f Vi sier at f har globalt skop (gresk skopos: mål (det vi sikter eller ser mot, fra skopein: se)), mens x og y har lokalt skop. Dette kalles leksikalsk skop fordi programmets leksikalske elementer (identifikatorene) er synlige innenfor de områder der de er definert. Det kalles statisk skop fordi et gitt navn i en gitt omgivelse alltid referer til en og samme gitte variabel. I dynamisk skop har hver variabel en stack av bindinger knyttet til lokale run-time lokasjoner. Dette har noen fortrinn og et hav av plagsomme ulemper. 34 > (define (f x y) (* (/ x y) 2)) > (f 9 3) ; f defineres globalt, mens x og y ; bare defineres lokalt i kroppen til f ; x og y bindes hhv. til verdiene 9 og 3 ; i kroppen til f under dennes utførelse 6 > (+ x 1) reference to undefined identifier: x > (define x 6) ; x defineres globalt og bindes til verdien 6 > x ; og kan nå også brukes globalt 6 > (f x x) ; x og y if f bindes hver for seg til den verdien ; den globalt definerte x har, dvs. 6 2 > (define y 3) ; y defineres globalt og bindes til verdien 3 > (define (g y) ; g defineres globalt og y defineres lokalt. (* (/ x y) 2)) > (g 3) ; x brukes i henhold til sin globale definisjon, men ; den lokale y stenger for utsynet til den globale y ; y bindes til verdien 3 i f under dennes utførelse ; mens x stadig har verdien 6. 4 35 Primitiver og Biblioteksprosedyrer En innebygget prosedyre er en prosedyre som er definert i språket Scheme, og som skal være med i alle implementasjoner av Scheme. Disse omfatter primitiver og biblioteksprosedyrer, som er definert vha. primitiver. Fra R5RS (Revised5 Report on the Algorithmic Language Scheme): […] Scheme's built-in procedures. The initial (or ``top level'') Scheme environment starts out with a number of variables bound to locations containing useful values, most of which are primitive procedures that manipulate data. For example, the variable abs is bound to (a location initially containing) a procedure of one argument that computes the absolute value of a number, and the variable + is bound to a procedure that computes sums. Built-in procedures that can easily be written in terms of other built-in procedures are identified as ``library procedures''. I noen versjoner av Scheme gjengir REPL verdien til en primitiv, f.eks. +, slik: #<primitive:+> I Racket gjengir REPL verdien til enhver prosedyre p slik: #<procedure:p> 36 Evaluering av et sammensatt uttrykk Evalueringsregel for sammensatte uttrykk 1. Evaluer utrrykkets deluttrykk! 2. Bruk den prosedyren som er verdien til første ("venstreste") deluttrykk på de argumentene som fremkommer ved evalueringen av de øvrige deluttrykkene! Uttrykket "er verdien til" har en spesiell signifikans i forhold til punkt 1. Poenget er at alt evalueres, etter bestemte regler, til en eller annen verdi. Eks: (+ 2 3) 1. Her er 2 og 3 selvevaluerende, dvs. de er sine egne verdier, mens + er navnet på en prosedyre, og dermed gir evalueringen av + denne prosedyren som første verdi i listen. 2. Anvend denne prosedyren på argumentene 2 og 3 —hvilket i dette tilfellet vil si: legg sammen 2 og 3. 37 Evalueringsregelen er rekursiv. Ved evalueringen av det sammensatte utrrykket (+ (* 2 3) 4) kommer evalueringsregelen til anvendelse på seg selv i forhold til deluttrykket (* 2 3) Evaluer deluttrykkene Evaluer deluttrykkene i deluttrykkene … Evaluer de atomære deluttrykkene Her er ett av eksemplene over (+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6)) Her med linjeskift og innrykk (+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6)) 38 Vi roterer 90, speilvender, fjerner parentesene, og følger evalueringen på de ulike nivåene. + * + 3 + - * 2 10 + 4 3 6 7 5 + * + 3 + 8 6 8 10 7 + * 3 + 16 3 6 + 48 9 57 NB! Dette viser ikke rekkefølgen i evalueringen. Den er slik: (a) + ==> #<procedure:+>, (b) * ==> #<procedure:*>, (c) 3 ==> 3, (d) + ==> #<procedure:+>, (e) * ==> #<procedure:*>, (f) 2 ==> 2, (g) 4 ==> 4, (h) (* 2 4) ==> 8, (i) + ==> #<procedure:+>, (j) 3 ==> 3, (k) 5 ==> 5, (l) (+ 3 5) ==> 8, (m) (+ 8 8) ==> 16, (n) (* 3 16) ==> 48, (o) + ==> #<procedure:+>, (p) – ==> #<procedure:–>, (q) 10 ==> 10, (r) 7==> 7, (s) (- 10 7) ==> 3, (t) (+ 3 6) ==> 9, (u) (+ 48 9) ==> 57 39 Her er en annen tre-representasjon av evalueringen der hvert subtre tilsvarer en parentes, og der verdiene på rotnodene angir de evaluerte verdiene. Evalueringen skjer fra roten og nedover og innsetting av verdiene fra bladene og oppover 40 Definisjoner, kall og utførelse (define (<prosedyrenavn> <formelle parametre>) <prosedyrekopp>) Prosedyrenavnet med kobling til parameterne og prosedyrekroppen legges i den globale omgivelsen eller omgivelsene bestående av den blokken der prosedyredefinisjonen ligger. Ved et kall på prosedyren blir formelle paramterene navn bundet til de aktuelle argumentene i de lokale omgivelsene som opprettes for utførelsen av kallet. Kroppen er et uttrykk som evaluere til det resultatet prosedyren skal returnere, når argumentene settes inn for de formelle parametrene, el. m.a.o. når variablene i prosedyrekroppen bindes til argumentene. 41 Substitusjonsmodellen (define (plus-en n) (+ n 1)) (define (dobbel n) (* n 2)) (define (kvadrat x) (* x x) (define (kvadratsum x y) (+ (kvadrat x) (kvadrat y))) (define (f a) (kvadratsum (plus-en a) (dobbel a))) Vi følger evalueringen av kallet (f 5) Kroppen til f er et sammensatt uttrykk med tre deler, Delene evalueres én for én Vi går her ut fra at de evalueres fra venstre, men dette er ikke et krav i R5RS, og rekkefølgen i evalueringen kunne like gjerne ha vært den motsatte. Slik er det imidlertid ikke i tilstandsbasert programmering. og resultatene settes inn i uttrykket, dvs. De opprinnelige delene substitures med resultatene av deres evalueringer. 42 kvadratsum evalueres umiddelbart til #<procedure:kvadratsum> , dvs. prosedyren kvadratsum. Det sammensatte uttrykkket (plus-en 5) evalueres på samme måte: plus-en evalueres umiddelbart til #<procedure:plus-en>, 5 evalueres umiddelbart til seg selv, og vi går inn i kroppen til plus-en: (+ 5 1) Her evalueres alle leddene umiddelbart, til henholdsvis #<primitive:+>, 5 og 1. og kallet på + utføres med 5 og 1 som argumenter og 6 som resultat. Nå kan uttrykket (plus-en 5) substitueres med 6 i kallet på kvadratsum. (uttrykket dobbel a) evalueres på samme måte, med 10 som resultat. 43 Dermed er evalueringen av alle leddene i kroppen til f utført, med uttrykket (#<procedure:kvadratsum> 6 10) som resultat, og vi går inn i kroppen til kvadratsum der x bindes til 6 og y bindes til 10. Dette gir det sammensatte uttrykket (+ (kvadrat 6) (kvadrat 10)) De tre leddene evalueres på tilsvarende måte som over, og vi får uttrykket (#<primitive:+> 36 100) som evalueres med 136 som resultat. 136 settes inn i kroppen til f, og f er dermed ferdig evaluert og returner 136. 44 Bruk f (f 5) Substituer prosedyrenavn f med kropp: (kvadratsum (plus-en a) (dobbel a)) Substituer parameter a med argument 5: (kvadratsum (plus-en 5) (dobbel 5)) Substituer prosedyrenavn plus-en med kropp: (kvadratsum (+ n 1) (dobbel 5)) Substituer parameter n med argument 5: (kvadratsum (+ 5 1) (dobbel 5)) Substituerprosedyrenavn + primitiv prosedyre: (#<primitive:+> 5 1) Substituer bruk av + med resultatsum: (kvadratsum 6 (dobbel 5)) Substituer pros-navn dobbel med kropp: (kvadratsum 6 (* n 2)) Substituer parameter n med argument 5: (kvadratsum 6 (* 5 2)) Substituer bruk av primitiv * m. result.produkt: (kvadratsum 6 10) Substituer pros-navn kvadratsum m. kropp: (+ (kvadrat x) (kvadrat y)) Substituer parameter x med argument 6: (+ (kvadrat 6) (kvadrat y)) Substituer pros-navn kvadrat med kropp: (+ (* x x) (kvadrat y)) Substituer parameter x med argument 6: (+ (* 6 6) (kvadrat y)) Substituer bruk av primitiv * m. result.produkt: (+ 36 (kvadrat y)) Substituer parameter y med argument 10: (+ 36 (kvadrat 10)) Substituer pros-navn kvadrat med kropp: (+ 36 (* x x)) Substituer parameter x med argument 10: (+ 36 (* 10 10)) Substituer bruk av primitiv * m. result.produkt: (+ 36 100) Substituer bruk av primitiv + med result.sum: 136 45 Evalueringsorden (define (kvadrat n) (* n n)) (define (double n) (+ n n)) Appliaktiv orden (som er det vi har sett så langt) Sammensatte uttrykk evalueres innefra og utover slik at resultatene av evalueringene av de innerste uttrykkene settes inn i de omkringliggende uttrykkene slik at disse kan evalueres osv. Til slutt vil alle deluttrykk i det ytterste uttrykket være regnet ut slik at dette evalueres. (double (kvadrat (double 2))) (double (kvadrat 4)) (double 16) 32 46 Normal orden Sammensatte uttrykk evalueres ved at formen til eventuelle ikke-primitive deluttrykk reduseres til primitive former, før den endelige evalueringen utføres. (double (kavdrat (double 2))) (double (kvadrat (+ 2 2))) (double (* (+ 2 2) (+ 2 2))) (+ (* (+ 2 2) (+ 2 2)) (* (+ 2 2) (+ 2 2))) (+ (* 4 4) (* 4 4)) (+ 16 16) 32 Det kan se litt klønete ut her, fordi vi ender med å utføre de samme regnestykene flere ganger, men, som vi skal se senere i kurset, er normalordensevaluering et essentiell element i utsatt evaluering og dermed i strømmer—som bl.a. gjør det mulig å behandle ”uendelig” sekvenser. 47 Følgende gir feilmelding ved applikativ, men ikke ved normal evalueringsorden > (define (f a) "hallo") > (f (* 2 "hei")) Problemet er det umulige produktet av et tall og en streng som utgjør argumentet i kallet på f. Siden parameteren a ikke brukes i kroppen til f, vil det strengt tatt ikke være nødvendig å evaluer produktet, men dette får vi altså bare glede av ved normal evalueringsorden. I Haskell, som er rent funksjonelt språk med utelukkende normalordens evaluering, gjøres noe lurt for å unngå dupliserte utregninger slik at språket blir brukelig i praksis, idet en regnestykke i en evaluering gjøres til et objekt som bare utføres én gang. 48 Forelesning 2 Kondisjonaler og predikater / tester / booleske uttrykk Betingelsesuttrykket cond Absoluttverdien til et tall x ┌ │ x, hvis x > 0 |x|= ┤ 0, hvis x = 0 │ –x, hvis x < 0 └ (define (abs x) (cond ((> x 0) x) ((= x 0) 0) ((< x 0) (- x)))) Primitiven – (minus) med kun ett argument har tilsynelatende en annen semantikk enn den samme primitiven med flere argumenter, idet den første returnerer negasjonen av sitt argument. Men forskjellen forsvinner når vi setter inn identitetselementet som første argument. -1 (- 1) (- 0 1) Merk forskjellen mellom dette og (- 1 0), som evaluerer til 1. 49 Generelt (cond (<p1> <e1>) (<p2> <e2>) . . . (<pn> <en>)) Det engelske ordet for parentesene etter cond er clauses. Vi kunne for eksempel kalle dem cond-ledd. Disse angir de enkeltvise betingelser og konsekventer, som til sammen angir hele cond-setningen. p står for predikat, mens e står for expression (uttrykk) Et predikat evaluerer alltid til én av de to booleske verdiene #t og #f. 50 Vi bruker termen predikat først og fremst om - funksjoner som tar ett argument og returnerer #t eller #f avhengig av om argumentet har en bestemt egenskap, og - funksjoner som tar to argumenter og returnerer #t eller #f avhengig av om argumentene står i en bestemt relasjon til hverandre. (number? 25) #t (number? "hei") #f (string=? "hei" "hallo") #f (> 5 2) #t Også booleske variabler kan sies å være predikater (argumentløse predikater). (define sant #t) Siden sant ikke tar argumenter, returnerer den alltid samme verdi, når definisjonssetningen først er utført. 51 Ellers snakker vi like gjerne om tester — og vi sier da at - en cond-clause består av en test fulgt av et annet uttrykk Evalueringen av et cond-uttrykk utføres slik at - hver enkelt test evalueres i tur og orden - inntil løpende test eventuelt evalueres til #t, og - i så fall evalueres det etterfølgende uttrykket i den aktuelle clause og resultatet av dette blir også resulatet av hele cond-uttrykket. Hvis ingen av testene evaluerer til #t, blir verdien til cond-uttrykket ubestemt. I Scheme returneres i slike tilfeller verdien #<void>. En typisk cond clause innholder en test og et etterfølgende uttrykk, men strengt tatt kan den inneholde testen alene, eller ett eller flere etterfølgende uttrykk. Uansett vil alle uttrykken i claus'en evalueres, og så vil resultatet av evalueringen av det siste uttrykket, som kan være testen, returneres. Dette kommer vi nærmere tilbake til. 52 Retur av en ubestemt verdi fra et cond-uttrykk gir ikke noe umiddelbart kjøreavbrudd, men en cond-setning som ikke er garantert å evaluere til en bestemt verdi, vil normalt representere en logisk brist i et program, som kan gi gale resultater eller kjøreavbrudd på et senere punkt. (define (dagnavn->dagnum dagnavn) (cond ((eq? dagnavn 'mandag) 1) ((eq? dagnavn 'tirsdag) 2) ((eq? dagnavn 'onsdag) 3) ((eq? dagnavn 'torsdag) 4) ((eq? dagnavn 'fredag) 5))) (define (hverdag? dagnavn) (< (dagnavn->dagnum dagnavn) 6)) > (hverdag? 'lørdag) <: expected argument of type <real number>; given #<void> Prosedyren hverdag? bruker prosedyren < for å sammenligne to tall. Siden ingen av cond-claus'ene i dagnavn->dagnum sjekker for 'lørdag, er returverdien derfra ubestemt, mens < krever to tallargumenter. 53 I alle andre tilfeller I cond-setningen over for beregning av absoluttverdien til x angis alle muligheter eksplisitt, men dermed er den siste testen overflødig i den forstand at dersom x hverken er større enn eller lik 0, så må x være mindre enn 0. For slike formål har vi nøkkelordet else (som kan oppfattes som et synonym for #t). (define (abs x) (cond ((> x 0) x) ((= x 0) 0) (else (- x)))) 54 Ett av to Nå er det heller ingen grunn til å skille mellom de tilfellene at x er større en 0 og at x er lik 0, siden det er verdien til x som skal returneres uansett. Vi kan uttrykke dette slik: (define (abs x) (cond ((>= x 0) x) (else (- x)))) Dette er en binær test idet den skiller mellom to tilfeller som til sammen dekker alle mulige. Valguttrykket if Siden binære tester er svært vanlige, har vi en egen konstruksjon for disse. (define (abs x) (if (>= x 0) x (- x))) Generelt: (if <test> <konsekvent> <alternativ>) Egentlig er det if som er den tilgrunnliggende formen, og cond, så vel som and og or (se under), er avledninger av denne 55 Spesialformene and og or og prosedyren not Det regner og det er onsdag (and det-regner det-er-onsdag) Det regner eller det er onsdag (or Det regner, men det er ikke onsdag (and det-regner (not det-er-onsdag)) Det hverken regner eller er onsdag (and (not det-regner) (not det-er-onsdag))) det-regner det-er-onsdag) (not (or det-regner det-er-onsdag)) Enten regner det eller så er det onsdag (and (or det-regner det-er-onsdag) (not (and det-regner det-er-onsdag))) 56 x er et skuddår hvis x er delelig med 4 og x er ikke delelig med 100 eller x er delelig med 400. x er delelig med y hvis x / y er et heltall, hvilket vil si det samme som at x / y ikke gir noen rest. For rest-beregningen bruker vi Scheme-primitiven (remainder x y) ==> resten etter heltallsdelingen av x på y. Vi definerer delelighetspredikatet divisible? vha. remainder. (define (divisible? x y) (= 0 (remainder x y))) og vi kan så definerere skuddårspredikatet vha. divisible?. (define (skuddår? x) (and (divisible? x 4) (or (not (divisible? x 100)) (divisible? x 400)))) Alternativt: (define (skuddår? x) (or (divisible? x 400) (and (divisible? x 4) (not (divisible? x 100))))) 57 Vi kan uttrykke det samme vha. betingelsesformen cond. (define (skuddår? x) ; hvis testen slår til, returners #t implisitt (cond ((divisible? x 400)) ((divisible? x 100) #f) ; her må vi returnere #f for å komme oss ut. ; hvis testen slår til, returners #t implisitt ((divisible? x 4)) (else #f))) Merk testrekkefølgen. Bytter vi om f.eks først og andre clause, får vi ikke fanget opp delelighet med 400. NB! Dette er et helt annet poeng enn det at et imperativt program er sekvensielt, dvs. at rekkefølgen i utførelsen av programmets prosedyrer kan ha betydning for sluttresultatet. Her er programutførelsen en sekvens av tilstander, gitt ved de foranderlige verdiene til programmets variabler, f.eks. slik at hvis prosedyren p endrer verdient til v og prosedyren q bruker v, så er resultatet av kallet på q bestemt av om p kalles før eller etter q. 58 Siden både cond, and og or er avledninger av if, må det være mulig å uttrykke ovenstående vha. if alene (define (skuddår? x) (or (divisible? x 400) (and (divisible? x 4) (not (divisible? x 100))))) (define (skuddår? x) (cond ((divisible? x 400)) ; hvis testen slår til, returners #t implisitt ((divisible? x 100) #f) ; her må vi returnere #f for å komme oss ut. ((divisible? x 4)) ; hvis testen slår til, returners #t implisitt (else #f))) (define (skuddår? år) (if (divisible? år 4) (if (divisible? år 100) (if (divisible? år 400) #t) #f)) #t ; delelig både på 4, 100 og 400 #f) ; delelig på 4 og 100, men ikke på 400 ; delelig på 4, men ikke på 100 ; ikke delelig på 4 59 Månedslengder Vi kan bruke predikatet skuddår? til å beregne lengden til en gitt måned i et gitt år. (define (månedslengde måned år) (cond ((= måned 1) 31) ((= måned 2) (if (skuddår år) 29 28)) ((= måned 3) 31) ((= måned 4) 30) ((= måned 5) 31) ((= måned 6) 30) ((= måned 7) 31) ((= måned 8) 31) ((= måned 9) 30) ((= måned 10) 31) ((= måned 11) 30) ((= måned 12) 31) (else (error "Ulovlig måned: " måned)))) 60 Er vi sikre på at 1 måned 12, kan vi bruke Vha. or kan vi omformulere denne slik 30 eller 31 som default verdi (det som gjelder i alle de tilfeller vi ikke har sjekket). (define (månedslengde måned år) (define (månedslengde måned år) (cond ((or (= måned 1) (cond ((or (= måned 4) (= måned 3) (= måned 6) (= måned 5) (= måned 9) (= måned 7) (= måned 11) (= måned 8) 30) (= måned 10) ((= måned 2) (= måned 12)) (if (skuddår år) 29 28)) 31) (else 31))) ((= måned 2) (if (skuddår år) 29 28)) ((or (= måned 4) (= måned 6) (= måned 9) (= måned 11)) 30) (else (error "Ulovlig måned: " måned)))) 61 For de som måtte være interessert På semestersiden for INF2810 http://www.uio.no/studier/emner/matnat/ifi/INF2810/v12/ slå opp R5RS under Ressurser og case i indeksen til R5RS. ;; Trygg måned (define (månedslengde m y) (case m ;; Utrygg måned (define (månedslengde m y) (case m ((1 3 5 7 8 10 12) 31) ((1 3 5 7 8 10 12) 31) ((4 6 9 11) 30) ((4 6 9 11) 30) (else (if (skuddår? y) 29 28)))) ((2) (if (skuddår? y) 29 28)) (else (error "Ulovlig måned: " måned)))) Case forutsetter at alle de verdiene det testes for er distinkte. 62 Evaluering av spesialformer I likhet med if og cond, er and og or spesialformer (mens not er en vanlig funksjon). Spesialformer skiller seg fra funksjoner bl.a. mht. evalueringen av argumenter. Alle argumenter til en prosedyre evalueres, fra venstre mot høyre, eller omvendt—før kallet utføres. Argumentene til en spesialform evalueres etter tur, i den rekkeølgen de står, men hvert enkelt argument evalueres bare når det eventuelt blir nødvendig. Vi skiller mellom Dette tilsvarer skillet mellom ivrig (eager) og applikativ og lat (lazy) evaluering. normal evalueringsorden . 63 Den tilgrunnleggende kondisjonale spesialformen er if, og cond, and og or er avledninger av denne. Semantikken til and og or er utsagnslogisk, slik at - et and-uttrykk evaluerer til #t hvis og bare hvis alle dets operandene evaluerer til #t, og - et or-uttrykk evaluerer til #t hvis og bare hvis minst én av dets operander evaluerer til #t. —————————————————————————— Semantikke for booleske verdier er i alle språk binær, men hva som brukes for å representere dette, varierer. - I bl.a. Pascal, C++ og Java brukes false og true, - mens verdiene i C er 0 for usant og 1 for sant, - og som vi har sett, er verdiene i Scheme #f og #t. 64 I C er det i tillegg slik at når vi betrakter et tall som en boolesk verdi, så er alle andre tall enn 0 ensbetydende med 1. Gitt constanten TRUE = 1 og en heltallsvariabel b 0: if (TRUE) …; if (b) …; if (b != 0) …; Scheme går enda lenger, ettersom alle andre verdier enn #f er enbsbetydende med #t. Gitt variabelen TRUE = #t og en variabel b #f: if TRUE …; if b …; NB dette er ikke Scheme-uttrykk (if (not (equal? b #f) …))); C Scheme (1 == 2) 0 (= 1 2) #f (2 == 2) 1 (= 2 2) #t if (x == 1); if (x) ; (if (= x #t)) (if x ) 3 ? 4 : 5; 4 (if 3 4 5) 4 65 I Scheme returneres alltid verdien til det uttrykket som evalueres sist. (or 1 2 3) 1 ; det er nok at ett sant argument til or evalueres. (if 1 2 3) 2 ; testen gir #t og konsekventene er det siste som evalueres (and 1 2 3) 3 ; alle sanne argumenter til and må evalueres. ; når testen i en cond-clause slår til, evalueres alle etterfølgene uttrykk i denne clause (se neste side) (cond (1 2 3) (4 5) (6 7)) 3 (cond (1) (2) (3)) 1 (cond ((string=? "ja" "nei")) ("vet ikke")) "vet ikke" De to siste cond-setningene viser at hvis siste test i et cond-uttrykk aldri kan evaluere til #f, så trenger vi ikke cond. 66 Sekvenser av uttrykk Vise typer uttrykk kan inneholde sekvenser. Dette gjelder prosedyrekropper og cond-clauses, og vi kan forme konsekventen og/eller alternativet i en if-setning til en sekvens vha. den syntaktiske formen begin (se eksemplet under). Vi har en melding m med adressat a og kanskje en c som skal ha en kopi, og hvis det er en c, må vi sende ett eksemplar av m til hver av a og c sammen med en beskjed om kopieringen (if (har-cc? m) (begin (send-til-adressat-med-beskjed-om-cc m a c) (send-til-cc-med-beskjed-om-adressat m a c)) (send-til adressat m a)) 67 Poenget med en sekvens er å få til en effekt om ikke nødvendigvis blinkende lys og ringende bjeller før vi returnerer den aktuelle verdien, eller å få til en sekvens av to eller flere effekter, som i eksemplet over. Dette bruker vi bl.a. til å skrive Scheme-programmer der innlesing og utskrift av data er essensielle formål. Vi kan også bruke det i debuggingsøyemed, når vi skriver ut en eller flere variabelerdier til skjermen samtidig som vi lar de funksjonelle beregningen gå sin gang, En effekt forårsaket av et funksjonelt program kalles gjerne en side-effect—for å poengtere at det essensielle, i et funksjonelt program er den returnerte funksjonsverdien. 68 Statisk (manifest) versus dynamisk (latent) typing Scheme har latente, i motsetning til manifeste, typer. Vi sier også at Scheme er svakt eller dynamiske typet Typer er knyttet til verdier—objekter—snarer enn til variabler. Bl.a.C, C++ og Java har manifest, sterk, statisk typing C/C++/Java: Scheme: String s = "hei"; forhåndsangivelse av type int i = s * 5; compile-time-feil (define s "hei") ingen typeangivelse (* s 5) run-time-feil 69 I prinsipper er det altså slik at gitt f.eks. (define v 5), så er verdien til v et heltall, mens v i og for seg ikke har noen type. I praksis tillater vi oss allikevel av og til å si "v er en heltallsvariabel" i stedet for "v har en heltallsverdi". Det dynamiske aspektet ved typingen ligger i at typen til en variabel bestemmes av den verdien variabelen bindes til. Men merk at siden Scheme har destruktive mekanismer vil typen til verdien til en variabel kunne endres ved verditilordning, f.eks. slik (set! v "hallo"), og det er kanskje ikke fullt så uproblematisk å si at v nå er en streng og ikke lenger et heltall. 70 Et regneeksempel: Å finne en kvadratrot Rent matematisk er kvadratroten av x, = √x = den y som er slik at y 0 og y2 = x, men for konkret å finne roten av x må vi ha en anvisning for hvordan vi kan regne oss frem til svaret—en algoritme. Her følger en slik anvisning. 1. Gjett på et tall y. 2. Sjekk forskjellen mellom x og y2, og hvis den er liten nok 3. returner y, eller 4. forbedre gjettingen til gjennomsnittet av y og x/y, dvs. (y + x/y)/2, og gå tilbake til 2. ——————————————————————————————————————————— La yk = (yk–1 + x/yk–1)/2. Da virker ovenstående fordi uansett om yk er større eller mindre enn √x så ligger yk+1 mellom √x og yk (√x < (yk + x/yk)/2 < y0), så, enten er yk > √x for alle k >= 0, eller så er y0 < √x, og yk > √x for alle k > 0, og uansett minsker avstanden mellom yk og √x for økende k. 71 Litt mer detaljert La = y – √x. (Merk at for å unngå 0-divisjon eller negativ rot må y > 0, og dermed må > –√x.) 2 + x (y + x/y) ——— 2 Da får vi = √x + ———— 2(√x + ) se mellomregningen under. Uansett om er positiv eller negativ, dvs. uansett om y er større eller mindre enn √x, så vil (y + x/y)/2 være størren enn √x. Videre, hvis y > √x så vil x/y < y og dermed vil (y + x/y)/2 < (y + y)/2, som er det samme som y. Så hvis y0 > √x får vi √x < y1 < y0, √x < y2 < y1, osv. dvs. vi nærme oss √x ovenfra. Og hvis y0 < √x så vil y1 > √x, og vi nærmere oss √x ovenfra fra og med andre runde. Mellomregning (√x + + x/(√x + )) ———————— 2 = √x 2(√x + ) 2 + x ————— + ———— 2(√x + ) 2(√x + ) 72 Her sier vi det samme på en litt annen måte Vi forutsetter at x > 1, og dermed at √x > 1. Vi skal holde på til (y + x/y)/2 er tilstrekkelig nær x, eller m.a.o. til |(y + x/y)/2 – x| ≤ et gitt, tilstrekkelig lite, tall. |a| = absoluttverdien til a. Algoritmen virker fordi (y + z2 /y)/2 ≥ z for alle y og z. For å se det, skriver vi om ulikheten som følger (y + z2/y)/2 ≥ z (y2 + z2)/2y ≥ z y2 + z2 ≥ 2yz y2 – 2yz + z2 ≥ 0 Fordi (y – z)2 er et kvadrat, er ulikheten alltid tilfredstilt. Dette betyr at uansett hvilken verdi vi gjetter på, dvs. hvilken y vi starter med, så vil vi ha (y + x/y)/2 ≥ x Hvis y > x så er y > x/y og dermed er y > (y + x/y)/2, og hvis vi indekserer y-ene, slik at yk+1 = (yk + x/yk)/2, ser vi at Så lenge y > x, så er yk+1 < yk. Dvs. y blir mindre for hver runde, og vi nærmer oss x ovenfra. 73 (y – z)2 ≥ 0. I Scheme kan vi uttrykke alt dette ved følgende prosedyrer: (define (kvadratrot x) (løpende-rotgjetting 1.0 x)) (define (løpende-rotgjetting y x) (if (godt-nok-gjettet? y x) y (løpende-rotgjetting (forbedre y x) x))) (define (godt-nok-gjettet? y x) (< (abs (- (kvadrat y) x)) 0.001)) (define (forbedre y x) (gjennomsnitt y (/ x y))) (define (kvadrat x) (* x x)) (define (gjennomsnitt a b) (/ (+ a b) 2)) 74 I programmering dreier alt seg dypest sett om løkker, om vi akkumulerer data, f.eks. ved å addere eller multiplisere tall, eller leser tekstlige data fra fil, om vi søker etter et gitt datum i en mengde data eller vi søker løsningen på et problem. Punktene 1-4 representerer en løkke. 1. gjett 2. test 3 hvis suksess, returner eller 4 forbedre gjetting og gå tilbake til 2. Vi starter med en initiell gjetting i punkt 1. start og fortsetter med gjentatte tester i punkt 2 og test forbedret gjetting i punkt 4 fortsett inntil testen gir oss basistilefellet ferdig hvorpå vi returnerer den relevante verdien i punkt 3. exit. 75 Her ser vi hvordan de ulike argumentene endres for suksessive kall på gjetteprosedyren for x = 2. y |y2 – x| x/y (y + x/y)/2 1.0 |1 – 2| 2/1 (1.0 + 2)/2 = 1 = 2 = 1.5 |2.25 – 2| 2/1.5 (1.5 + 1.3333)/2 = 0.25 = 1.3333 = 1.4167 |2.007 – 2| 2/1.4167 (1.4167 + 1.4118)/2 = 0.007 = 1.4118 = 1.4142 1.5 1.4167 1.4142 |1.9999 – 2| = 0.0001 76 Abstrahering av prosedyrale nivåer Vi kan se på forholdet mellom de ulike prosedyrene som et abstraksjonshierarki. kvadratrot | løpende-rotgjetting __ | forbedre | gjennomsnitt | godt-nok-gjettet? | | kvadrat abs Poenget er at hvert nivå skal være fullstendig forståelig ut fra sine egne premisser. Hadde det ikke vært slik, ville vi heller ikke ha vært i stand til å programmere i f.eks. Scheme uten å kjenne til og forstå hvordan alle Scheme-primitivene var implementert. 77 Blokkstrukturer og interne definisjoner 1. definisjoner 2. uttrykk Med programmeringsspråket Algol (fra begynnelsen av 3. blokkstart 4. definisjoner 5. uttrykk 6. blokkstart 7. definisjoner 8. uttrykk 9. 10. 1960-tallet) ble blokkstrukturer innført, bl.a. for å kunne operere med lokale definisjoner. Dette var hverken med i FORTRAN eller den opprinnelige Lisp (de to første høynivåspråkene), men er med i Scheme. blokkslutt uttrykk 11. blokkslutt 12. uttrykk 78 Dette kan vi utnytte i definisjonen av kvadratrot, når vi sier at - prosedyrehodet utgjør blokkstart og - prosedyrens sluttparentes utgjør blokkslutt. (define (kvadratrot x) (define (godt-nok-gjettet? y) (< (abs (- (kvadrat y) x)) 0.001)) (define (forbedre y) (gjennomsnitt y (/ x y))) (define (løpende-rotgjetting y) (if (godt-nok-gjettet? y) y (løpende-rotgjetting (forbedre y)))) (løpende-rotgjetting 1.0)) Parameteren x, som ikke endres under beregningene, er synlig innenfor hele blokken og trenger ikke å sendes som argument til de lokale prosedyrene. Merk ellers at de generelle nytterutinene kvadrat og gjennomsnitt ikke hører til prosedyren. Kanskje noe overraskende gir hverken C, C++ eller Java mulighet for lokale prosedyrer—kun lokale variabler. 79 Prosedyrale prosesser Vi har to termer for løkker: (gjøre det samme (idem)) iterasjon generelt: løkker spesifikt: løkker med oppdatering av relevante (tilstands)variabler (løpe om igjen) rekursjon løkker ved prosedyrer som kaller seg selv med oppdaterte argumenter. Så å si alle språk har mekanismer for iterasjon i den spesifikke betydningen. Dette gjelder også Scheme, men måten å implementere løkker på i et funksjonelt språk, er ved rekursjon. I et språk som f.eks. java vil rekursjon alltid kreve mer plass og tid enn iterasjon, fordi prosedyrestakken fylles opp med én prosedyre for hvert rekursivt kall, og programmet må bruke tid på å nøste seg ut av rekursjonen. Som vi straks skal se, er dette ikke noe problem i Scheme, fordi Scheme gjenkjenner iterative prosesser som sådan, og returnerer umiddelbart etter siste rekursive kall. 80 Lineær rekursjon og iterasjon Prosedyren løpende-rotgjetting over gir en iterativ prosess. Når vi er fornøyd er resultatet alt regnet ut, og vi kan returnere dette umiddelbart. Fakultetesfunksjonen gir produktet av heltallene fra 1 til og med en gitt n. n! = 1·2· … ·(n – 1)·n Vi ser at funksjonen kan defineres reksursivt slik: n! = n(n – 1), n1 Dette danner utgangspunktet for følgende Scheme-implementasjon: (define (fakultet n) (if (= n 1) 1 (* n (fakultet (- n 1))))) 81 For å se hvordan denne virker, bruker vi substitusjonsmodellen (fakultet 6) (* 6 (fakultet 5)) ; kan ikke mutiplisere før vi har evaluert (a) = (fakultet 5) (*6 (* 5 (fakultet 4))) ; kan ikke mutiplisere før vi har evaluert (b) = (fakultet 4) (* 6 (* 5 (* 4 (fakultet 3)))) ; kan ikke mutiplisere før vi har evaluert (c) = (fakultet 3) (* 6 (* 5 (* 4 (* 3 (fakultet 2))))) ; kan ikke mutiplisere før vi har evaluert (d) = (fakultet 2) (* 6 (* 5 (* 4 (* 3 (* 2 (fakultet 1)))))) ; kan ikke mutiplisere før vi har evaluert (e) = (fakultet 1) ; Vi har basistilfellet som evaluerer til 1 (* 6 (* 5 (* 4 (* 3 (* 2 1))))) ; Vi setter inn 1 i (e), som evaluerer til 2 (* 6 (* 5 (* 4 (* 3 2)))) ; Vi setter inn 2 i (d) som evaluerer til 6 (* 6 (* 5 (* 4 6))) ; Vi setter inn 6 i (c) som evaluerer til 24 (* 6 (* 5 24)) ; Vi setter inn 24 i (b) som evaluerer til 120 (* 6 120) ; Vi setter inn 120 i (a) som evaluerer til 720 720 ; Vi returnerer 720 82 En alternativ løsning går ut på å - telle seg opp fra 1 til n eller ned fra n til 1 - samtidig som vi tar med oss et produkt - som suksessivt økes ved at det multipliseres ved telleren. (define (fakultet-iterativ n produkt) (if (= n 1) produkt (fakultet-iterativ (- n 1) (* produkt n)))) Her er en suksesjon av kall: (fakultet-iterativ 6 1) (fakultet-iterativ 5 6) ; 6 * 1 = 6 (fakultet-iterativ 4 30) ; 5 * 6 = 30 (fakultet-iterativ 3 120) ; 4 * 30 = 120 (fakultet-iterativ 2 360) ; 3 * 120 = 360 (fakultet-iterativ 1 720) ; 2 * 360 = 720 720 83 De to prosedyrene er begge rekursivt definert i den forstand at de kaller seg selv, men de gir opphav til to ulike prosesser hhv. en rekursiv og en iterativ prosess. - Den rekursive prosessen er kjennetegnet ved - funksjonsverdien selv går inn i det totale regnestykket, hvilket gir en kjede av utsatte operasjoner som vokser frem til siste rekursive kall, og deretter krymper, ettersom vi får returverdiene fra de enkelt kallene, innenfra og utover, og disse kan settes inn som operander i de enkelt operasjonene slik at disse kan utføres. - Den iterative prosesses er kjennetegnet ved at det er prosedyrens argumenter som går inn i regnestykket, og slik opptrere som variabler som suksessivt får sine verdier oppdatert. Forskjellen mellom iterative og rekursive prosesser er tema for første obligatoriske oppgave. 84 Halerekursjon La p være en rekursiv prosedyre, der kallet på p ligger ytterst i kroppen til p. Dette kalles halerekursjon (tail recursion), fordi når p returnerer, så er alt som skal gjøres gjort. La p kalles fra et sted q utenfor p. Alle beregningene i den løkken som dette genererer utføres ved oppdatering av argumentene til p, og dermed er ikke returverdien fra p relevant, før vi er tilbake ved q. Ved det innerste kallet på p, vil resultatet allerede foreligge i de oppdaterte argumenten, og vi kan hoppe direkte med dette reultatet til q. Alle Scheme-implementasjoner forutsettes å kunne oppdage når det foreligger halerekursjon, og sende returverdien direkte fra det innerste kallet til stedet for det ytterste kallet. 85 Tracing De fleste Scheme-omgivelser har mekanismer for tracing ettersporing av prosedyrekall. (I Racket må man først skrive (require racket/trace)). Her ser vi bare aktuelle argumentverdier og returverdier. (trace fakultet fakultet-iterativ) |(fakultet 6) |(fakultet-iterativ 6 1) | (fakultet 5) | (fakultet-iterativ 5 6) | |(fakultet 4) | |(fakultet-iterativ 4 30) | | (fakultet 3) | | (fakultet-iterativ 3 120) | | |(fakultet 2) | | |(fakultet-iterativ 2 360) | | | (fakultet 1) | | | (fakultet-iterativ 1 720) | | | 1 | | | 720 | | |2 | | |720 | | 6 | | 720 | |24 | |720 | 120 | 720 |720 |720 Ved tracing i DrSchemes suspenderes denne underliggende halerekursjonsmeknismen, og tracingen av fakultet-iterativ gir dermed inntrykk av en rekursiv prosess, 86 Allmenntilfellet og basistilfellet Testen i de to fakultetsprosedyrene skiller mellom det som gjelder generelt, for n > 1, allmenntilfellet, og det som gjelder spesielt, for n = 1, basistilfellet. Det siste er avgjørende for at prosessen skal terminere. Her er en alternativ iterativ versjon der vi teller opp, og der basistilfellet består i at vi har talt oss opp til og med n: (define (fakultet-iterativ i n produkt) (if (> i n) produkt (fakultet-iterativ (+ i 1) n (produkt * i)))) 87 Tre-rekursjon Fra Leonardo Pisano alias Fibonacci: Liber Abaci 1202: "En man gjerder inne et [ungt] kaninpar. Hvor mange kaninpar produseres fra dette i løpet av et år, når vi antar at hvert par får et nytt par i måneden, og et nytt par trenger en måned på å bli produktive?" mnd (1) (2) (3) (4) (5) (6) Herfra blir det for mye styr å holde rede på hvert par, så vi nøyer oss med å telle dem (7) (8) (9) (10) (11) (12) akkummulerte par Vi har ett ungt par A Par A blir modent og parer seg Par A føder par B og parer seg igjen Par A føder par C og parer seg nok engang, og par B blir modent og parer seg. Par A og B føder par D og E og parer seg igjen, og par C blir modent og parer seg Parene A , B og C føder par F , G og H og parer seg igjen, og parene D og E blir modne og parer seg De 5 svangre parene fra runde (6) føder 5 nye og parer seg igjen, mens de 3 nye parene fra runde (6) blir modne og parer seg De 8 svangre parene fra runde (7) føder 8 nye par og parer seg igjen mens de 5 nye parene fra runde (7) blir modne og parer seg De 13 svangre parene fra runde (8) føder 13 nye par og parer seg igjen mens de 8 nye parene fra runde (8) blir modne og parer seg De 21 svangre parene fra runde (9) føder 21 nye par og parer seg igjen mens de 13 nye parene fra runde (9) blir modne og parer seg De 34 svangre parene fra runde (10) føder 34 nye par og parer seg igjen mens de 21 nyeene fra runde (10) par blir modne og parer seg De 55 svangre parene fra runde (11) føder 55 nye par 88 1 1 2 3 5 8 13 21 34 55 89 144 Basistilfellet er greit: I måned 1 er første par ennå ikke kjønssmodne, så det blir ingen besvangring i måned 1 og ingen fødsler i måned 2 og dermed er antall par i andre måned = antall par i første måned = 1. Allmenntilfellet, fra og med tredje måned, er mer komplisert enn for fakultetsberegningen, fordi vi må ta hensyn til både hvor mange som ble født og hvor mange som ble svangre i forrige måned, og det siste er avhengig av hvor mange som ble født i måneden før forrige. For k > 2 vil alle par i måned k – 2, både tidligere fødte og nyfødte, være kjønnsmodne måneden etter, så antall besvangringer i måned k – 1 = antall par i måned k – 2, antall par i måned k – 2, fødte i måned k = antall antall par i måned k = antall par i måned k – 1 + antall besvangrede i måned k – 1 = antall par i måned k – 1 + antall Dermed får vi at Ut fra dette kan vi definere fibonaccifunksjonen slik i Scheme 89 par i måned k – 2 (define (fibonacci n) (cond ((= n 1) 1) ((= n 2) 1) (else (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))) Om vi lar funksjonen også være definert for 0, med funksjonsverdien 0, så får vi stadig fibonacci(2)= fibonacci(1)+ fibonacci(0)= 0 + 1 = 1 Funksjonen blir da seende slik ut. (define (fibonacci n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))) Eller med andre ord (define (fibonacci n) (if (< n 2) n (+ (fibonacci (- n 1)) (fibonacci (- n 2))))) 90 Et problem med denne algoritmen er alle de gjentatte beregningene. | | fib(4)==> 3 | | | | fib(3)==> 2 | | | | | | fib(2)==> 1 fib(1)==> 1 | | | | | | fib(1)==> 1 fib(0)==> 0 fib(5)==> 5 | | | | fib(2)==> 1 | | | | | | fib(1)==> 1 fib(0)==> 0 91 | | fib(3)==> 2 | | | | | | fib(2)==> 1 fib(1)==> 1 | | | | | | fib(1)==> 1 fib(0)==> 0 For å unngå dette benytter vi oss av at, siden fibk = fibk-1 + fibk-2 så er første tall i summen i én runde andre tall i summen i neste runde. (define (fibonacci n) (fib-iter 1 0 2 n)) (define (fib-iter fibk-1 fibk-2 k n) (if (= k n) (+ fibk-1 fibk-2) (fib-iter (+ fibk-1 fibk-2) fibk-1 (+ k 1) n))) fibk = fibk-1 + fibk-2 1 2 3 5 fibk-1 1 1 2 3 fibk-2 0 1 1 2 Men for at denne algoritmen skal virke, må vi kalle prosedyren med k = 2, og vi kan dermed ikke bruke den for n < 2. 92 k 2 3 4 5 n 5 5 5 5 For å tillempe algoritmen slik at den virket for alle n ≥ 2, kunne vi ha lagt inn spesielle tester i fibonacci for n < 2. Alternativt kan vi - la telleren ligge én runde foran summen, og - returnere fibk-1 når k = n. Eller, slik det gjøres i læreboka, - la telleren ligge to runder foran, men vi må da - telle ned fra n til 0 og - returnere fibk-2 når n = 0. (define (fib-iter fibk-1 fibk-2 n) (if (= n 0) fibk-2 (fib-iter (+ fibk-1 fibk-2) fibk-1 (- n 1)))) fibk-1 1 1 2 3 5 8 fibk-2 0 1 1 2 3 5 n (= antall runder igjen) 5 4 3 2 1 0 93 Merk at siden n ikke inngår i noe regnestykke, så er det likegyldig om vi teller opp eller ned. Uansett skal prosedyren kalles med fibk-1 = 1 og fibk-2 = 0. Kort om eksponenter potenser (powers) og logaritmer En potens er et produkt av n like faktorer, f.eks. kan 5 × 5 × 5 × 5 skrives som 54. (1) a0 = 1 (6) anam = an+m (2) a1 = a (7) an/am = an–m (3) a2 = aa, (8) (an)m = anm (4) a–p = 1/ap (9) xn = enlog x (5) ap/q = q√(ap) = (q√a)p (10) x = b a3 = aaa, … l og b x Ad (10) Gitt en base (et grunntall) b, så er logaritmen til x, det tallet b må opphøyes i for å gi x. Det følger av (1), at log 1 = 0, for en hvilken som helst base. Tallet e er det tallet for hvilket (11) loge e = 1. loge x kalles den naturlige logaritmen til x og skrives vanligvis bare 'log x' eller 'ln x'. Algoritme for å finne log2 x: Gå ut fra at log2 x = 1 + log2 x/2 for hele positive n, og at log2 0 = 0. For alle baser > 0 bryter funksjonen sammen—har funksjonene en singularitet—for x = 0, dvs. det finnes ingen a og b slik at ba = 0. 00 er ubestemt. Vi kunne jo bli enige om at 00 skulle være 0, men det finne en rekke gode grunner til heller å la 00 være 1. 94 Eksponensiering Algoritme for å regne ut bn: Gå i løkke inntil n = 0 og - gang b med seg selv og - reduser n med 1. Rekursivt (define (expt b n) (if (= n 0) 1 (* b (expt b (- n 1))))) Iterativt (define (expt-iter b produkt n) (if (= n 0) produkt (expt-iter b (* produkt b) (- n 1))) Begge prosessene har lineært tidsforbruk. Den rekursive har også lineært plassforbruk, mens den iterative har plassforbruk = 1. 95 Hvordan kan vi effektivisere eksponensieringsalgoritmen? Det beste vi kan håpe på er en reduksjon fra linær til logaritmisk vekst. Og dette kan vi også få til vha. suksessive halveringer — på tilsvarende måte som ved binært søk (mer om det siden). (b b b b b b b b b b b b b b b b) b^16 (b^8)^2 ((b^4)^2)^2 (((b^2)^2)^2)^2 ((((b)^2)^2)^2)^2 (b b b b b b b b )2 ((b b b b)2)2 (((b b)2)2)2 (((b)2)2)2)2 Dette gårt greit når eksponenten er en potens av 2, men bare da. (define (fast-expt-p2 b n) (if (= n 1) b (square (fast-expt-p2 b (/ n 2))))) 96 Men vi ønsker å gjøre dette generelt, og vi tar da utganspunkt i følgende (a) bn = (bn/2)2 for like n (b) bn = bbn-1 for odde n Rasjonaliseringsgevinsten ligger i (a) og vi bruker (b), om nødvendig, for å komme til (a) i neste runde. Dette gir følgende Scheme-prosedyre (define (fast-expt b n) (cond ((= n 0) 1) ((even? n) (square (fast-expt b (/ n 2)))) (else (* b (fast-expt b (- n 1)))))) 97 (Merk at basistilfellet i fast-expt-p2 er n = 1, mens det i alle de andre variantene, inklusive fast-expt, er n = 0. Grunnen er at n i fast-expt-p2 reduseres utelukkende ved divisjon med 2, mens den i alle de andre variantene reduseres ved subtraksjon — i alle fall for odde n. I fast-expt-p2 vil n før eller siden bli 1,og hadde vi fortsatt med reduksjon etter dette, ville vi ha fått en brøk med 1 som teller og voksende potenser av 2 som nevner, og aldri nådd 0. I fast-expt derimot, vil vi, for en eventuelt n = 2, få n = 1 i neste runde, og deretter en reduksjon til n = 0, fordi 1 er odde.) 98 Permutasjoner og faktoriell vekst Her er et tre som viser alle permutasjoner av strengen "abcd": a b c d ——————————————————————————————————————————————————————— a b c d b a a a c d b b d c d c ————————————— ————————————— ————————————— ————————————— a a a b b b c c c d d d b c d a c d a b d a b c c b b d a a b a a b a a d d c c d c d d b c c b ———- ———- ———- ———- ———- ———- ———- ———- ———- ———- ———- ———a a a a a a b b b b b b c c c c c c d d d d d d b b c c d d a a c c d d a a b b d d a a b b c c c d b d b c c d a d a c b d a d a b b c a c a b d c d b c b d c d a c a d b d a b a c b c a b a - For det første får vi fire permutasjoner med hver av de fire bosktavene i første posisjon. - For hver av disse får vi tre permutasjoner med hver av de tre etterfølgende bokstavene i andre posisjon. - Og for hver av disse igjen får vi to permutasjoner med hver av de to etterfølgende bokstavene i tredje posisjon. - Endelig får vi for hver av disse én permutasjon. Alt i alt får vi 4321 = 4! = 24 permutasjoner, og vi ser lett at med en 5-tegns streng, får vi 5 ganger så mange, dvs. 5! = 120, permutasjoner, og generelt får vi for n verdier n! permutasjoner. 99 Tids- og plassbehov Lærebokas uttrykk "order of growth" har ingen god overettelse i norsk. Vi kan kalle det vekstordenen til ressursbehovet for beregningen av en funksjon for økende input, eller størrelsesordenen til et ressursbehov (tids- eller plassbehov) for et gitt input, n. Hvis f og g er funksjoner, og k1 og k2 er konstanter, og ressursbehovet for beregningen av f(n) ligger mellom k1g(n) og k2g(n), sier vi for følgende verdier av g at veksten til f er av følgende typer. ln n logaritmisk n, lineær nP plynomisk xn eksponentiell n! faktoriell (g P, dvs. g er et polynom) Noen ganger fingraderer vi og skiller mellom f.eks. radikal vekst (g(n) = n), kvadratisk vekst (g(n) = n2) og kubisk vekst (g(n) = n3). 100 Her er noen eksempler: Søking kan gjøres linært, med lineært tidsforbruk, eller, hvis mengden er sortert, stegvis med radikalt tidforbruk eller binært med logaritmisk tidforbruk. lineært søk linær tid ( n) stegvis søk radikal tid ( 2√n) binært søk logaritmisk tid ( log2 n) 1 2 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 - - - - - - - - - - - - - - - - - s - - - - - - - - - - - - - - - - - - - - - - - - > - - - - - - - - - - - - - - - - - s - - - - - - - > < - - - - - - - - - - - - - - - - - - - - - - - - - s s s s s s - - - - - - - - - - - - - - - Ingen av disse gir noe plassforbruk (ut over 1, og den plass data allerede opptar). En rekke velkjente enkle arlogritmer for sortering har kvadratisk tidsforbruk, mens de raskteste, som Merge-sort, har et idéelt tidsforbruk på nlog2 n. Symbolet (stor theta) brukes for å angi veksten (the order of growth) i ressursbruken til en prosess. I lære boka innføres det en funksjon R(n) som gir ressursbehovet til en gitt prosess for input n. 101 Ressursene kan være antall regneoperasjoner, dvs. tid, eller antall bits, dvs. plass. Vi kunne like gjerne snakke om kompleksiteten C til en funksjon f — C(f(n)). Gitt en funksjon g(n) og to konstanter k1 og k2, uavhengig av n, slik at k1g(n) C(f(n)) k2g(n), sier vi at C(f(n)) = (g(n)) eller, noe enklere, at kompleksiteten til f = (g), eller, aller enklest, at f (g). f kompleksitet linært søk (n) stegvis søk (√n) binært søk (log2 n) vanlig sortering (n2) merge-sort (nlog2 n). 102 (fra Hardy and Wright 1979, pp. 7-8). Forholdet mellom theta, big-O og little-o Let n be an integer variable which tends to infinity and let x be a continuous variable tending to some limit. Also, let (n) or (x) be a positive function and f(n) or f(x) any function. Then the symbols O(x) (sometimes called "big-O") and o(x) (sometimes called "little-o") are known as the Landau symbols and defined as follows. 1. f = O() means that |f| < A for some constant A and all values of n and x, 2. f = o() means that f/ 0. A function is in big-theta of f if it is not much worse but also not much better than f. Big-omega notation is the inverse of the Landau symbol O, (f(n)) = O(f(n)) (f(n)). f(n) O(g(n)) g(n) (f(n)) |f| = størrelsen til f = antall kall på f eller det antall bits som skal til for å beskrive en løsning—f.eks. et rekursjonstre eller utføre beregningen. Angivelser av kompleksitet kan være tilsynelatende ganske grove. F.eks dekker (n2) hele spekteret an2 + bn + c mens polynomisk tid dekker det betydelig større spektret a1nm + a2nm-1 + … + am-1n2 + amn, men når n blir tilstrekkelig stor blir verdiene til leddene etter det første så små at vi ikke bryr oss om dem. Ellers er det slik at eksponentiell og fakultær vekst gjør problemløsningen praktisk umulig for n over en viss størrelse—faktisk slik at noen problemer ville kreve mer enn universets levetid, eller at beskrivelsen av problemets løsning ville kreve flere atomer enn det finnes i hele universet. Et problemer med eksponensiell vekst går typisk ut på å finne alle mulige kombinasjoner av ett eller annet (eks: powerset). Et problemer med faktoriell vekst går ut på å finne alle permutasjoner av en liste (se over). Et tema innenfor programmeringsteori gjelder reduksjon av arbeidsmengde —typisk fra eksponentiell til polynomisk tid, eller fra lineær til logaritmisk tid. Eksempel på det første: Vi skal finne rasketse vei gjennom et nettverk av gater fra A til B. Eksponentiell vekst får vi hvis vi rett og slett måler opp alle mulige veier for å kunne velge ut den korteste av disse. Vi kan imidlertid redusere dette til polynomisk vekst vha. snedige algoritmer som vi ikke skal ta opp her (men i oblig 2). 103 104 Forelesning 3 Prosedyreobjekter og prosedyrer av høyere orden Her er noen prosedyrer for summering av rekker av tall basistilfellet, initialverdi almenntilfellet, summeringen (define (sum-integers a b) (if (> a b) 0 (+ a (sum-integers (+ a 1) b)))) (+ (square a) (sum-squares (+ a 1) b)))) (+ (cube a) (sum-cubes (+ a 1) b)))) 0 (+ (sqrt a) (sum-roots (+ a 1) b)))) 0 (+ (/ 1.0 (* a (+ a 2))) (pi-sum (+ a 4) b)))) (define (sum-squares a b) (if (> a b) 0 (define (sum-cubes a b) (if (> a b) 0 (define (sum-roots a b) (if (> a b) (define (pi-sum a b) (if (> a b) 1 1 1 Den siste er basert på rekken /8 = ——— + ——— + ——— + 13 57 911 105 Tilsammen fremviser disse et mønster med variasjoner, tilsvarende en prosedyre som tar varierende argumenter, med den forskjell at variasjonene her ikke angår data, men selve fremgangsmåten. Mønsteret består i at vi summerer en sekvens av funksjonsverdier, mens variasjonene består i funksjonen og endringen av argumentet til denne. summering funksjon argumentendring sum-heltall f(x) = x next(x) = x + 1 sum-kvadrater f(x) = x2 next(x) = x + 1 sum-kuber f(x) = x3 next(x) = x + 1 sum-røtter f(x) = √x next(x) = x + 1 pi-sum f(x) = 1/(x2 + 2x) next(x) = x + 4 I Scheme kan vi bruke prosedyrer som argumenter, på linje med bl.a. tall og vi sier at er prosedyrer er førsteklasses objekter. 106 Vi kan i lys av dette definere den generelle summeprosedyren slik (define (sum f (if (> a b) ; funksjonen a ; argumentet til f next ; iteratoren, fremdriveren b) ; basisverdien, målverdien 0 (+ (f a) (sum f (next a) next b)))) Vi har nå gjort f til en høyereordensprosedyre, dvs. en prosedyre som tar ett eller flere prosedyreargumenter eller returnerer prosedyrer eller begge deler. For de fire første summene trenger vi en prosedyre for inkrementering av argumentverdien samt identitetsfunskjonen og prosedyrer for kvadrering, kubering og kvadratrotsutrekking (define (inc i) (+ i 1)) (define (identity x) x) (define (square x) (* x x)) (define (cube x) (* x x x)) kvadratrotsprosedyren sqrt er en Scheme-primitiv, 107 Med dette kan vi skrive om de fire første spesifikke summeringsprosedyrene slik. (define (sum-integers a b) (sum identity a inc b)) (define (sum-squares a b) (sum square a inc b)) (define (sum-cubes a b) (sum cube a inc b)) (define (sum-roots a b) (sum sqrt a inc b)) Merk at selv om ikke identity, square, cube og inc er primitiver eller biblioteksrutiner, er de generelle nok til å kunne ha forsvart en plass blant disse—på linje med sqrt. Det samme kan vi imidlertid ikke si om de funksjonene vi bruker i pi-sum, og vi definerer derfor disse ad hoc som lokale prosedyrer. (define (pi-sum a b) (define (f x) (/ 1.0 (* x (+ x 2)))) ; lokal prosedyre for beregning av funksjonsverdi (define (next x) (+ x 4)) ; lokal prosedyre for endring av argumentverdi (sum f a next b)) ; kroppen til pi-sum 108 Lambda Den tilgrunnliggende mekanismen for prosedyredefinering i Scheme er Lambda. Formen (define (<prosedyrenavn> <formelle parametre>) <prosedyrekopp>) som vi har brukt så langt, er egentlig en omskrivning av (syntaktisk sukker for) formen (define <prosedyrenavn> (lambda (<formelle parametre>) <prosedyrekopp>)) Eks: (define (square x) (* x x)) (define square (lambda (x) (* x x))) Nå behøver ikke et lambda-uttrykk som dette alltid stå inne i et definisjonsuttrykk. Det kan faktisk opptre helt på egenhånd — f.eks. slik: ((lambda (x) (* x x)) 3) 9 I standard lambda-notasjon (se første forelesning): ( x . x2)(3), NB! dette er ikke et S-uttrykk. Et S-uttrykk består står av en parentes med en prosedyre på første plass. 109 Legg merke til parentesen rundt lambdauttrykket og det etterfølgende tallet. Regelen for evaluering av et sammensatt uttrykk sier at vi først skal evaluere leddene og deretter anvende det første resultatet på de øvrige (om noen). Gitt den siste definisjon av kvadrat, er ((lambda (x) (* x x)) 3) (square 3) Det ser vi også om vi her setter inn definiens (lambda (x) (* x x)) definiendum square for i et kall på square. (square 3) ((lambda (x) (* x x)) 3) square evaluerer per definisjon til (lambda (x) (* x x)). 110 Lambda er en spesialform (med samme enkle parentessyntaks som alle andre Scheme-former) som returnerer en prosedyre. Og når lamda-uttrykket er evaluert, kan den returnerte prosedyren brukes som en hvilken som helst annen prosedyre, uansett hvor lambda-uttrykket måtte opptre. For en prosedyre som pi-sum, der vi nettopp har bruk for ad hoc engangsprosedyrer, betyr dette at vi kan legge de aktuelle prosedyrene direkte inn på argumentplassene i kallet på sum. Opprinnelig versjon Ny versjon (define (pi-sum a b) (define (pi-sum a b) (define (f x) (/ 1.0 (* x (+ x 2)))) (sum (lambda (x) (/ 1.0 (* x (+ x 2)))) (define (next x) (+ x 4)) a (sum f a next b)) (lambda (x) (+ x 4)) b)) 111 Let, let* og letrec I prinsippet kan lambda-konstruksjonen sies å ligge til grunn for alt. La oss først se på en enkel variabeldefinisjon med en påfølgende bruk av den definerte variabelen. (define v 5) (* v 2) 10 Dette kan, i tråd med lambda-notasjon, forstås som en avledning av ((lambda (v) (* v 2)) 5) 10 med den forskjell at skopet til v ikke er globalt, men begrenset til kroppen til lambda-uttrykket. En annen avledning får vi med formen let (let ((v 5)) (* v 2)) Den enste forskjellen mellom de siste er leseligheten. I let-uttrykket er det lettere å se hva som bindes til hva. 10 112 De tre formene representert ved uttrykkene 10 (define v 5) (* v 2) ((lambda (v) (* v 2)) 5) 10 (let ((v 5)) (* v 2)) 10 er altså ekvivalente, men let-formen har et par fortrinn mht. leselighet. - I forhold til define-konstruksjonen er fordelen med let at vi tydelig angir, og avgrenser, det området i programmet der bindingen mellom variabelen og dens verdi gjelder. - I forhold til lambda-konstruksjonen er fordelene med let at vi angir den verdien variabelen skal bindes til foran kroppen. 113 Den generelle formen til let er (let ((v1 e1) ; variable1 expression1 (v2 e2) … (vn en)) <kropp>) Merk at bindingene mellom variablene og deres verdier ikke gjøres gjeldende før i kroppen. Det betyr at vi ikke kan definere én variabel i den innledende listen vha. en annen. (let ((a 1) (b (+ a 1))) ; ULOVLIG b) Her kan ikke returverdien til uttrykket bestemmes, fordi verdien til b forsøkes definert ved en ennå ikke definert a. For å løse dette kan vi bruke nøstede let-uttrykk: (let ((a 1)) (let ((b (+ a 1))) b) 114 Eller vi kan bruke forenklingen let*, som nettopp sikrer at let* er syntaktisk sukker variablene bindes i tur og orden allerede i variabellisten. for nøstet let. (let* ((a 1) (b (+ a 1))) b) Vi kan også bruke let til å definere prosedyrer (let ((p (lambda (x) (+ x 2)))) (p 2)) 4 Men for å definere rekursive prosedyrer må vi bruke letrec. (letrec ((fac (lambda (n) (if (= n 1) 1 (* n (fac (- n 1))))))) (fac 5)) 120 115 ; Siste høyreparentes svarer til ; første venstrparentes etter letrec . ; Siste høyreparentes svarer til ; venstrparentesen foran letrec . Binding av variabler Vi kaller forholdet mellom en variabel og dens verdi en binding. Definerte variabler er bundet mens udefinerte variabler er frie. (define f (lambda (y)(+ x y))) y er bundet men x er fri (define g (lambda (x)(lambda (y)(+ x y)))) både x og y er bundet Et forsøk på å bruke en fri variabel gir kjøreavbrudd. x crasher fordi x er fri (f 5) crasher fordi x er fri ((g 3) 5) går bra Prosedyreparametre er formelt bundne variabler i prosedyrens kropp. Prosedyreparametre er rent konkret bundet den forstand at idet prosedyren utføres er så er variablene bundet til argumentverdiene. 116 Eksempel: Regning på datoer For datoer innefor et århundre ønsker vi å kunne - konvertere mellom enkeltvise åttesifrede tall og tripler med år måned og dag, og - sjekke om en gitt dato er lovlig. Som enkelttall skal datoen ha formen aaaammdd. F.eks. 1997.5.17 skal skrives 19970117. * Vi lager et slikt tall fra en gitt datotrippel ved å - gange året med 10000, - måneden med 100 og - legge sammen resultatene av disse multiplikasjonene og dagen. F.eks. gir 17. mai 1997: 1997 * 10000 + 5 * 100 + 17 = 19970517. For å komme tilbake til trippelen må vi for å få - året, dele (heltallsdividere) på 1000, år - måneden, først dele på 100 og deretter dele = 19970517/10000 = 1997 måned = resten((19970517/100), 100) = 5 resultatet på 100, og ta resten etter divisjonen, - dagen, dele på 100, og ta resten etter divisjonen. dag = resten(19970517, 100) * Det er verdt å merke, selv om det ikke har betydning her, at denne formen uten vider gir rett sorteringsorden for datoer. 117 = 17 Fra ddmmaa til (dag måned år) Vi nøyer vi oss med å konvertere den ene veien — fra tall til dato-trippel. Til dette benytter vi funksjonene quotient og remainder. quotient tar to tallargumenter a og b og returnerer resultatet av divisjonen a/b avrundet ned til nærmeste hele tall. (quotient 13 5) (Se også truncate.) 2 remainder tar to heltallsargumenter a og b og returnerer resten etter heltallsdivisjonen a/b. (remainder 13 5) 3 (13 / 5 = 2, og 13 - 25 = 3) Lar vi n være det åttesifrede tallet, er datotrippelen gitt ved: år: (quotient n 10000) måned: (remainder (quotient n 100) 100) dag: (remainder n 100) For å sjekke datoens lovlighet må vi sjekke dagen mot de ulike månedslengdene, og i den forbindelse må vi sjekke spesielt for februar om året er et skuddår . Her bruker vi predikatet leapyear? som er det samme som skuddår? i et tidligere eksempel. 118 Implementasjon Vi er nå klare til å skrive funksjonen eight-digits->date-triple. Funksjonen tar et tall som argument og returnerer om mulig en lovlig datotrippel eller #f. (define (eight-digits->date-triple n) (let ((year (quotient n 10000)) (month (remainder (quotient n 100) 100))) (if (or (< month 1) (> month 12)) #f (let ((day (remmainder n 100)) (maxday (cond ((or (= month 1) (= month 3) (= month 5) (= month 7) (= month 8) (= month 10) (= month 12)) 31) ((or (= month 4) (= month 6) (= month 9) (= month 11)) 30) (else (if (leapyear? year) 29 28))) (and (>= day 0) (<= day maxday) (list day month year))))) ;* (eigth-digits->date-triple 19970117) (1997 5 17) * Hvis de to første ”argumentene” til and evaluerer til #t, blir også det siste ”argumentet” evaluert, og siden dette ikke evaluerer til #f, evealuerer heller ikke and-utrrykket til #f, men derimot til resultatet av evalueringen av det siste argumentet, nemlig (list day month year). 119 Hvor skal year-variabelen introduseres? year brukes ikke utenfor alternativet i if-uttrykket, og burde kanskje ha vært introdusert der. (if (or (< month 1) (> month 12)) #f (let ((year (quotient n 10000)) (day (remainder n 100)) (maxday (cond ((or (= month 1) (= month 3) (= month 5) (= month 7) (= month 8) (= month 10) (= month 12)) 31) ((or (= month 4) (= month 6) (= month 9) (= month 11)) 30) (else (if (leapyear year) 29 28)) ...) Dette vil imidlertid ikke virke fordi year brukes i beregningen av maxday, mens variablene i let-uttrykket ikke er bundet før i uttrykkets kropp. 120 stedet må vi legge et let-uttrykk til rundt det innerste — slik: (let ((year (quotient n 10000)) (let ((day (remainder n 100)) (maxday <beregn-maxday>) (and (>= day 1) (<= d maxday) (list day month year))))))) ...) Eller vi kan bruke let*. (let* ((year (quotient n 10000)) (day (remainder n 100)) (maxday <beregn-maxday>) (and (>= day 1) (<= d maxday) (list day month year))))))) 121 Med let* kan vi forsåvidt legge alle variablene i funksjonen inn i én og samme liste. I stedet for å sjekke månedslengden før alt annet, lar vi cond-utrrykket for beregning av maxday returner 0, for ulovlig måned, slik at alle dagnumre blir ulovlige. (define (eight-digits->date-triple n) (let* ((month (remainder (quotient n 100) 100)) (year (quotient n 10000)) (day (remainder n 100)) (maxday (cond ((or (= month 1) (= month 3) (= month 5) (= month 7) (= month 8) (= month 10) (= month 12)) 31) ((or (= month 4) (= month 6) (= month 9) (= month 11)) 30) ((= month 2) (if (leapyear? year) 29 28)) (else 0)))) (and (>= d 1) (<= d maxday) (list day month year)))) 122 123 124 Noen flere eksempler på bruk av lambda En klassiker: (define (make-adder num) (lambda (x) (+ x num))) ((make-adder 4) 7) 11 Vi bruker substitusjon for å se hva som foregår: (make-adder 4) (lambda (x) (+ x 4)) ((make-adder 4) 7) ((lambda (x) (+ x 4)) 7) (+ 7 4) 11 ((lambda (y) ((lambda (x) (+ x y)) 7)) 4)) 11 Med let ser det slik ut. (let ((y 4) (let (x 7) (+ x y)) 11 125 Hvilke navn gir mening hvor og når? I definisjonen av make-adder gir x mening i lambda-utrykkets kropp, og bare der, mens num gir mening i den ytre funksjonskroppen og dermed også i lambda-utrykkets kropp. I neste omgang, når vi kaller make-adder for å få laget en prosedyre, sender vi et aktuelt argument på den plassen num angir. Dette bindes til num, i definisjonen av resultatprosedyren. Resultatprosedyren tar ett argument, angitt ved variabelen x. I siste omgang, når vi kaller resultatprosedyren, dvs. resultatet av kallet på make-adder, binder vi x til det aktuelle argumentet og erstatter x med dette under utførelsen av kallet. 126 Den prosedyren make-adder returnerer, la oss kalle den p, har med seg den omgivelsen den returneres fra, der num er bundet til argumentet til make-adder. Denne omgivelsen er utilgjengelig for alle andre enn p. Når p kalles opprettes det en omgivelse "innenfor" omgivelsen til p, der x bindes til argumentet til p, og uttrykket (+ x num) evalueres. (define (make-adder num) (lambda (x) (+ x num))) num = 4 (make-adder 4) (lambda (x) (+ x num)) num = 4 ((make-adder 4) 7) x=7 (+ x num)) 127 (sml. et objekts private felt i Java) Et annet eksempel: (define (flip proc) (lambda (a b) (proc b a))) Argumentet til flip skal være en prosedyre som tar to argumenter, f.eks. subtraksjonsprosedyren -, og returnerer en prosedyre som bytter rekkefølgen på argumentene til argumentprosedyren under utførelsen. (- 5 2) 3 ((flip -) 5 2) -3 (/ 3 6) 1/2 ((flip /) 3 6) 2 (> 3 6) #f ((flip >) 3 6) #t ((flip string-append) "tine" "kan") "kantine" 128 Litt om tall Naturlige tall De positive heltallene {1, 2, 3, ...} . Noen regner også 0 som et naturlig tall. Vi angir for mengden av naturlige tall. Heltall (integer) {..., -2, -1, 0, 1, 2, ....} Vi angir (tysk Zahl) for mengden av heltall. Et heltall n kan være negativt (n Rasjonale tall - ), null (n = 0), positivt (n + ) eller nonnegativt (n * (fra latin ratio som bl.a betyr utregning—her: (utregning av) forholdet mellom to tall) Et tall som kan uttrykkes som en brøk p/q der p og q er heltall og q 0, kalles et rasjonalt tall med teller (numerator) p og nevner (denominator) q. Vi angir for mengden av rasjonale tall. De rasjonale tallene kan, i likhet med heltallene, telles, dvs. oppramses (enumeres). (søk på countable numbers) (At en mengde er tellbare betyr ikke at vi alltid kan finne ut hvor stor den er ved å telle dens elementer. En mengde kan være både tellbar og uendelig.) Det er alltid mulig å finne et rasjonaltt tall mellom hvilke som helst to rasjonale tall, Vi sier at de rasjonale tallene er tette, noe heltallene ikke er, men ikke kontinuerlige, slik de reelle tallene er. 129 = + {0}). Irrasjonale tall = ikke-rasjonale tall (eg. ikke utregnbare) . Noen av disse som e, og 2 er mer kjent enn andre Reelle tall Unionen av de rasjonale og de irrasjonale tall , angitt ved symbolet . Mengden av reell tall kalles også kontinuum (el. kontinuet el. kontinuumet), angitt ved symbolet c. Komplekse og imgainære tall De reelle tall kan utvides ved tilføyelsen av det imaginære tallet i = -1. Tall på formen x + iy, der x and y begge er reelle tall, kalles komplekse tall. Transcendentale tall er tall som ikke er løsning av noe polynom (x er løsningen av et polynom hvis a0 + a1x + a2x2 + ... + anxn = 0). Mest kjent er og e. 130 Telling av rasjonale tall Vi kan anskueliggjøre som en matrise der radene har formen 1/n, 2/n, 3/n, … og kolonnene har formen n/1, n/2, n/3, …. Men hvordan teller vi tallene i matrisen, når radene og kolonnene hver for seg er uendelige? 1/1 2/1 3/1 1/2 2/2 1/3 2/3 1/4 1/6 1/7 7/1 6/1 6/2 5/3 4/4 3/5 2/6 5/2 4/3 3/4 2/5 5/1 4/2 3/3 2/4 1/5 3/2 4/1 8/1 ... 7/2 6/3 5/4 4/5 3/6 2/7 1/8 : Dette gir alle tallene i , men det gir også en masse tall vi ikke ønsker å ha med, dvs. vil vil bare ha med de tall x/y der x og y er relativt prime (hvilket vil si at gcd(x, y) = 1). 131 Lukning i algebra De naturlige tall er lukket under addisjon og multiplikasjon, idet enhver sum s = a + b og ethvert produkt p = ab, der a og b er naturlige tall, er selv naturlige tall. Heltallene er også lukket under subtraksjon, og de rasjonale tallene er dessuten lukket under divisjon. De relle tallene har samme lukningsegenskaper som rasjonale. De komplekse tallene er dessuten lukket under rotutrekning. Lukning i algebra nevnes bl.a. for å unngå forveksling med lukning, closure, i betydningen innpakning av en prosedyre sammen med dennes omgivelser, slik at enhver instans av prosedyren har med seg variablene i disse omgivelser, også utenfor det stedet der prosedyren er definert. Forfatterne av SICP mener at Begrepet lukning bør være reservert for algebra. 132 Dataabstraksjoner Vi gjorde tidligere et poeng av at prosedyrene i et program inngår i et hierarkisk system der vi kan gå inn på de ulike nivåer og gi fullstendige beskrivelser av hva som foregår, ut fra en trygghet om at prosedyrene på lavere nivåer virker etter sin hensikt. Dette er én side ved abstrahering —bl.a kalt black-box-abstrahering. Vi skal nå knytte begrepet asbtrahering nærmere til begrepet datatype. Vi skal bl.a. se at det går an å snakke utfyllende om aritmetiske talloperasjoner, uten å ha noe begrep om hvordan heltall er representert på dypere nivåer, og vi skal anskueliggjøre dette vha. ulike representasjoner av heltall. Vi starter imidlertid med å se på rasjonale tallbrøker (fractions), om man vil. 133 Vi går ut fra at et rasjonalt tall på en eller annen måte er satt sammen av to hele tall kalt numerator (teller—angir antall deler) og denominator (nevner—angir del-størrelsen). Dette uttrykker vi i Scheme vha en konstruktur-prosedyre og to selektor-prosedyrer: (define (make-rat x y) <det rasjonale tallet hvis numerator = x og denominator = y>) (define (numer x) <numeratoren til det rasjonale tallet x>) (define (denom x) <denominatoren til det rasjonale tallet x>) 134 Vi ønsker å definere operasjonene addisjon, subtraksjon, multiplikasjon og divisjon for rasjonale tall (jfr. disses lukningsegenskaper). Uavhengig av representasjonsmåten skal følgende aritmetiske forhold gjelde: Eksempler a c + b d = a d + c b b d ( 1) a c - b d = a d - c b b d ( 2) a c b d = a c b d ( 3) a b c d = a c = b d 2 4 3 + 2 5 + = 5 3 5 3 = 2 4 3 - 2 5 + = 5 3 5 3 = 4 22 15 4 4 2 5 3 = 2 15 4 2 5 3 = 4 3 2 5 = 8 15 4 a d c b ( 4) 5 = 2 3 a d = c b ( 5) 135 12 10 6 (= ) 5 Dette uttrykker vi i Scheme ved følgende prosedyrer (define (add-rat x y) ; se (1) (make-rat (+ (* (numer x) (denom y)) (* (numer y) (denom x))) (* (denom x) (denom y)))) (define (sub-rat x y) ; se (2) (make-rat (- (* (numer x) (denom y)) (* (numer y) (denom x))) (* (denom x) (denom y)))) (define (mul-rat x y) ; se (3) (make-rat (* (numer x) (numer y)) (* (denom x) (denom y)))) (define (div-rat x y) ; se (4) (make-rat (* (numer x) (denom y)) (* (denom x) (numer y)))) (define (equal-rat? x y) ; se (5) (= (* (numer x) (denom y)) (* (numer y) (denom x)))) 136 Merk at vi har innført og brukt, men ennå ikke implementert, konstruktoren make-rat og selektorene numer og denom. Men ovenstående må gjelde uansett hvordan disse implementeres. Én implementasjon går ut på å bruke det fundamentale Lisp-begrepet par. Et par konstrueres ved prosedyren cons, cons er en forkortelse for construct, og elementene selekteres vha. hhv. car og cdr. car er en forkortelse for (cons 1 2) (1 . 2) Contents of the Address part of Register number cdr er en forkortelse for (define par (cons 1 2)) ; konstruktor (car par) 1 ; selektor for første del av paret (cdr par) 2 ; selektor for andre del av paret Contents of the Decrement part of Register number Vha. disse primitivene kan vi definere konstruktorene og selekteorene for rasjonale tall slik: (define (make-rat x y) (cons x y)) (define (numer x) (car x)) (define (denom x) (cdr x)) 137 Vi kan også innfører en dedisert utskriftsprosedyre for rasjonale tall vha. den generelle utskriftsprosedyren display. (define (print-rat x) (newline) (display (numer x)) (display "/") (display (denom x))) newline gir linjeskift. display tar ett argument av en hvilken som helst type og skriver ut dettes verdi. I motsetning til REPL, skriver display ut strenger uten anførselstegn rundt dem. (define one-third (make-rat 1 3)) (print-rat one-third) 1/3 Utskriftsprosedyren skriver ut numerator og denominator hver for seg, uten hensyn til forholdet mellom dem. 138 Eks: (add-rat one-third one-third) => (6 . 9) (print-rat (add-rat one-third one-third)) (Se implementasjonene på side 128) 6/9 Som vi ser kan denne brøken reduseres. (display (+ (/ 1 3) (/ 1 3))) 2/3 Dette kan vi også få til — vha. primitiven gcd som tar to eller flere tall som argumenter og returneres disses største felles divisor (greatest common divisor). (gcd 420 378) 42 420 = 22357. 378 = 23337. Felles: 42 = 237. 42 deler både 420 og 378, og det finnes ikke noe tall større enn 42 som deler både 420 og 378. Vi får da 378 378/42 9 —— = ——— = — 420 420/42 10 139 Vi kan enten (a) bruke gcd i konstruktoren make-rat (define (make-rat x y) (let ((g (gcd x y) (cons (/ x g) (/ y g)))) eller (b) i selektorene numer og denom. Mer kompakt (define (numer x) (define (numer x) (/ (car x) (gcd (car x) (cdr x)))) (let ((g (gcd (car x) (cdr x)))) (/ (car x) g))) (define (denom x) (define (denom x) (/ (cdr x) (gcd (car x) (cdr x)))) (let ((g (gcd (car x) (cdr x)))) (/ (cdr x) g))) På det abstraksjonsnivået der våre rasjonaltallsprosedyrer brukes, er det ingen semantisk forskjell mellom (a) og (b). Men det kan være en effektivitetsmessig forskjell, som er uten interesse i denne sammenhengen. 140 Abstraksjonsbarrierer De fleste objektorienterte språk har syntaktiske mekanismer for å skille mellom et objekts - private (skjulte beskyttede) egenskaper - typisk datafelt, og metoder som kan endre disse, og - alment tilgjengelige - typisk metoder som gir tilgang til visse data, eller utfører beregninger eller andre oppgaver. I Scheme-programmering har vi ingen syntaks for slike skiller, men vi insisterer på visse konvensjoner. I henhold til disse implementerer vi våre datatyper ved et sett prosedyrer som typisk omfatter - et predikat (eller flere) - en konstruktor (eller flere) og - noen selektorer. 141 Brukeren av typen får signaturene1 til, men ikke implementasjonene av, disse prosedyrene. Eks.1: type par predikat Eks.2: type rasjonal pair? predikat rational? konstruktor cons konstruktor make-rat selektorer car selektorer numer cdr ;* denom sammenligner equal-rat? * Det finnes en Scheme-primitiv med navnet rational?, men her må en eventuell implementasjon være vår egen, i samsvar med de øvrige implementasjonene på samme nivå. Det er imidlertid ingenting i veien for å implementere vår egen rational? vha. primitiven rational? Her representer Eks.2 en abstraksjonsbarriere mot brukeren av våre prosedyrer for rasjonale tall, i den forstand at brukeren må kjenne prosedyrenes navn, men ikke trenger å vite hvordan prosedyrene er implementert. I forhold til omverdene, er det eneste interessante at 1 gitt x = (make-rat n d), så garanterer vi at (numer x) / (denom x) = n/d. ; når n og d er heltall, Ikke helt som i Java. Siden Scheme er dynamisk typet, må paramater- og returtypene angis i dokumentasjonen 142 Par Vi kan generalisere dette til at en datatype er gitt ved - ett sett med prosedyrer for konstruksjon og seleksjon og - ett sett med betingelser som disse prosedyrer må tilfredstille. En overraskende implementasjon av par Her følger en konstruktor og to selektorer som tilfredsstiller vår forpliktelse overfor bruker hva angår par. (define (cons x y) (define (dispatch message) (cond ((= message 0) x) ((= message 1) y) (else (error "Argument must be 0 or 1 CONS" message)))) dispatch) (define (car z) (z 0)) (define (cdr z) (z 1)) 143 Vi legger for det første merke til at returverdien fra cons er en én-arguments prosedyre nærmere bestemt den lokale prosedyren dispatch (formidle, send videre) Dernest merker vi oss at i kroppen til hver av de to selektorene car og cdr opptrer den formelle parameteren z som en prosedyre, i kroppen til car kalles z med 0 som argument i kroppen til cdr kalles z med 1 som argument Prosedyren z er en instans av den lokale dispatch-prosedyren i cons. Denne har hele tiden med seg de omgivelsene den ble skapt innenfor. Her omfatter omgivelsene Sammenlign parametrene x og y med bundet til de verdiene disse fikk ved make-adder det kallet som førte til at prosedyren ble instansiert. på side 129. 144 Med ovenstående implementasjon av konstruktoren cons og selektorene car og cdr kan vi definere et par med nøyaktig samme semantikk som tidligere (s. 151): (define par (cons 1 2)) ; par er nå et prosedyreobjekt med en omgivelse der x og y er bundet til hhv. 1 og 2 (car par) 1 ; car kalles med prosedyreobjektet par som argument (cdr par) 2 ; car kalles med prosedyreobjektet par som argument Men legg igjen merke til at med denne implementasjonen av cons returneres en prosedyre—ikke et par på formen (x . y). (cons 1 2) #<procedure:formidle> ———————— NB! Uavhengig av hvordan par faktisk er implementert i Scheme viser ovenstående at vi kan implementere par på denne måten, fordi prosedyrer er førsteklasses objekter. 145 En omskriving av cons-implementasjon vha. lambda Vi kan skrive om den ovennevnte implementasjonen av cons vha. lambda. Siden dispatch-prosedyren ikke kalles i konstrukturen, trenger den heller ikke noe navn der, og vi kan returnere en anonym prosedyre direkte. (define (cons x y) (lambda(message) (cond ((= message 0) x) ((= message 1) y) (else (error "Argument must be 0 or 1 CONS" message))))) 146 Hierarkier og lukningsegenskaper Par er en svært nyttig og effektiv byggesten for konstruksjon av praktisk talt alle de sammensatte datatyper en kan tenke seg. I SICP brukes følgende grafiske representasjon av par. Som vi ser kan vi bruke cons til å lage par hvis elementer er par hvilket vil si at par er lukket under cons. 147 Dette gjør det mulig å skape hierarkiske datastrukturer ––trær. sykdomsbehandling medisinsk kirurgisk kniv laser fysioterapeutisk medikamentelt profylaktisk massasje alternativt trening ad hoc religiøst omvendelse forbønn healing snåsisk '(sykdomsbehandling (medisinsk (kirurgisk (kniv laser)) (medikamentelt (profylaktisk ad-hoc))) (fysioterapeutisk (massasje trening)) (alternativt (religiøst (omvendelse forbønn)) (healing (snåsisk)))) På neste side ser vi hvordan ovenstående kan konstrueres vha. cons. Der bruker vi følgende kode for a lage et par med et ”tomt” andre-element: (cons 1 '()) ==> (1) Den enkle apostrofen gjør det mulig å bruke ”variabler” (symboler) som ikke er definert. 148 (cons 'sykdomsbehandling (cons (cons 'medisinsk (cons (cons 'kirurgisk (cons (cons 'kniv (cons 'laser '())) '())) (cons (cons 'medikamentelt (cons (cons 'profylaktisk (cons 'ad-hoc '())) '())) '()))) (cons (cons 'fysioterapeutisk (cons (cons 'massasje (cons 'trening '())) '())) (cons (cons 'alternativt (cons (cons 'religiøst (cons (cons 'omvendelse (cons 'forbønn '())) '())) (cons (cons 'healing (cons (cons 'snåsisk '()) '())) '()))) '())))) 149 Lister Før vi ser nærmere på trær, skal vi ta for oss den enkelere listestrukturen. (cons 1 (cons 2 (cons 3 (cons 4 '())))) (1 2 3 4) NB! I læreboken brukes verdien nil som synonym for den tomme listen representert grafisk ved boksen med diagonal strek. Ellers angis den tomme listen med en tom parentes prefikset med en enkel apostrof: '(), slik som i figuren over, og (uten å problematisere bruken av ' i denne omgang). vi skal for vår del stort sett bruke denne varianten NB! Verdien nil er ikke definert i R5RS. 150 For å forenkle konstruksjonen av lister har Scheme prosedyren list. (list 1 2 3 4) (cons 1 (cons 2 (cons 3 (cons 4 '())))) Merk at denne ekvivalensen innebærer at også følgende ekvivalenser gjelder: (list 1 2 3 4) (cons 1 (list 2 3 4)) (cons 1 (cons 2 (list 3 4))) slik at f.eks. (car (list 1 2 3 4)) 1 (cdr (list 1 2 3 4)) (2 3 4) (car (cdr (list 1 2 3 4))) 2 (cdr (cdr (list 1 2 3 4))) (3 4) (car (cdr (cdr (list 1 2 3 4)))) 3 (cdr (cdr (cdr (list 1 2 3 4)))) (4) (car (cdr (cdr (cdr (list 1 2 3 4))))) 4 (cdr (cdr (cdr (cdr (list 1 2 3 4))))) '() Siden det er nokså vanlig å kombinere de to selektorene på alle mulige måter, finnes det egne biblioteksprosedyrer på formen cXr, der X kan være fra 2 til 4 kombinasjoner av a-er og/eller d-er (caar, cadr, …, cdddar, cddddr) 151 Vha. list kan sykdomsbehandlingstreet konstrueres slik: (list 'sykdomsbehandling (list 'medisinsk (list 'kirurgisk (list 'kniv 'laser)) (list 'medikamentelt (list 'profylaktisk 'ad-hoc))) (list 'fysioterapeutisk (list 'massasje 'trening)) (list 'alternativt (list 'religiøst (list 'omvendelse 'forbønn)) (list 'healing (list 'snåsisk)))) 152 Flere dataabstraksjoner Tall En ukeoppogave går ut på å implementere heltall på ulike måter. Her dreier det som tre nivåer: (a) abstraksjonen av heltallene mht. de operasjoner under hvilke de er lukket, (b) den absktrakte implementasjonen disse operasjonen, og (c) den konkrete implementasjonen av disse operasjonene mht. deres fysiske / visuelle / symbolske representasjon. 153 For lukningsegenskapene, nivå (a), bruker vi succ og pred fra nivå (b) til å definerer funksjonene add og mul, for hhv. addisjon og multiplikasjon som er de operasjonene heltallene er lukket under (sammen med subtraksjon, som vi ikke bryr oss med i her). På nivå (b) ser vi hen til at heltallene er tellbare, og ordnet, slik at det ene tallet følger etter det andre—f.eks. slik at 3 følger etter 2. Vi kan dermed innføre etterfølgerfunksjonen inc og increment forgjengerfunksjonen dec decrement som vi definerer vha. de repreresentasjonsavhengige basisprosedyrene på nivå (c). 154 På nivå (c) definere vi etterfølgerfunksjonen med henblikk på hvordan tallene er representert, som en linje med enkeltvise (discrete) punkter, ordnede rekker med sifre, etc. Implemnterer vi succ og pred vha. Scheme-primitivene + og – (se oppgave 1.9, s. 36), vil nivå (c) bestå av implementasjonen av disse i Scheme selv. En annen mulig representasjon er rekker av tomme parenteser der etterfølgeren til f.eks. ()() er ()()(), eller et nøste av parentester der etterfølgeren til (()) er ((())). Her kan vi på nivå (c) bruke typen par med konstruktoren cons og selektorene car og cdr. 155 Nedenstående er svar på en ukeoppgave, men det skader ikke at dere har sett løsningen før, så lenge dere ikke titter under arbeidet med løsningen Tall generelt (define (plus x y) (if (zero? x) y (inc (plus (dec x) y)))) (define (times x y) (if (zero? x) 0 (plus (times (dec x) y) y))) Sifrede tall (define (inc x) (+ x 1)) (define (dec x) (- x 1)) (define number? number?) ; Scheme primitve 156 Brede tall (define zero? null?) (define zero '()) (define (inc x) (cons '() x)) (define (dec x) (cdr x)) (define (number? x) (or (zero? x) (and (zero? (car x)) (number? (cdr x))))) Noen brede tall (define x (inc (inc (dec (inc (inc zero)))))) ; (() () ()) (define y (dec (inc (dec (inc (inc (inc zero))))))) ; (() ()) (define z (inc (inc (inc y)))) (plus x y) ; (() () () ()) (() () () () ()) (times y z) (() () () () () () () ()) 157 Dype tall (define zero? null?) (define zero '()) (define (inc x) (cons x '())) ; ==> (x), dvs. lista med x som første og eneste element (define (dec x) (car x)) ; ==> første (og eneste) element i x. (define (number? x) (or (null? x) (and (null? (cdr x)) (tall? (car x))))) Noen dype tall tilsvarer sifret ta 3 (define x (inc (inc (dec (inc (inc zero)))))) ; (((()))) (define y (inc (inc x))) ; (((((()))))) tilsvarer sifret ta 4 (plus x y) ((((((((())))))))) (times x y) (((((((((((((((()))))))))))))))) 158 ; tilsvarer sifret ta 4 ; tilsvarer sifret ta 12 159 160 Forelesning 4 Operasjoner på lister Konstruktoren list tar ingen, ett eller flere argumenter og returnerer listen med de gitte argumentene. Listen konstrueres rekursivt vha. cons. (list 1 2 3 4) (cons 1 (list 2 3 4)) (cons 1 (cons 2 (list 3 4))) (cons 1 (cons 2 (cons 3 (list 4))) (cons 1 (cons 2 (cons 3 (cons 4 '())))) I hvert ledd (par) p i en liste er (car p) elementet, verdien, i leddet og (cdr p) resten av en listen. (define L (list 1 2 3 4)) (define M (list (list 1 2) 3 4)) (car (cdr (car (cdr ... (car (car (car (cdr ... L) ==> L) ==> (cdr L)) ==> (cdr L)) ==> 1 (2 3 4) 2 (3 4) 161 M) ==> (car M)) ==> (cdr (car M))) ==> M) ==> (1 2) 1 2 (3 4) Hente ut et element fra en gitt posisjon i en liste. (define (list-ref items n) list-ref kunne ha vært gort mer robust i forhold til en mulig for stor n, (if (= n 0) (car items) (list-ref (cdr items) (- n 1)))) ved en ekstra sjekk for tom liste, men dette ville stort sett ikke ha vært hensiktsmessig, siden en slik feil gjerne er et symptom på dårlig logikk, som vi ønsker å luke ut av koden. Scheme-primtiven list-ref er like lite robust som ovenstående. Vi teller elementene fra 0, Å telle fra 0 i stedet for fra 1 er både naturlig og bekvemt i programmering, slik at f.eks. det tredje elementet har ref = 2. - naturlig fordi avstanden til første element i en liste eller vektor = 0 - bekvemt ved modulus(klokke)-sekvenser, der restverdi 0 betyr begynnelsen på en ny runde. Merk at underveis angir n posisjonen til (klokka går fra 0 til 24, ikke fra 1 til 25). det søkte elementet i den gjenværende lista. (list-ref '(1 2 3 4) 2) 3 (list-ref '(1 2 3 4) 5) cdr krever et par, men fikk den tomme listen items n items n (1 2 3 4) (2 3 4) (3 4) 2 1 0 (1 2 3 4) (2 3 4) (3 4) (4) () 5 4 3 2 1 listen er tom men n er ennå ikke 0. 162 Beregne lengden til en liste. (define (length items) (if (null? items) 0 (+ 1 (length (cdr items)))) Iterativ variant (define (length items) (define (length-iter lst n) (if (null? lst) n (length-iter (cdr lst) (+ n 1)))) (length-iter items 0)) 163 Skjøte en liste til en annen (define (append list1 list2) (if (null? list1) ; Basis: list1 er tom, så vi returnerer list2, der alle elementene i list2 ; opprinnelig list1 nå ligger foran første element i opprinn. (cons (car list1) ; Allment: Legg første gjenværende element i list1 foran list2. (append (cdr list1) list2)))) ; sammenskjøtingen av resten av list1 og økende list2. . (append '(1 2 3) '(4 5 6)) (cons 1 (append '(2 3) '(4 5 6))) (cons 1 (cons 2 (append '(3) '(4 5 6)))) (cons 1 (cons 2 (cons 3 (append '() '(4 5 6))))) (cons 1 (cons 2 (cons 3 '(4 5 6))))) (cons 1 (cons 2 '(3 4 5 6))))) (cons 1 '(2 3 4 5 6))))) (1 2 3 4 5 6) 164 Avbildning (mapping) av lister Map (avbildning) Skalering En koblingsoperasjon mellom elementene i to mengder (define (scale-list items factor) F.eks. mellom tall og tall, som i scale-list, eller mellom tall og tegn, i en tegntabell. (if (null? items) '() Eller m.a.o: Gitt mengdene A og B: En assossiasjon (cons (* factor (car items)) mellom hvert element i A og et eller annet element i B. (scale-list (cdr items) factor)))) (scale-list '(1 2 3 4 5) 10) Eller m.a.o: En avbildning f : AB er en funksjon f slik at det for hver a A finnes et element f(a) B. (cons (* 1 10) (scale-list '(2 3 4 5) 10)) (cons (* 1 10) (cons (* 2 10) (scale-list '(3 4 5) 10))) ... (cons 10 (cons 20 (cons 30 (cons 40 (cons 50 '()))))) (10 20 30 40 50) 165 Filtrering av en liste (define (remove-evens int-list) (cond ((null? int-list) '()) ((even? (car int-list)) (remove-evens (cdr int-list))) (else (cons (car int-list) (remove-items (cdr int-list)))))) (define (filter predicate sequence) (cond ((null? sequence) '()) ((predicate (car sequence)) (cons (car sequence) (filter predicate (cdr sequence)))) (else (filter predicate (cdr sequence))))) (filter odd? '(1 2 3 4 5 6 7 8 9 10))) ==> (1 3 5 7 9) (filter (lambda (x) (= 0 (remainder x 3))) '(1 2 3 4 5 6 7 8 9 10))) ==> (3 6 9) odd? og even? er biblioteksrutiner. 166 Eksempel på lister med annet enn tall (define (count-vowals letters) (cond ((null? letters) 0) ((member (car letters) '(a e i o u y æ ø å)) ; er dennet bokstaven i mengden av vokaler? (+ 1 (count-vowals (cdr letters)))) (else (count-vowals (cdr letters))))) (count-vowals '(i n s t i t u t t – f o r – i n f o r m a t i k k)) 8 Primitiven member (Se R5RS 6.3.2) er et semipredikat som tar en verdi og en liste og returnerer enten listen f.o.m. første forekomst av den gitte verdien, For semipredikater bruker vi konvensjonelt eller #f, hvis verdien ikke ble funnet i listen. ikke et spørsmålstegn sist i navnet. (member 'u '(a e i o u y æ ø å)) ==> (u y æ ø å) (member 's '(a e i o u y æ ø å)) ==> #f (define (vowal? c) (member c '(a e i o u y æ ø å))) (filter vowal? '(p y t h o n)) (y o) (length (filter vowal? '(i n s t i t u t t – f o r – i n f o r m a t i k k))) 8 167 Generisk mapping (define (map proc items) (if (null? items) '() (cons (proc (car items)) (map proc (cdr items))))) Mapping fra heltall til heltall (map (lambda (x) (* x x x)) '(1 2 3 4)) (1 8 27 64) Mapping fra heltall til boolean (map even? '(1 2 3 4)) (#f #t #f #t) Mapping fra heltall til symbol (map (lambda (x) (if (odd? x) 'odd 'even)) '(1 2 3 4)) (odd even odd even) Skalering ved mapping Hvis vi substituerer map med dens definisjon, (define (scale-list items factor) får vi scale-list slik den er definert over. (map (lambda (x) (* x factor)) items)) 168 Hierarkiske strukturer — trær Her er et lite tre med 6 noder, hvorav 4 er blader, dvs. ikke-par. (define tre (cons (list 1 2) (list 3 4))) Vi merker oss at treet er en liste med tre elementer, hvorav det første selv er en liste. (length tre) 3 Gitt en prosedyre count-leaves som teller bladene i et gitt tre (her 1, 2, 3, 4) får vi. (count-leaves tre) 4 ; implementasjonen står på neste side Om vi dobler treet ved å lage en liste med treet selv og én to kopi, (define dobbelttre (list tre tre) (((1 2) 3 4) ((1 2) 3 4))) får vi: (length dobbelttre) 2 (count-leaves dobbelttre) 8 169 For å telle bladene i et tre, må vi forholde oss til at noen elementer er trær, dvs. lister, mens andre er blader og skal telles. Vi har ikke innført noe liste-predikat, men vi har et par-predikat, Det finnes også en primitiv list? og i og med at en liste er et par, kan vi bruke dette. men den tar vi senere. (define (count-leaves x) ; null? gir #t for den tomme listen — #f for alt annet (cond ((null? x) 0) ((not (pair? x)) 1) ; x er et blad, så legg til 1 (else ; x er et ikke-tomt tre, så (+ (count-leaves (car x)) (count-leaves (cdr x)))))) ; tell og legg sammen antall blader i første del (car-delen) ; og antall blader i resten av listen (cdr-delen) (count-leaves '((1 2) 3 4)) / \ (+ (count-leaves '(1 2)) (count-leaves '(3 4))) (+ (+ 1 (count-leaves '(2)) (+ 1 (count-leaves '(4))) (+ (+ 1 (+ 1 (count-leaves '()))) (+ (+ 1 (+ 1 0))) (+ (+ 1 1) (+ 1 (+ 1 (count-leaves '())))) \ / (+ 2 2) 4 (+ 1 (+ 1 0))) (+ 1 1)) Den parallellprosesseringen som er vist her, er ikke reell. Som kjent vil evalueringen av første ledd i et sammensattuttrykk gå til bunns før evalueringen av andre ledd starter. 170 Merk testfølgen i prosedyren over. Den tomme listen tilfredstiller begge testene (null? x) og (not (pair? x)). Hadde vi snudd testrekkeølgen ville vi ha også ha talt med tomme grener. I koden under er testrekkefølgen snudd, og den gir dermed gir galt resultat. (define (count-leaves-and-nil x) ; x er et blad eller nil (cond ((not (pair? x)) 1) ((null? x) 0) ; fanges opp av forrige clause, og slår aldri til (else (+ (count-leaves-and-nil (car x)) ; x er et ikke tomt tre (count-leaves-and-nil (cdr x)))))) (count-leaves '((1 2) 3 4)) 4 (count-leaves-and-nil '((1 2) 3 4)) 6 171 Avbildning av trær Skalering Vi skalerer et tre på tilsvarende måte som vi skalerer en liste, dvs. ved å skalere hvert enkelt data-element (blad). Ellers følger algoritmen samme mønster som bladtellingsalgoritmen. (define (scale-tree tree factor) ; returner det tomme treet (cond ((null? tree) '()) ((not (pair? tree)) (* tree factor)) ; returner skaleringen av bladverdien (else (cons (scale-tree (car tree) factor) ; returner et nytt par bestående av (scale-tree (cdr tree) factor))))) ; skaleringen av venstre og høyre sub-tre Legg nøye merke til at returverdien er et nytt tre ikke en modifikasjon av det opprinnelige 172 (scale-tree ((1 2) 3 4) 2) ; skaleringsfaktoren = 2 tre inn | (cons ; ytterste cons ------------; arg 1 til ytterste cons | | | | (scale-tree (1 2) 2) | | | (cons ; nest ytterste1 cons ----3 4 ; arg 1 til nest ytterste1 cons | | | | | | (scale-tree 1 2) | | | | | (* 1 2) 1 2 | | | | | 2 | | | | 2 ; evaluert arg 1 til nest ytterste1 cons | | | | (scale-tree (2) 2) ; arg 2 til nest ytterste1 cons | | | | | (cons ; innerste cons1 | | | | | | (scale-tree 2 2) ; arg 1 til innerste1 cons | | | | | | | (* 2 2) | | | | | | | 4 | | | | | | 4--------------------------; evaluert arg 1 til innerste1 cons | | | | | | (scale-tree () 2) ; arg 2 til innerste1 cons1 | | | | | | ()-------------------------; evaluert arg 2 til innerste1 cons | | | | | (4)-------------------------; evaluert innerste cons1 | | | | (4)------------------------- ; evaluert arg 2 til nest ytterste1 cons | | | (2 4)-------------------------; evaluert nest ytterste1 cons | | (2 4)------------------------; evaluert arg 1 til ytterste cons | | (scale-tree (3 4) 2) ; arg 2 til ytterste cons | | | (cons ; nest ytterste2 cons | | | | (scale-tree 3 2) ; arg 1 til nest ytterste2 cons | | | | | (* 3 2) | | | | | 6 | | | | 6 ---------------------------; evaluert arg 1 til nest ytterste2 cons | | | | (scale-tree (4) 2) ; arg 2 til nest ytterste2 cons | | | | | (cons ; innerste2 cons | | | | | | (scale-tree 4 2) ; arg 1 til innerste2 cons | | | | | | | (* 4 2) | | | | | | | 8 | | | | | | 8--------------------------; evaluert arg 1 til innerste2 cons | | | | | | (scale-tree () 2) ; arg 2 til innerste2 cons | | | | | | ()-------------------------; evaluert arg 2 til innerste2 cons | | | | | (8)--------------------------; evaluert innerste2 cons tre ut | | | | (8)--------------------------; evaluert arg 2 til nest ytterste2 cons ------------| | | | | | (6 8)-------------------------; evaluert nest ytterste2 cons | | (6 8)--------------------------; evaluert arg 1 til ytterste cons ----6 8 | ((2 4) 6 8)---------------------; evaluert ytterste cons | | ((2 4) 6 8)----------------------; evaluert scale-tree 2 4 173 Alternativt kan vi skalere treet ved å avbilde hvert subtre slik (define (scale-tree tree factor) (map (lambda (sub-tree) (if (pair? sub-tree) (scale-tree sub-tree factor) ; scalér subtreet (* sub-tree factor))) ; scalér bladet tree)) Her er envariant av ovenstående der vi har gitt den prosedyren vi mapper på, et navn. (define (scale-tree tree factor) (define (scale-sub-tree sub-tree) (if (pair? sub-tree) (scale-tree sub-tree factor) ; scalér subtreet, merk at det rekursive kallet går via hovedprosedyren (* sub-tree factor))) ; scalér bladet (map scale-sub-tree tree)) 174 Primitiver og bibliotekstprosedyrer for par og lister (pair? obj) ==> #t hvis obj er et par (merk at den tomme listen ikke er et par) (list? obj) ==> #t hvis obj er en liste (null? obj) ==> #t hvis obj er den tomme listen (cons obj1 obj2) ==> paret der første og andre element er hhv.obj1 og obj2 (list obj1 ...) ==> listen med de gitte objektene (append L1 L2 ...) ==> sammenskjøtingen av de gitt listene (to eller flere) (car pair) ==> første element i det gitte paret (cdr pair) ==> andre element i det gitte paret (caar L) ==> (car (car L)) (cadr L) ... (cdddar L) ==> (car (cdr L)) ==> (cdr (cdr (cdr (car L)))) (cddddr L) ==> (cdr (cdr (cdr (cdr L)))) (length L) ==> antall elementer i listen L (reverse L) ==> listen med elementene i listen L i omvendt rekkefølge (list-ref L k) ==> k'te element i listen L, regnet fra 0. (list-tail L k) ==> sublisten av listen L etter de k første elementene (member x L) ==> sublisten av listen L f.o.m. første forekomst av x eller #f, hvis x ikke finnes i L 175 Sammenhengen mellom bruk av cons og append og nedtelling og opptelling (define (enum n) (if (= n 0) '() (cons n (enum (- n 1))))) (enum 5) (5 4 3 2 1) (define (enum n) ; SYNKENDE (if (= n 0) '() (append (enum (- n 1)) (list n)))) (enum 5) (1 2 3 4 5) ; STIGENDE (define (enum a b) (if (> a b) '() (cons a (enum (+ a 1) b)))) (enum 1 5) (1 2 3 4 5) ; STIGENDE (define (enum a b) (if (> a b) '() (append (list a) (enum (+ a 1) b)))) (enum 1 5) (1 2 3 4 5) ; STIGENDE (define (enum a b) (if (> a b) '() (append (enum (+ a 1) b) (list a)))) (enum 1 5) (5 4 3 2 1) ; SYNKENDE 176 SICP 2.2.3 Sekvensielle operasjoner Vi ser på to prosedyrer som tilsynelatende gjør nokså ulike ting. Den ene tar et tre som argument og beregner summen av kvadratene av alle odde tall i treet. (define (sum-odd-squares tree) (cond ((null? tree) 0) ((not (pair? tree)) (if (odd? tree) (square tree) 0)) (else (+ (sum-odd-squares (car tree)) ; Basis 0: ikke noe mer i dette subtreet ; Basis 1: et tall, ; regn det med hvis det er odde. ; Almenntilfellet: et tre ; addér summen for car-subtreet (sum-odd-squares (cdr tree)))))) ; og summen for cdr-subtreet Den andre tar et heltall n og lager en liste over alle partall blant de n første fibonacci-tall. (define (even-fibs n) (define (next k) (if (> k n) '() (let ((f (fib k))) (if (even? f) (cons f (next (+ k 1))) (next (+ k 1)))))) ; Merk at next gir en rekursiv prosess ; Basis: Vi har talt oss frem til og med n’te fibonaccitall, så ; returner den tomme listen ; Allment: Behandle k’te fibonaccitall. ; Skal det være med, så ; legger vi det inn foran de eventuelle etterfølgende, ; og hvis ikke, tar vi bare med de eventuelle etterfølgende. (next 0)) 177 Vi kan fremstille de to prosessene parallelt slik: sum-odd-squares even-fibs regn opp: blader regn opp: heltall filtrér: odde avbild: fibonér avbild: kvadrér filtrér: partall akkumulér: +, 0 akkumulér: cons, ’() Bytter vi rekkefølgen mellom filtrering og avbildning i den en eller den andre kolonnen, blir parallellen fullstendig. Men for å generalisere, kan vi ikke ta utgangspunkt i prosedyrer der delprosessene er vevd sammen i én og samme prosess. I stedet skiller vi ut de enkelt delprosessene og plasserer dem i en sekvens av prosesser. Det vi gjør får preg av signalbehandling, der vi sender signaler gjennom filtre, forsterkere, omformere og akkumulatorer. 178 Oppregning (enumerering) i en fortløpende flat liste av verdier (flat = ikke et tre) ; lag en liste med tallene fra og med a til og med b. (define (enumerate-interval a b) (if (> a b) '() (cons a (enumerate-interval (+ a 1) b)))) ; lag en liste med alle elementene i tree. (define (enum-tree tree) (cond ((null? tree) nil) ((not (pair? tree)) (list tree)) (else (append (enum-tree (car tree)) ; skjøt sammen enumereringen av venstre subtre (enum-tree (cdr tree)))))) ; og enumereringen av høyre subtre Merk at vi her konverterer fra tre til liste, og derfor bruker append Vi skjøter sammen enumereringen av treet i car-delen og av treet i cdr-delen i løpende node. mens vi i scale-tree (6 sider tidligere) mapper fra et tre til et annet, og derfor bruker cons. Vi rekonstruerer strukturen til treet, men gir bladene nytt innhold. 179 Akkumulering — kombinering av elementene i en liste til én verdi—f.eks. en liste av tall til en sum (define (accumulate combine init-val seq) ; combine må være en binær (to-arguments) prosedyre (if (null? seq) init-val (combine (car seq) (accumulate combine init-val (cdr seq))))) (accumulate + 0 (enum-interval 1 6)) 21 (accumulate * 1 (enum-interval 1 6)) 720 (accumulate sqrt 1 (enum-interval 1 6)) RT-feil: sqrt tar kun ett arg. (accumulate + 0 (1 2 3 4)) (+ 1 (accumulate + 0 (2 3 4))) (+ 1 (+ 2 (accumulate + 0 (3 4)))) (+ 1 (+ 2 (+ 3 (accumulate + 0 (4))))) (+ 1 (+ 2 (+ 3 (+ 4 (accumulate + 0 ()))))) (+ 1 (+ 2 (+ 3 (+ 4 0)))) (+ 1 (+ 2 (+ 3 4))) (+ 1 (+ 2 7)) (+ 1 9) 10 180 Redefinering av sum-odd-squares og even-fibs (fra 3. forelesning) som operasjonssekvenser: (define (sum-odd-squares tree) (accumulate + 0 (map square (filter odd? (enum-tree tree))))) (define (even-fibs n) (accumulate cons '() (filter even? (map fib (enum-interval 0 n))))) Legg merke til at i det siste eksemplet, der kombinatoren er cons, er returverdien selv en liste. (Dette gjør eksemplet (som er fra SICP) litt tullete, siden vi alt har en liste, returnert fra filter, men det viser parallellen mellom de to operasjonssekvensene.) 181 Nøstet avbildning Problem: Gitt et heltall n, finn alle ordnede par (i, j) for 1 i < j n, alle par (i, j) mellom 1 og n slik at i < j slik at summen i + j er et primtall! Sluttresultatet skal være en liste med tripler på formen (i j i j i+j). Eks: (2 5 7) output Dette kan gjøres i en nestet løkke: 2 1 - for hver j fra 2 til n, 3 1 2 ((2 3 5))) 4 1 2 3 ((1 4 5) (3 4 7)) 5 1 2 3 4 ((2 5 7)) 6 1 2 3 4 5 ((1 6 7) (5 6 11))) - for hver i fra 1 til j – 1, - hvis i + j er et primtall - lag en trippel. (((1 2 3)) Vi kunne også løpt gjennom i'ene i i j den yttre og j'ene i den indre løkka: 1 2 3 4 5 6 - for hver i fra 1 til n - 1, 2 3 4 5 6 ((2 3 5) (2 5 7))) 3 4 5 6 ((3 4 7)) 4 5 6 () 5 6 ((5 6 11))) - for hver j fra i + 1 til n, - hvis i + j er et primtall - lag en trippel. output 182 (((1 2 3) (1 4 5) (1 6 7)) La oss i første omgang se hvordan dette kan løses ved å gå innenfra og utover (bottom up): (a) Innerst lager vi en trippel fra en to-elements liste: ; NB! pair er her rent formelt en liste med to elementer (define (make-pair-sum pair) ; parets første tall (list (car pair) (cadr pair) ; parets andre tall (+ (car pair) (cadr pair)))) ; summen av parets to tall (b) Så lager vi alle triplene for primtallsparene for fixert j og for i = 1 … j - 1: (define (make-prime-pair-segment i j) ; tell opp i fra 1 til j og cons hvert nytt par (cond ((= i j) '()) ; summen er et primtall, så ((prime? (+ i j)) ; cons denne triplen (cons (make-pair-sum (list i j)) (make-prime-pair-segment (+ i 1) j))) ; på de etterfølgende (else (make-prime-pair-segment (+ i 1) j)))) (c) Ytterst lager vi alle triplene for alle j fra 2 til n: ; tell ned j fra n til 2 og append hvert nytt segment (define (make-prime-pair-list j) (if (< j 2) '() (append (make-prime-pair-list (- j 1)) (make-prime-pair-segment 1 j)))) 183 ; Lag første del av listen, og ; skjøt så dette segmentet til listen Ovenstående gir et greit, men spesifikt, program som løser et spesifikt problemet. At løsningen er spesifikk er et resultata av at de ulike operasjonen er vevd sammen, bl.a. ved at vi enumererer, tester og lager trippellisten i en og samme operasjon i make-prime-pair-segment. Det vi er interessert i her, er å se om problemet kan løses i en sekvens av uavhengig standardoperasjoner på lister, her: enumerering, filtrering, mapping og akkumulering. (a) Trippel-konstruktoren blir den samme som i bottom-up-løsningen: (define (make-pair-sum pair) (list (car pair) (cadr pair) (+ (car pair) (cadr pair)))) (b) Dernest trenger vi et predikat for primtallspar: (define (prime-sum? pair) (prime? (+ (car pair) (cadr pair)))) 184 I SICP, både i den løpende teksten og i øvelsene, angis ulike algoritmer for primtallstesting, men ingen er enkle nok til å vises her. Generering av de ordnede parene (c) Lag listen med j'ene ved å enumerere fra 2 til n: (2 3 … n) (enumerate-interval 2 n) (d) Lag listen med i'ene ved å enumerere fra 1 til j – 1: (1 2 … j-1) (enumerate-interval 1 ( - j 1)) (e) Lag parene for én j ved å mappe fra (d) til liste med par (i j) i en kontekst der j er kjent: (map (lambda (i) (list i j)) (enumerate-interval 1 ( - j 1))) ((5 1) (5 2) …) (f) Lag parene for alle j'ene ved å mappe fra (c) til (e): (map (lambda (j) ; (e) (map (lambda (i) (list i j)) (enumerate-interval 1 ( - j 1)))) ; (d) ; (c) (enumerate-interval 2 n)) (((1 2)) ((1 3) (2 3)) ((1 4) (2 4) (3 4)) ...) 185 Vi fikk en liste med lister av par (((1 2)) ((1 3) (2 3)) ((1 4) (2 4) (3 4)) ...) men ønsker oss en flat liste av par ((1 2) (1 3) (2 3) (1 4) (2 4) (3 4) ...) (g) Akkumuler med append, for å skjøte sammen og løfte opp listene på nivå 2 til topnivålisten: (s. 197) (accumulate append '() ; (f) (map (lambda (j) (map (lambda (i) (list i j)) (enumerate-interval 1 ( - j 1)))) ; (e) ; (d) ; (c) (enumerate-interval 2 n))) ————————————————————————————————————— Denne listeutflatingen er såpass vanlig at vi lager en egen rutine flatmap for denne: (define (flatmap proc seq) (accumulate append '() (map proc seq))) 186 Cloue'et med flatmap er at append tar to lister og (slik + tar to tall og returnerer én returnerer ett,) at append faktisk tar én eller flere lister, er en annen sak slik at vi, når vi suksessivt legger neste liste til den akkumulerte lista, ender opp med én liste. (accumulate append '() '((a b c) (d e f) (g h i))) (append (a b c) (accumulate append '() '((d e f) (g h i)))) (append (a b c) (append (d e f) (accumulate append '() '((g h i))))) (append (a b c) (append (d e f) (append (g h i) (accumulate append '() '())))) (append (a b c) (append (d e f) (g h i))) (append (a b c) (d e f g h i)) (a b c d e f g h i) 187 (h) Med alt dette på plass får vi følgende løsning: (define (prime-sum-pairs n) ; (a) (map make-pair-sum ; (b) (filter prime-sum? ; (f-g) (flatmap (lambda (j) (map (lambda (i) (list i j)) ; (e) (enumerate-interval 1 ( - j 1)))) ; (d) (enumerate-interval 2 n))))) 188 ; (c) Forelesning 5 Grafsøk Presentasjon av oblig 2 INF2810 er ikke et kurs i grafteori, men søk i grafer er godt egnet for å illustrere en rekke sider ved bruk av lister. Det er kanskje ikke mest vanlig med funksjonelle implementasjoner av algoritmer for grafsøk, men det er både mulig og hensiktsmessig. 189 I det følgende ser vi på søkealgoritmer som - finner en vei, men ikke nødvendigvis den korteste - alltid finner korteste vei - enten med hensyn til antall noder langs veien, eller - med hensyn til summen av avstandene mellom nabonodene eller - med hensyn til summen av avstandene mellom nabonodene plus estimater av korteste avstand fra de enkelt nodene til mål. Vi går ut fra at - både startnoden, heretter kalt start, og målnoden, heretter kalt mål, for søket finnes, og at - det finnes i alle fall én vei fra start til mål. 190 Vi skal finne en sykkelrute fra Alba til Juba, og returnere til Alba med ruten, når vi har funnet den. 191 La n være den landsbyen vi er i for øyeblikket, og la N være listen med de uprøvde naboene til n. 1. Er N tom sykler vi tilbake til den landsbyen vi nettopp kom fra, med uforettet sak, og stryker n fra sykkelruten. Ellers, la m være neste uprøvde nabo av n. 2. Er m = Juba, legger vi m inn sist i sykkelruten og sykler tilbake med denne. 3. Har vi har vært i m før, prøver vi om igjen fra punkt 1, med resten av N. 4. Hverken 2. eller 3. slo til, hvilket vil si at m Juba, og vi har ikke vært i m før, så vi sykler til m, og legger m inn i sykkelruten. 5. Hverken 2. eller 3. slo til og 4. førte ikke frem, hvilket vil si at m Juba, vi har ikke vært i m før, og vi fant ingen vei via m, så vi prøver om igjen fra punkt 1. med resten av N. Fortsetter vi slik, og det finnes minst én vei fra Alba til Juba, kommer vi til syvende og sist frem. 192 Her er en litt mer formalisert beskrivelse av strategien, der P er veien frem til løpende node. 1. Hvis N er tom, returnér #f. 2. Hvis (car N) = mål, returnér (cons (car N) P). 3. Hvis (car N) P, Prøv om igjen fra 1. med N = (cdr N) og uendret P. 4. ; Kommer vi hit er N tom, (car N) mål, og (car N) P. Prøv om igjen fra 1. med N = naboene til (car N) og P = (cons (car N) P). ; Hvis dette lyktes, er vi fremme og returnerer herfra. 5. ; Kommer vi hit er N tom, (car N) mål og (car N) P, men vi fant ikke frem via (car N). Prøv om igjen fra 1. med N = (cdr N) og uendret P. 193 - I punktene 3. og 5., sjekker vi resten av nabolisten til løpende landsby n og forblir i n. - I punkt 4. kan det hende at første gjenværende uprøvde nabo av n er en landsby på veien til mål, og vi søker videre via denne. - Returverdien fra punkt 2. er hele sykkelruten fra start til mål, som vi tar med tilbake til foregående landsby. - Resultatet av søket i punkt 4. er enten #f, og vi faller gjennom til punkt 5., eller sykkelruten, som vi tar med tilbake til foregående landsby. 194 I en rekursiv implementasjon av algoritmen gjelder bl.a. følgende: - Når vi ikke finner noen vei videre fra den landsbyen vi står i, punkt 1. forsvinner denne landsbyen automatisk fra sykkelruten idet vi returnerer til det utenforliggende kallet. - Sykkeruten blir komplett idet vi når målet punkt 2. og returneres i en kjede fra kall til utenforliggende kall, punkt 4. til den til slutt returneres til det ytterste, initielle, kallet. - For å unngå å gå i ring, må vi hele tiden må vite hvor vi har vært, punkt 3. og dermed må sykkeruten konstrueres under søket. punkt 4. Siden sykkelruten konstrueres under søket, og de rekursive kallene ikke er argumenter til andre kall, får vi i prinsippet en iterativ prosess, men vi får allikevel ikke uten videre halerekursjon, ettersom vi får ulike rekursive kall under ulike betingelser. Hvis vi f.eks. implemeneterer dette ved hjelp av en cond-setning, er det først når vi evt. havner i else-grenen at vi er i en såkalt halekontekst. 195 Vi kan representer kartet som en graf med noder og forbindelseslinjer. Den visuelle utformingen av grafen har bare betydning. i den grad den gir oss informasjon som vi kan bruke i implementeringen av grafen Her får vi naborelasjoner, veilengder og kartkoordinater, men veikurvaturen er uten betydning. 196 Her får vi naborelasjoner og veilengder, men plasseringen av nodene er uten betydning. I programmet lar vi grafen være representert ved en liste der hvert byobjekt (node), inneholder byens navn, fulgt av navnene til byens nabobyer. ((a b c d e) (b a c) (c a b f) (d a e h) (e a d) (f c g i) Her får vi kun naborelasjonene, (g f h i j) men det er alt vi trenger i det to (h d g j) første søkealgoritmene vi skal se (i f g j) (j g h i)) på: dybde-først og bredde-først. Her er nodenavnene førstebokstavene i bynavnene. Dette er en forenklet versjon. I den fulle versjonen. inneholder hvert nodeobjekt byenes kartkoordinater plassert mellom nodenavnet og nabonavnene. 197 Den algoritmen vi brukte for kartlegging av sykkelruten kalles Depth-First-Search (DFS), fordi, så lenge vi ennå ikke har sett målet, og ikke har kommet tilbake til et sted vi alt har besøkt, går vi til den første naboen til løpende node, og derfra til den første naboen til den første naboen til løpende node, osv. vi beveger vi oss således primært nedover i nodelisten, i dybden, og vi går bare sideveis gjennom nabolisten, i bredden, når vi er tvunget til det. 198 DFS fører oss alltid til målet, men veien kan bli kronglete, og søket vil kunne inneholde tilbakesporinger, implisitt ved retur fra bomturer underveis. 199 Vi kan prøve å rekonstruere de tre søkeksemplene på kartet vha. pekefineren a d gir søket a-b-c-f-g-h-d c f gir søket c-a-b-BOM-d-e-BOM-h-g-f a j gir søket a-b-c-f-g-h-d-h-BOM-g-j 200 DFS fører som nevnt alltid til målet, om det lar seg nå, men ikke nødvendigvis langs den korteste veien. Når vi nå skal se på algoritmer for å finne korteste vei fra a til b, vil vi ha nytte av følgende: En assossiasjonsliste er en liste av par, der Her er fødselsår assossiert med navn. - car-elementene er oppslagsnøkler og (define født '((per 1980) (kari 1985) (pål 1990) (mari 1995) (espen 2000) (mette 2005))) - cdr-elementene er data. Vi sier at data i et par er assosiert med parets nøkkel. 201 For å slå opp i en assosiasjonsliste bruker vi semipredikatet assoc som tar - en nøkkel som første og - en assosiasjonliste som andre argument, og returnerer - elementet med den gitte nøkkelen, eller - #f, hvis nøkkelen ikke fantes i listen. (assoc 'mari født) ==> (mari 1995) (assoc 'morten født) ==> #f. Vi kan bruke assoc bl.a. for å få tak i nabolisten til en node, og dermed kommer den også til nytte i en implementasjon av DFS. 202 Ulike listetyper, operasjonelt definert En stack er en Last-In-First-Out-liste med de tilhørende operasjonene push top pop (bl.a.). En stack i Lisp er en liste med de tilhørende operasjonene (cons element L) (car L) (cdr L), eller m.a.o. en liste i Lisp er per definisjon en stack. En kø er en First-In-First-Out-liste med de tilhørende operasjonene push front pop En kø i Lisp er en liste med de tilhørende operasjonene (append L (list element)) (car L) (cdr L) En prioritetskø er en struktur (et tre eller en liste) der push–operasjonen består i å plasser et element etter prioritet, slik at følgende betingelse alltid er oppfylt: for alle par (x, y), hvis y ligger etter x i køen, så er prioriten til y prioriteten til x. 203 (bl.a.). Har vi en prioritetskø representert ved en liste, vil følgende rekursive prosess plasserer et element i køen: La Q være den til enhver tid gjenværende delen av køen, og la e være det elementene som skal inn. 1. Er Q tom, hvilket betyr at alle elementene i køen har høyere prioritet enn e, returnerer vi listen med det ene elementet e. 2. Har e høyere prioritet enn første element i Q, returnerer vi listen med e foran Q, og 3. ellers ; Q var ikke tom, og alle elementene så langt har hatt høyere prioritet enn e. leter vi videre i resten av Q. Lar vi prioritetskøen være representet ved et tre, kan vi redusere tiden for å finne rett plass for et nytt element i køen fra gjennomsnittlig n/2, som er kostnadene ved lineært søk, til i beste fall log2 n. Det ville imidlertid være uhensiktsmessig å implementere et slikt tre utelukkende vha. ikke-muterbare lister. — Se neste side. 204 Her—som alltid i en funksjonell løsning—returneres en ny liste med kopier av alle elementene fra den opprinnelige listen plus det nye elementet, på sin rette plass. I en imperativ løsning med verditilordning beholdes de opprinelig elementen, mens det nye elementet settes inn på rett plass, ved at noen pekere gis nye verdier. I Scheme er det mulig bl.a. å endre cdr-verdien til et element. Hvis f.eks. q er løpende par i Q, og p skal plasseres mellom q og (cdr q), kan vi sette cdr-delen i q til (cons p (cdr q)). Vi sier at en struktur som kan endres på denne måten er muterbar, og vi karakteriserer det å endre strukturen som en destruktiv operasjon, Ved hjelp destruktive operasjoner kan vi implementere søketrær minst like enkelt i Scheme som i andre språk. 205 Gitt en innsetningsalgoritme som den over, er det enkelt å se at prioritetskøbetingelsen alltid er tilfredstilt for Q. 1. Hvis prioritetsbetingelsen er tilfredstilt før innsetting av et nytt element, så er den også tilfredstilt etter innsettingen. 2. Hvis prioritetsbetingelsen er er tilfredstilt nå, så har alle elementene etter frontelementet i Q lavere enn eller samme prioritet som frontelementet, og alle elementene etter andre element i Q har lavere enn eller samme prioritet som dette, og dermed er prioritetsbetingelsen tilfredstilt også, etter at vi har poppet frontelement. 206 For å finne korteste vei, ser vi først på Breadth First Search, som garanterer veien med færrest antall noder. Vi har - en kø Q, der nodene legges inn slik at hvis a og b er i Q, og a ligger foran b, så er avstanden fra start til a avstanden fra start til b, - en stakk S, der vi har nøyaktig ett eksemplar av hver node som har blitt poppet fra Q, - en vei P (for path) som vi konstruerer på grunnlag av S, når S inneholder alle de nodene vi trenger, noe som vil være tilfelle når vi har kommet frem til målnoden. 207 Et objekt i Q og S er et par bestående av en node og nodens umiddelbare forgjenger på veien fra start til noden. Som nevnt, forutsetter vi at nodene start og mål finnes i grafen, og at det for hvert nodepar i grafen finnes det en vei fra den ene til den andre. Algoritmen har to faser traverseringen, der vi besøker hver node i tur og orden, og tilbakesporingen, der vi (re)konstruerer P ved hjelp av S. 208 Vi sier at grafen er connected. Under traverseringen flyttes bjektene i tur og orden fra fronten av Q til toppen av S. Samtidig ekspanderes det aktuelle objektet, idet de av de aktuelle naboene som vi ikke alt har sett, legges inn i køen i henhold til prioritestkriteriet. De nodene vi har sett vil, være de som er representert i S. På den måten får Q bare ett objekt for hver node, og det samme gjelder S. Men merk at en node kan være representert mer enn én gang som forgjenger. 209 (t.1) Vi starter traverseringen med å legge node start i Q med ingen (angitt ved #f) som forgjenger. Deretter gjør vi som følger, når (n, f) = (node, forgjenger) = det til enhver tid første elementet i Q: (t.2) Hvis n = mål (t.3) legger vi (n, f) på S og returnerer S. (t.4) Hvis det alt finnes et element i S med n som node, (t.5) går vi vider fra (t.2) med uendret S og resten av Q. og ellers (dvs. hvis n ≠ mål og (n, x) S for alle x) (t.6) legger vi (n, f) på S, og (t.7) ekspanderer n (legger de av naboene til n som ikke er i S, inn i Q), (t.8) før vi går vider fra (t.2) med ny S og med resten av utvidet Q. 210 Her vises Q og S for hver runde i traverseringen i et BFS-søk fra a til h i grafen over. S Q 1. S Q 2. S Q 3. S Q 4. S Q 5. S Q 6. S Q 7. S Q 8. S Q 9. S () ((a #f)) a ((a #f)) ((b a) (c b ((b a) (a ((c a) (d c ((c a) (b ((d a) (e d ((d a) (c ((e a) (c e ((e a) (d ((c b) (f c ((e a) (d ((f c) (e f ((f c) (e ((e d) (h e ((f c) (e ((h d) (g h ((h d) (f a) (d a) (e a)) (a . (b c d e)) #f)) a) (e a) (c b)) (b . (a c)) a er allerede i S. a) (a #f)) a) (c b) (f c)) (c . (a b f)) a og b er allerede i S. a) (b a) (a #f)) b) (f c) (e d) (h d)) (d . (a e h)) a er allerede i S. a) (c a) (b a) (a #f)) c) (e d) (h d)) (e . (a d)) a og d er allerede i S. a) (c a) (b a) (a #f)) d) (h d)) (c . (a b f)) c er i S og dermed må a, b og f alle være eller ha vært i Q. a) (d a) (c a) (b a) (a #f)) d) (g f) (i f)) (f . (c g i)) c er allerede i S. a) (d a) (c a) (b a) (a #f)) f) (i f)) (e . (a d)) e er i S og dermed må a og d begge være eller ha vært i Q. c) (e a) (d a) (c a) (b a) (a #f)) 211 Hvordan er stakken ordnet etter at målnoden er funnet? Etter traverseringen ligger målnoden øverst og startnoden nederst i S, og - for hvert element (x y) ≠ start i S, (y er forgjengeren til x) finnes det et element (y z) etter (x y) i S, og (z er forgjengeren til y) - det finnes ikke noe element (y w) i S, slik at w z, dvs. det finnes ingen andre elementer med y som etterfølger. Poenget er at én node bare kan være representert ved ett objekt i S. 212 (b.1) Vi starter tilbakesporingen med å hente første element fra S og legge det inn som første element i P. Deretter gjør vi som følger: (b.2) Hvis første gjenværende elementet i S ikke har noen forgjenger, så er vi ved start, og (b.3) vi legger dette elementet først i P og returnerer P. (b.4) Hvis noden i det første gjenværende elementet i S = forgjengeren i det første elementet i P, (b.5) legger vi det første elementet i S først i P og (b.6) går videre fra (b.2) med forlenget P og resten av S. Ellers (b.7) ((car S) ≠ start) og ((car S) er ingen forgjenger av (car S)). går vi videre fra (b.2) med uendret P og resten av S. Etter tilbakesporingen inneholder P node-objektene langs den korteste veien fra start til og mål, og (b.8) vi avslutter med å returnere listen med nodenavnene i P. Her vises rekonstruksjonen av P på grunnlag av S etter traverseringen. _______________ _______________ ↑ ↓ ↑ ↓ S = ((h d) (f c) (e a) (d a) (c a) (b a) (a #f)) P = ((a #f) (d a) (h d)) (map car P) ===> (a d h) 213 S vokser ved at for hvert suksessive frontelement i Q, hvis noden i elementet ikke er representert i S, legges elementet på toppen av S. Dermed har elementene i S synkende prioritet fra toppen og nedover,slik at øverste element ligger lengst fra start, og nederste element i S ligger nærmest start, så når vi så rekonstruerer veien P, idet vi går ned gjennom S og legger hvert nytt element som skal være med i P, på toppen av P, vil P til slutt inneholde nodene i veien etter stigende prioritet, slik at første element i S ligger nærmest start og siste element ligger lengst fra start, 214 Hvorfor er ikke P bare en reversering av S? Eller med andre ord, hvordan kan P bli kortere enn S? Spørsmålet bunner i at Q til enhver tid er ordnet etter synkende prioritet, og at det dermed ikke kan komme noe element fra Q til S som er nærmer start enn noe element som alt er på S. Poenget er at det kan finnes mer enn ett element på S i samme avstand fra start. 215 Vi ser så på Uniform Cost Search, som garanterer den korteste veien i antall lengdenheter. Hele veilengden er summen av avstandene mellom nabonodene langs veien. For dette trenger vi, i tillegg til tabellen med noder og nabolister, - en tabell med veilengder assosiert med nabopar — f.eks. slik (define nodes '((a b c d e) (b a c) (c a b f) (d a e h) (e a d) (f c g i) (g f h i j) (h d g j) (i f g j) (j g h i)) (define '(((a . ((a . ((a . ((a . ((b . ((c . ((d . ((d . ((f . ((f . ((g . ((g . ((g . ((h . ((i . distances b) . 1) c) . 3) d) . 3) e) . 1) c) . 1) f) . 1) e) . 1) h) . 4) g) . 1) i) . 3) h) . 1) i) . 2) j) . 3) j) . 1) j) . 1))) Merk at hvert nabopar bare er representert med én nøkkel, slik at hvis vi vil ha tak i avstanden mellom x og y, kan det hende vi må slå opp - først med (cons x y) og - deretter med (cons y x) som nøkkel. (or (assoc (cons x y) distances) (assoc (cons y x) distances)) Hvis f.eks. x = e og y = d, vil vi få (assoc (cons x y)) ==> #f, (assoc (cons y x)) ==> ((d . e) . 1), og dermed returnerer or-setning ((d . e) . 1). 216 Algoritmen for UCS er den samme som for BFS, bortsett fra det som gjelder ekspanderingen av løpende node. Generelt består ekspanderingen av en node n i at vi, for hver nabo x av n, som ikke er representert i S, legger x med n som forgjenger inn på rett plass i Q, i henhold til prioriteten til n og prioritetene til elementene i Q. 217 Hva angår BFS kan vi resonnere slik. La SN være mengden av start sine naboer. la SNN være mengden av de naboene til SN som ikke selv er i SN (som ikke selv er naboer til start), la SNNN være mengden av de naboene til SNN som ikke er i SN SNN (hverken i SNN eller i SN), osv. Da ligger alle nodene i SN lengre fra start enn det start selv gjør (rimeligvis), alle nodene i SNN ligger lengre fra start enn det alle nodene i SN gjør, og alle nodene i SNNN ligger lengre fra start enn det alle nodene i SNN gjør, osv. Så når noden n er kommet til fronten av Q, ligger alle de naboene til n som ikke alt er representert i S, lengre fra start enn det alle nodene i Q gjør, og ekspanderingen av n består ganske enkelt i å skjøte n sin naboliste, minus de naboene som alt er representert i S, til Q. 218 Hva angår UCS må vi resonnere slik. I UCS, som i BFS, er køen ordnet etter avstanden fra start, men i USC ser vi på veilengdene, og ikke på antall noder fra start, og siden en lang vei kan inneholde færre noder enn en kortere vei, el. omv. en kort vei kan inneholde flere noder enn en lengre vei, kan vi ikke ganske enkelt legge hvert nytt objekt sist i Q. I stedet må vi lete oss frem til plassen for hvert nytt objekt vi legger inn i Q. 219 Men hvordan kan vi være sikre på, når vi legger et objekt på stakken, at det ikke kommer et "bedre" objekt inn i køen på et senere tidspunkt? La (x, y) være objektet på toppen av S. Kan det da finnes en z som har x som nabo og ligger nærmere start enn det y gjør, og kan i så fall objektet (x, z) finne sin vei inn i Q etter at (x, y) ble tatt ut? Siden (x, y) er i S, må y allerede ha kommet seg gjennom Q, men skulle z ha ligget nærmere start enn det y gjør, måtte alle nodene langs veien fra start til z også ha ligget nærmere start, og dermed ville y alltid ha blitt liggende bak disse nodene etterhvert som de ble lagt inn i Q, og den første av disse ville ha vært lagt inn i Q alt i første runde. Altså kan det ikke finnes noen slik z. 220 For å holde styr på veilengdene, lar vi nå nodeobjektene i Q og S inneholde noden og forgjengeren, som før og i tillegg avstanden fra start til noden via forgjengeren, (n, f, a). Et eksempel: Følgende er én vei (av flere) fra a til g: (a e d h g). Tabellen gir oss avstanden a e = 1, avstanden e d = 1, avstanden d h = 4, og avstanden h g = 1. Dette gir følgende objekter (e a 1), (d e 2), (h d 6) og (g h 7). 221 La a, b og c være etterfølgende noder på veien fra start, slik at ingen av a, b og c er representert i S, la p være avstanden fra start til b via a, p = avstanden fra start til a + avstanden fra a til b. la q være avstanden fra b til c (som vi finner ved å slå opp i avstandstabellen), og la nodeobjektet (b, a, p) ligge først i Q. Da skal objektet (c, b, p + q) plasseres i Q. For å få dette inn på rett plass, kan vi bruke prioritetskøinnsettingsalgoritmen over. Et objekt x har høyere prioritet enn et annet objekt y hvis avstandsverdien i x < avstandsverdien i y. prioritet(x, t, u) > prioritet (y, v, w) u < w. I dette tilfellet skal vårt nye objekt (c, b, p + q) inn foran det første element Q som har en avstandsverdi > p + q. 222 første (x, y, z) i Q, slik at z > p + q. Her vises Q og S for hver runde i traverseringen i et UCS-søk fra a til h i grafen over. S Q 1. S Q 2. S Q 3. S Q 4. S Q 5. S Q 6. S Q 7. S Q 8. S Q 9. S Q 10. S () ((a #f 0)) a ((a #f 0)) ((b a 1) (e b ((b a 1) (a ((e a 1) (c e ((e a 1) (b ((c b 2) (d c ((c b 2) (e ((d e 2) (c d ((d e 2) (c ((c a 3) (d c ((d e 2) (c ((d a 3) (f d ((e d 2) (c ((f c 3) (h f ((f c 3) (e ((g f 4) (h g ((g f 4) (f ((h g 5) (h h ((h g 5) (g a 1) (c a 3) (d a 3)) (a . (b c d e)) #f 0)) b 2) (c a 3) (d a 3)) (b . (a c)) a 1) (a #f 0)) e 2) (c a 3) (d a 3)) (e . (a d)) a 1) (b a 1) (a #f 0)) a 3) (d a 3) (f c 3)) (c . (a b f)) b 2) (e a 1) (b a 1) (a #f 0)) a 3) (f c 3) (h d 6)) (d . (a e h)) b 2) (e a 1) (b a 1) (a #f 0)) c 3) (h d 6)) (c . (a b f)) b 2) (e a 1) (b a 1) (a #f 0)) d 6)) (d . (a e h)) d 2) (c b 2) (e a 3) (b a 1) (a #f 0)) d 6) (i f 6)) c 3) (e d 2) (c b 2) (e a 3) (b a 1) (a #f 0)) d 6) (i f 6) (i g 6) (j g 7)) (f . (c g i)) (g . (f h i j)) f 4) (f c 3) (e d 2) (c b 2) (e a 3) (b a 1) (a #f 0)) 223 Til slutt ser vi på algoritmen A*. Denne er identisk med UCS, bortsett fra i utregningen av prioritetskriteriet. A* kjenner til mer enn de rene veilengdene. I vårt tilfelle lar vi dette være koordinatene til nodene i kartet. A* tar i betraktning veilengdene på samme måte som UCS gjør, men tar i tillegg i betraktning estimater av korteste avstand fra løpende node til mål. 224 Ved hjelp av Pytagoras gjøres dette slik. Hvis u = (a, b) og v = (c, d) er punkter på kartet, så er den horisontale avstanden fra u til v = a – c, og den vertikale avstanden fra u til v = b – d. De tilsvarende avstandslinjene sammen med luftlinjen fra u til v danner en rettvinklet trekant, og dermed er luftlinjeavstanden fra u til v = (b – d)2 + (b – d)2. Merk at en eller begge av a – c og b – d kan være negative, men siden disse kvadreres i utregningen av luftlinjeavstanden, spiller ikke det noen rolle. 225 Vi lar koordinatene være en del av nodeobjektene i grafrepresentasjonen. F.eks. representerer (a (55.4 . 71.9) b c d e) noden med navnet a, koordinatene (55.4 . 71.9) og naboene b, c, d og e. ((a (b (c (d (e (f (g (h (i (j (55.4 (52.6 (51.0 (58.4 (58.1 (53.3 (56.0 (56.2 (51.9 (54.1 . . . . . . . . . . 71.9) 71.7) 69.9) 69.9) 71.9) 69.0) 68.2) 65.6) 65.1) 64.6) b a a a a c f d f g c d e) c) b f) e h) d) g i) h i j) g j) g j) h i)) ((a ((a ((a ((a ((b ((c ((d ((d ((f ((f ((g ((g ((g ((h ((i . . . . . . . . . . . . . . . b) c) d) e) c) f) e) h) g) i) h) i) j) j) j) 226 . . . . . . . . . . . . . . . 3.0) 6.4) 7.9) 3.0) 3.2) 3.1) 2.9) 11.6) 2.9) 8.5) 3.2) 5.8) 8.1) 3.1) 3.0))) Her vises Q og S for hver runde i traverseringen i et A*-søk fra a til h i grafen over. S Q 1 S Q 2 S Q 3 S Q 4 S Q 5 S Q 6 S Q 7 S Q 8 S Q 9 S Q 10 S () ((a F a ((a F ((e a e ((e a ((b a b ((b a ((d e d ((d e ((d a d ((d e ((c b c ((c b ((c a c ((c b ((f c f ((f c ((g f g ((g f ((h g h ((h g 0)) 0)) 3.0) (b a 3.0) (d a 7.9) (c a 6.4)) (a . (b c d e)) 3.0) (a F 3.0) (d e 0)) 5.9) (d a 7.9) (c a 6.4)) (e . (a d)) 3.0) (e a 5.9) (d a 3.0) (a F 7.9) (c b 0)) 6.2) (c a 6.4)) (b . (a c)) 5.9) (b a 7.9) (c b 3.0) (e a 6.2) (c a 3.0) (a F 0)) 6.4) (h d 17.5)) (d . (a e h)) 5.9) (b a 6.2) (c a 3.0) (e a 3.0) (a F 6.4) (h d 17.5)) 0)) 6.2) (d e 6.4) (f c 5.9) (b a 3.0) (e a 9.3) (h d 17.5)) 3.0) (a F 6.2) (d e 5.9) (b a 9.3) (h d 17.5)) 3.0) (e a (d . (a e h)) 0)) 3.0) (a F (c . (a b f)) 0)) (c . (a b f)) 9.3) (c b 6.2) (d e 5.9) (b a 12.2) (h d 17.5) (i f 17.8)) 3.0) (e a 3.0) (a F 0)) (f . (c g i)) 12.2) (f c 9.3) (c b 6.2) (d e 5.9) (b a 3.0) (e a 3.0) (a F 0)) 15.4) (h d 17.5) (i f 17.8) (i g 18.0) (j g 20.3)) (g . (f h i j)) 15.4) (g f 12.2) (f c 9.3) (c b 6.2) (d e 227 5.9) (b a 3.0) (e a 3.0) (a F 0)) Det kan være ønskelig å få tilbake både veien og veilengden fra UCS og A*. Nå ligger jo avstanden fra start til mål allerede i toppelementet i S, så det vi gjør, er ganske enkelt å returnere dette sammen med P fra tilbakesporingen. ——————————————————————————————————————————— OBLI 2 Oblig 2 går ut på å implementere BFS, UCS og A*. Selv om avstandstabellen ikke brukes i BFS, og koordinatene ikke brukes i UCS, skal allikevel samme grafrepresentasjon brukes i alle implementasjonene. Dette får konsekvenser for seleksjonen, slik at f.eks. noden, koordinatene og nabolisten i et nodeobjekt er gitt ved hhv. car, cadr og cddr. 228 Oppsummering I DFS går vi hele tiden videre, uten å tenke på hvor langt vi har gått, til den første naboen som vi ikke alt har besøkt, inntil vi er ved målet, eller ikke kommer lengre. I siste fall trekker vi oss tilbake til siste kryss med minst én nabo vi ennå ikke har besøkt, og fortsetter søket derfra. BFS innebærer en slags implisitt telling av nodene langs veien. Dette gir rett veilengde, hvis alle veistykkene har lengde 1. Da er avstanden fra start til hver nabo x av start = 1, avstanden fra start til hver nabo av x som ikke er nabo av start = 2, osv, og dermed gir BFS en ordning av nodene i Q etter økende avstand fra start, ganske enkelt ved at hvert nytt element legges sist i Q, og vi behøver ikke å regne ut prioriteten. 229 La p(x, y) være prioriteten til objektet der x er noden og y er forgjengeren til x, la d(x, y) være avstanden mellom nabobyene x og y, la v(x, y) være avstanden fra start til noden x via forgjengeren y, og lar e(x) være den estimerte minsteavstanden fra x til mål. I UCS ser vi kun bakover, idet vi sammenligner lengdene på ulike veier frem til der vi står, via ulike forgjengere, og får dermed p(x, y) = v(x, y). I A* ser vi også fremover, idet vi gjetter hvilken vei det vil lønne seg å ta, ut fra de informasjoner vi har i tillegg til den retrospektive veilengdeinformasjonen. p(x, y) = v(x, y) + e(x). Vi beregner e(x) på stedet for hver x. I vårt tilfelle lar vi minsteavstnaden være luftlinjelengden fra x til mål, som vi beregner ved hjelp av koordinatene til x og målnoden. 230 Øya Graaf og dens naboøy Soub Her er et større kart for dem som måtte ha moro av det. Fila Graaf.gif er en mer livlig utgave av kartet. Både denne og Scheme-representasjonen Graaf.ss ligger i mappen Graaf. GRAAF SOUB 231 232 Ymse tillegsstoff for spesielt interesserte I programmeringssammenheng brukes grafer typisk til å representere nettverk mellom datamaskiner og andre enheter som routere , hub’er, etc. En graf kan også brukes til å representere et system av veier og knutepunkter, Eks: Trafikanten. En slik graf kan representeres ved et ordnet par (V, E), der V er en mengde hjørner (vertices), og E er en mengde kanter (edges). En kant har et hjørne i hver ende. (En annen betydning av graf er en kurve i et koordinatsystem—som uttrykk for forholdet mellom argumentene og funksjonsverdiene til en matematisk funksjon, men det er ingen begrepsmessig forbindelse mellom kurvegrafer og grafer med hjørner og kanter.) 233 Termene hjørner og kanter kobler grafer til betraktninger av mangekantede legemer. Et mangekanete legeme kan legges inn i planet (be embedded in the plane), der vi betrakter det topologiske—hvilke punkter som er forbundet med hvilke, mens det geometriske—vinkler, kurvatur, etc er uten betydning. 234 topos er gresk for sted, En mangekant har også mange sider. Faktisk er antall sider = antall kanter –antall hjørner + 2. I den grafiske representasjon er undersiden, den delen av planet som ligger utenfor grafens ytterkanter. Dette er lettere å se om vi representerer mangekanten på en kule i stedet for i planet. Kulen (eller sfæren—the sphere), og planet er toplogisk identiske. Hvis vi tenker oss et mangekantet legeme som, under påvirkning av et kjemikalium ble elastisk, slik at det kunne blåses opp til en ball, så ville hjørnene og kantene fremtre på ballen som en graf. 235 En graf kan brukes til å analysere et kart mht. områder som grenser til hverandre. Hvis vi sier at en grense mellom to områder må omfatte mer en ett enkelt punkt1, kan områdene representeres som hjørner og grensene som kanter. En rekke grafteoretisk problemer er knyttet til farging av kart. Det mest kjente er fire-farge-teoremet, som sier at, for ethvert kart i planet, trenger vi ikke mer enn 4 farger, for å fargelegge kartet slik at intet par av tilgrensende områder har samme farge. 1 Slik at f.eks. Xanthos ikke kan sies å grense til noen av Kyanous Estelos og Erythros Boreios. 236 Forelesning 6 SICP 2.3 Symbolske data Vi har så langt alt vesentlig sett på tall— enkeltstående tall, talluttrykk, og lister og trær med tall. Et av målene for John McCarthy (opphavsmannen til Lisp) var å lage et språk for manipulering av symboler— slik man f.eks. gjør i matematikken (algebra) ved behandling av ligninger med én eller flere ukjente. Men har vi et språk for symbolmanipulering, trenger vi selvsagt ikke begrense oss til matematikken. Vi kan også regne på begrepsmessige relasjoner mellom mer eller mindre kompliserte objekter personer, naturlige språk, etc.. Ett program kan være data til et annet (f.eks. er kildekoden data til kompilatoren). Med symboler kan vi anskueliggjøre dette innenfor ett og samme program. 237 For å kunne operere på symboler, trenger vi en symbolsyntaks. En ting er de navngitte objekter vi har i og med programmets prosedyrer og variabler, men for skikkelig symbolbehandling ønsker vi også variabel-variabler, dvs. Scheme-variabler med variabelsymboler som verdier Så å si alle høynivåspråk, herunder Scheme, har typer for enkelttegn og tegnstrenger, men disse er ikke særlig godt egnet for å etablere det metabegrepet vi ønsker oss. Scheme har derfor, i tillegg til Selv om Scheme er løst (dynamisk) typet er type- talltypene, typene tegn og boolean kontrollen strikt der det er relevant. F.eks. kan og de sammensatte typene sammenligningsprosedyrene <, <=, =, >=, og > par, liste, vektor og streng, bare brukes på tall. Men, som vi skal se, finnes det ekvivalensprosedyrer som tillater argumenter av en egen type symbol. ulike typer, selv om en sammenligning på tvers av typer alltid vil gi resultatet false. 238 (symbol? 'foo) => #t (symbol? (car '(a b))) => #t (symbol? "bar") => #f ; her har vi en tegnstreng (symbol? #t) => #f ; her har vi en boolesk verdi——ikke et symbol (symbol? '()) => #f ; den tomme listen er ikke et symbol (symbol? nil) => #f ; nil er en Scheme-variabel, bundet til den tomme listen. (symbol? 'nil) => #t ; men dette er symbolet——ikke den tomme listen => #t ; nil er nå en Scheme-variabel bundet til symbolet nil. (define nil '()) (define nil 'nil) (symbol? nil) Symboler kan opptre helt og holdent på linje med tall, strenger og objekter av andre typer, men de kan også opptre som—eller representere—variabler. Dvs. på det syntaktiske nivået er de stadig objekter, men på det symbolbehandlende metanivået er de variabler. 239 Symboler er kjennetegnet ved at to symboler er identiske dersom de staves likt. Et symbol opptrer i sitert — quote'et — form. quote er en spesialform som bl.a. brukes for å innføre symboler. 'sym, og dermed (quote sym), evaluerer til sym. 'sym er syntaktisk sukker for REPL skriver ut den forenklede varianten, slik at f.eks.(quote (quote sym)) skrives 'sym. (quote sym) Bruken av én enkelt prefisket apostrof er entydig, fordi Noen få tegn kan ikke quotes direkte: etterfølgende whitespace eller sluttparentes skiller det aktuelle symbolet fra etterfølgende språklige elementer. (list 1 2 3)) '(a b c)) (list 'a 'b 'c)) . | ( , ) ' [ " ] ` { \ } hash, punktum, komma, enkel og dobbel apostrof, backquote, backslash, pipe og parentesene (og muligens noen til), men de kan quotes sammen med escapetegnet \. I Racket skrives slike symboler ut omgitt av pipes. Eks '\# skrives |#|. quote kan også brukes for lister. '(1 2 3)) # Alle slags Scheme-uttrykk—prosedyrekall, bruk av spesialformer, definisjoner, etc.—kan quotes, og alle quotede uttrykk kan evalueres vha. prosedyren eval, slik at for eksempel (eval '(+ 2 3)) og ((eval '+) 2 3)begge evaluerer til 5, men hvis resultatet av eval ikke gir mening, går det galt, f.eks. slikt at (eval 'a) ville gi kjøreavbrudd, dersom ikke a alt var definert. 240 Sammenligningsprosedyrer for ulike typer objekter tall - tall, strenger - strenger, symboler - symboler, etc. I forbindelsen med tallbehandling har vi brukt sammenligningsprosedyrene for likhet og størrelsesrelasjoner: =, <, >, <= og >=. I Scheme virker disse, som nevnt, bare for tall, og vi har I mange språk er disse overlesset slik at de også virker for strenger og C++ tillater ytterligere skreddersydd (custom) overlessing for egendefinerte typer. andre sammenligningsoperatorer for objekter av andre typer; og vi kan også innlemme egne sammenligningsoperator i abstraksjonsabrrieren for en egendefinert type. Det finnes imidlertid også generelle ekvivalenspredikater, bl.a eq? gjelder objekters identitet (referanse / adresse / lokasjon (plass i memory)) equal? gjelder numerisk, boolesk, tegnmessige eller symbolmessige likhet, eller sammensatte objekter, mht. deres innhold, element for element. 241 Det finnes også et ekvivalenspredikatet eqv? som gjelder gjelder objekters identitet og eventuell numeriske, booleske, tegnmessige eller symbolmessige likhet. Vi klarer oss imidlertid lenge med eq? og equal? To distinkte objekter kan ha samme innhold. (define symbolpar-1 '(sym bol)) (define symbolpar-2 '(sym bol)) (eq? symbolpar-1 symbolpar-2) #f (samme innhold — ulike objekter) (equal? symbolpar-1 symbolpar-2) #t (samme innhold) (define symbol-1 'sym) (define symbol-2 'sym) (eq? symbol-1 symbol-2) #t (symboler som staves likt, er identiske) (equal? symbol-1 symbol-2) #t " (eq? symbol-1 (car symbolpar-2)) #t " (= symbol-1 symbol-2) feilmelding: = expects type <number> ... (eq? 2 2) #t (fra Racket — ellers, i hht R5RS, uspesifisert) (equal? 2 2) Idéelt skal det, for av såvel tall som symboler, være slik at hvis ulike forekomster har samme verdi så er de også samme objekt(cfr. Platon), og slik er det også i Racket. #t 242 SICP 2.3. Symbolsk differensiering (derivasjon) Differensiering i matematikken går ut på å finne funksjoners endringsrater eller deriverte (avledede). F.eks. har funksjonen f(x) = 3x endringsraten 3, dvs. for hver endring av x endres funskjonsverdien med 3. For f(x) = 3x er dette en grei beskrivelse ettersom f er linær. For ikke-lineære funksjoner som f.eks. x2, trenger vi en mer raffinert beskrivelse. Vi sier at den deriverte av en funksjon f(x) er grenseverdien for uttrykket f(x + h) – f(x) h (i) når h (på en eller annen måte) går mot 0. Merk at f(x + 0) – f(x) , altså 0 , 0 0 Eller sagt på en annen måte (ii) f ' (x) = lim f(x + h) – f(x) h h0 Eller på nok en måte, når vi sier at det til en endring x av x svarer en endring y av y. (iii) dy y f ' (x) = — = lim dx x0 x NB! Uttrykket dx/dy angir ikke en brøk, men kun et symbol for den deriverte — som altså er en grenseverdi. Her er det viktig å holde ting fra hverandre. 243 ikke gir mening Bl.a. følgende regler gjelder for derivasjon: (1) c' = 0 den deriverte av en konstant = 0 (2) x' = 1 den deriverte av identitetsfunksjonen = 1 (3) (f(x) + g(x))' = f '(x) + g'(x) den deriverte av summen av to funksjoner = summen av de deriverte av de to funksjonene (4) (f(x) g(x))' = f '(x)g(x) + g'(x)f(x) den deriverte av produktet av to funksjoner = summen av den deriverte av den første ganger den andre og den deriverte av den andre ganger den første (5) f(x) ( —— )' g(x) (f '(x)g(x) – g'(x)f(x)) = (g(x))2 den deriverte av brøken av to funksjoner = differansen mellom den deriverte av telleren ganger nevneren og den deriverte av nevneren ganger telleren, delt på kvadratet av nevneren (6) (f(g(x)))' = f '(g(x))g'(x) den deriverte av en sammensatt funksjon = den deriverte av den ytterte mht. den innerste ganger den deriverte av den innerste Ved gjentatt anvendelse av (4) kan vi avlede (7) (xn)' = nxn-1 3 Her har jeg brukt f ' for den dervierte av f. For å unngå forvekslingen med formen quote, bruker jeg heretter formen dy/dx — den deriverte av y mht. x. 2 f.eks. den deriverte av x = 3x fordi (xxx)' = ((xx)x)' = (xx)'x + (xx)x' = (x'x + xx')x + xx1 = (1x + x1)x + xx1 = xx + xx + xx = 3xx. 244 Differensiering i Scheme Vi tar for oss et utvalg av disse reglene, når vi nå går løs på symbolsk differensiering i Scheme, i første omgang (1) – (4) som i SICP er notert slik2: dc (1) = 0 dx (2) dx dx = 1 (3) d(u + v) —— dx = du dv — + — dx dx (4) d(uv) —— dx = dv du u— + v — dx dx NB! Selv om dette ser ut som (og forsåvidt er) matematikk, er det for oss primært symbolmanipulering etter bestemt regler. Finner vi et uttrykk med formen til venstresiden i for eksempel (3), skal vi lage et uttrykk som det som står på høyresiden i (3). 2 Det finnes flere notasjoner for den deriverte—med ulike opphavsmenn (ingen kvinner)—først og fremst Euler, Newton, Leibniz og Lagrange. 245 Her er noen eksempler på det vi ønsker å få til, når vi deriverer med hensyn på x: d(x + 3) ——— dx 1, d(xy) —— y, dx d(3x) —— 3, dx d((xy)(3x)) ———— 6xy dx Vi velger imidlertid i første omgang prefiks- fremfor infiks-notasjon, siden prefiks er lettere å arbeide med i Scheme. Og i tillegg til det uttrykket som skal deriveres, lar vi symbolet for den variabelen det skal deriveres med hensyn på, være argument til deriveringsprosedyren Vi vil altså ha en funksjon som virker slik: (deriv '(+ x 3) 'x) 1 (deriv '(* x 3) 'x) 3 (deriv '(* x y) 'x) y (deriv '(* (* x y) (* x 3)) 'x) ; den deriverte av (+ x 3) mht. x = 1 3x xy xy ——— + 3x ——— = 6xy dx dx Det skal imidlertid vise seg at vi må arbeide en del, før vi kan få til resultater som disse. 246 Første utgaven av deriveringsprosedyren ser slik ut. ; derivér uttrykket exp med hensyn til variabelen var. (define (deriv exp var) ; regel (1): konstant (cond ((number? exp) 0) ; regel (1) el. (2): konstant eller hensynsvariabel ((variable? exp) (if (same-variable? exp var) 1 ; regel (2): hensynsvariabel 0)) ; regel (1): konstant ; regel (3): sum ((sum? exp) (make-sum (deriv (addend exp) var) (deriv (augend exp) var))) ; rekursér for å derivere addenden ; rekursér for å derivere augenden ; regel (4): produkt ((product? exp) (make-sum (make-product (multiplier exp) (deriv (multiplicand exp) var)); rekursér for å deriv. multiplikand (make-product (deriv (multiplier exp) var) (multiplicand exp)))) (else (error "unknown expression type – DERIV" exp)))) 247 ; rekursér for å deriv. multiplikator Over har vi brukt predikater og selektorer vi ennå ikke har definert. Hvordan disse skal defineres, beror på hvordan vi velger å representere de (algebraiske) uttrykkene som skal deriveres. Ved hjelp av lister, og med prefiks-notasjon, får vi følgende implementasjon: Tall er tall (define (number? x) (number? x)) ; number? er en Scheme-primitive Variabler er symboler ; symbol? er en Scheme-primitive (define (variable? x) (symbol? x)) Vi trenger en sammenligningsprosedyre for å kunne kjenne igjen den variabelen vi deriverer med hensyn på, (define (same-variable? v1 v2) (and (variable? v1) ; Her hadde det holdt med ett kall på variable? fordi (variable? v2) ; hvis den ene av v1 og v2 er en variabel (eq? v1 v2))) ; vil ikke eq? returnere #t med mindre også den andre er en variabel. 248 Summer og produkter er lister med Scheme's prefixform der første element er en operand-tag (define (make-sum a1 a2) (list '+ a1 a2)) (define (sum? exp) ; Et uttrykk er en sum (and (pair? exp) ; hvis det er et par, og ; dets første element er symbolet +, (eq? (car exp) '+))) (define (addend sum) (cadr sum)) ; Andre element er det som får noe lagt til seg. (define (augend sum) (caddr sum)) ; Tredje element er det som legges til. (define (make-product a1 a2) (list '* a1 a2)) ; Et uttrykk er et produkt (define (product? exp) ; hvis det er et par, og (and (pair? exp) ; dets første element er symbolet *. (eq? (car exp) '*))) (define (multiplier sum) (cadr sum)) ; Andre element er det antall ganger noe skal ganges. (define (multiplicand sum) (caddr sum)) ; Tredje element er det som skal ganges. 249 Med denne implementasjonen får vi følgende output med de inndata som er vist over. (deriv '(+ x 3) 'x) (+ 1 0) ; regel (3, 2, 1) (deriv '(* x 3) 'x) (+ (* x 0) (* 1 3)) ; regel (4, 2, 1) (deriv '(* x y) 'x) (+ (* x 0) (* 1 y)) ; regel (4, 2, 1) (deriv '(* (* x y) (* x 3)) 'x) (+ (* (* x y) ; regel (4) (+ (* x 0) (* 1 3))) (* (+ (* x 0) (* 1 y)) (* x 3))) Til sammenligning viser vi om igjen det output vi kunne ønske oss. (deriv '(+ x 3) 'x) 1 (deriv '(* x 3) 'x) 3 (deriv '(* x y) 'x) y (deriv '(* (* x y) (* x 3)) 'x) 6xy 250 ; regel (3, 2, 1) ; regel (3, 2, 1) Vi kan komme et stykke på vei ved å modifisere konstruktorene, og dette kan vi gjøre uten å endre derivasjonsprosedyren. (define (make-sum a1 a2) (cond ((=number? a1 0) a2) ; addend = 0, så summen = augend ((=number? a2 0) a1) ; augend = 0, så summen = addend ((and (number? a1) (number? a2)) ; begge er tall, så vi returnmerer ; den numeriske summen (+ a1 a2)) (else (list '+ a1 a2)))) (define (make-product m1 m2) (cond ((or (=number? m1 0) (=number? m2 0) 0)) ; minst én faktor = 0, så produktet blir også 0 ((=number? m1 1) m2) ; multiplikand = 1, så produktet = multiplikator ((=number? m2 1) m1) ; multiplikator = 1, så produktet = multiplikand ((and (number? m1) (number? m2)) ; begge er tall, så vi returnmerer ; det numeriske produktet (* m1 m2)) (else (list '* m1 m2)))) 251 Sammenligningsfunksjonen =number? x n tar en variabel eller et tall som første argument og et tall (forutsetningsvis) som andre argument, og returnerer #t hvis første argument er samme tall som andre. (define (=number? x n) (and (number? x) (= x n))) Dette gir følgende forbedrede output: (deriv '(+ x 3) 'x) 1 (deriv '(* x 3) 'x) 3 (deriv '(* x y) 'x) y (deriv '(* (* x y) (* x 3)) 'x) (+ (* (* x y) 3) (* y (* x 3))) — klart bedre enn resultatet av den opprinnelige implementasjonen, (deriv '(* (* x y) (* x 3)) 'x) (+ (* (* x y) (+ (* x 0) (* 1 3))) (* (+ (* x 0) (* 1 y)) (* x 3))) men vi har fremdeles har et stykke igjen. Dette er et tema for ukeoppgavene. 252 SICP 2.3.3 Representasjon av mengder Schemes liste-begrep gir en mulig representasjon av mengder, forutsatt visse modifikasjoner og presiseringner. Bl.a. må vi ta vare på at - kardinaliteten til en mengde er gitt ved antall distinkte elementer i mengden, mens - lengden til en liste er gitt ved antall elementer overhodet. En bag eller et multiset er en elementsamling med multiplisitet—flere forekomster av samme element / verdi. Mengde (1, 2, 3) = (2, 1, 3) = (1, 1, 2, 3) Multiset (1, 2, 3) = (2, 1, 3) (1, 1, 2, 3) Liste (1, 2, 3) (2, 1, 3) (1, 1, 2, 3) (se under om ordnede mengder) Dette gjør vi (som for rasjonelle tall og algebraiske uttrykk) ved å definere datatypen mengde (set) ved noen grunnoperasjoner i første omgang snitt og union (det siste som øvelse) samt en konstruktor for å legge et element til en mengde, og et predikat for å avgjøre om noe er et medlem i en mengde. 253 For å ta det siste først: Scheme har bl.a. semipredikatene memq og member for å sjekke om noe er et element i en liste. I implementasjonen av disse brukes henholdsvis eq? og equal?. SICP bruker fortrinnsvis og vi bruker utelukkende eq? og equal?. Som nevnt over er forskjellen mellom disse generelt den at (eq? x y) returnerer #t hvis x og y er identiske—altså samme objekt (lokasjon) mens (equal? x y) returnerer #t hvis x og y er like, element for element. (define x '(a b c)) ; y er nå en kopi av, men ikke identisk med, x. (define y (map (lambda (e) e) x)) (eq? x y) #f (equal? x y) #t Hva symboler angår, er hele poenget at (eq? sym1 sym2) (equal? sym1 sym2). Som sagt: to symboler som staves likt er identiske (også rent fysisk i maskinen). Gitt to symboler a og b, så gjelder at: (string=? (symbolstring a) (symbolstring b)) (eq? a b). Se R5RS 6.3.3 og 6.5.5. 254 Primitiven member tar et objekt og en liste som argumenter, søker rekursivt gjennom den gitte listen etter det gitte objektet vha. equal og returnerer ved eventuelt funn den delen av listen som begynner med det funne objektet, eller #f, hvis objektet ikke ble funnet. Her er vi imidlertid interessert i ekvivalenspredikater snarere enn semipredikater, og definerer ett mht. identitet og ett mht. likhet: (define (memq? elm set) (cond ((null? set) #f) ((eq? elm (car set)) #t) (else (memq? elm (cdr set))))) (define (member? elm set) (cond ((null? set) #f) (memq? 'b '(a b c)) #t (memq? 'd '(a b c)) #f (memq? '(b c) '(a (b c) d)) #f (member? 'b '(a b c)) #t (member? '(b c) '(a (b c) d)) #t ((equal? elm (car set)) #t) (else (member? elm (cdr set))))) Merk at den eksplisitte returverdien #t i andre cond-clausee her ikke er nødvendig ettersom eq? og equal? er ekte predikater og returnerer enten #t eller #f. 255 Vi vil ha med predikatet element-of-set? i abstraksjonsbarrieren for mengder. Skulle vi ha ønsket at mengder skulle kunne inneholde mengder, måtte vi ha definert element-of-set? vha. member?. For å kunne legge et element inn i en mengde definerer vi: (define (adjoin-set x set) (if (element-of-set? x set) set (cons x set))) Prosedyren returnerer den aktuelle mengden, etter at det gitte elementet evt. er lagt inn. Når vil velger å unngå duplisering av elementer er det av pragmatiske grunner, for å forenkle mengdesoprasjoner som snitt og union. 256 Men om vi ønsker ordnede mengder, kan vi ikke tillate mengder i mengder Snittoperasjonen kan vi definere slik: (define (intersection-set set1 set2) ;S (cond ((or (null? set1) (null? set2)) '()) ;én eller begge lister er tomme, så ingen flere felles elementer ((element-of-set? (car set1) set2) ;set1-elementet finnes i begge mengdene (cons (car set1) (intersection-set (cdr set1) set2))) (else (intersection-set (cdr set1) set2)))) ;set1-elementet finnes ikkje i set2. (intersection-set '(a b d g h i) '(b c d e h k)) (b d h) Ser vi på arbeidsmengden her, finner vi at element-of-set? og adjoin-set, som kaller element-of-set? én gang, har linær arbeidsmengde, mens intersection-set som kaller element-of-set? for hvert element i den ene argumentmengden, har kvadratisk arbeidmsengde. Det samme vil union-operasjonen få. 257 Merk at set2 forblir uendret gjennom alle rekursive kall, og testen (null? set2) er dermed bare relevant ved første kall på intersection. For å tydeliggjøre dette, kunne vi ha skrevet prosedyren slik: (define (intersection-set set1 set2) (define (iter set1) (cond ((null? set1) '()) ((member (car set1) set2) (cons (car set1) (iter (cdr set1)))) (else (iter (cdr set1))))) (if (null? set2) '() (iter set1))) Og dette gir dessuten, som man ser, en iterativ prosess. 258 Mengder som ordnede lister En ordnet mengde er et par (S, R) der - S er en mengde og - R er en binær ordningsrelasjon på mengdens elementer, typisk mengden av tallene og relasjonen 3, (Z, ). Par-relasjonen gjør lister god egnet for representasjon av ordnede mengder, der car og cdr alltid angir det første og det etterfølgende element. Vi søker i en uordnet liste L. Vårt anliggende her er imidlertid ikke ordnede mengder, men å effektivisere mengdesoprasjonene ved hjelp av ordnede lister - Hvis det for hvert søk x er svært sannsynlig at xer i L, får vi en gjennomsnittlig søkelengde = |L|/2, Ved operasjoner på ordnede lister kan vi og redusere den linære søkelengden fra n til n/2 - hvis det for hvert søk x er svært lite sannsynlig at xer i L, får vi en gjennomsnittlig søkelengde = |L|, for elementer som ikke blir funnet. Men fremfor alt kan vi redusere arbeidsmengden for snitt og union fra kvadratisk til lineær. men, som det fremgår av neste side, - hvis L var ordnet, ville vi i begge tilfeller få en gjennomsnittlig søkelengde = |L|/2. (|L| = lengden til L.) 3 I Scheme er også parene (character, char<=?) og (string, string<=?) ordnede mengder, men her begrenser vi oss til å se på mengder av tall. 259 Halvering av linær søkelengde i en ordnet mengde: (define (element-of-set? elem set) (cond ((null? set) #f) ((= elem (car set)) #t) ((< elem (car set)) #f) (else (element-of-set? elem (cdr set))))) Effektiviseringen oppnås ved at vi stopper når det evt. ikke er noe vits i å lete lenger (element-of-set? 3 '(1 2 4 5)) #f Vi stopper her når vi kommet halveis, og ser at det søkte elementet ikke kan ligge lenger ut. 260 Linearisering av snittoperasjon vha. parallell gjennomløping av de ordnede argumentmengdene (define (intersection-set set1 set2) (if (or (null? set1) (null? set2)) '() (let ((x1 (car set1)) (x2 (car set2))) (cond ((< x1 x2) (intersection-set (cdr set1) set2)) ((< x2 x1) (intersection-set set1 (cdr set2))) (else ; x1 = x2 (cons x1 (intersection-set (cdr set1) (cdr set2)))))))) (intersection-set '(1 2 4 7 8 9 11 14 15) '(2 3 4 5 8 10 11 12 14 17)) (2 4 8 11 14) 261 Ovenstående utforming av algoritmen avviker et ørlite grann fra lærebokens, idet vi nøyer oss med å teste for to av de tre mulige størrelsesrelasjonene mellom x1 og x2 og lar den tredje være implisert i else-grenen. La oss følge utførelsen av ovenstående kall, idet (vi later som om4) snittet fylles opp underveis. runde set1 x1 set2 x2 snitt 1 (1 2 4 7 8 9) 1 (2 3 4 5 8 10) 2 () 2 (2 4 7 8 9) 2 (2 3 4 5 8 10) 2 () 3 (4 7 8 9) 4 (3 4 5 8 10) 3 (2) 4 (4 7 8 9) 4 (4 5 8 10) 4 (2) 5 (7 8 9) 7 (5 8 10) 5 (2 4) 6 (7 8 9) 7 (8 10) 8 (2 4) 7 (8 9) 8 (8 10) 8 (2 4) 9 (9) 9 (10) () - - 10 10 (2 4 8) - (2 4 8) Vi ser at får en arbeidsmengde n + m. 4 Siden prosedyren gir en rekursiv prosess, vil i realiteten snittet ikke fylles opp før rekursjonen avvikles—kall for utenforliggende kall. 262 Fletting Ovenstående kan sees som en variant av en mer generell flette(merge)-algoritme. Ordinær fletting gir unionen av to mengder med eller uten duplisering av elementer, avhengig av hva som skjer ved likhet (i else-grenen i algoritmen over). Ellers kan vi variere algoritmen mht. når vi cons-er inn nye elementer i resultatmengden. Variasjonene omfatter: - full fletting med multiplisitet (mulige multiple forekomster av elementer) (typisk ved Merge Sort) - union, dvs. fletting med bare unike elementer i resultatmengden - snitt - differansen : set1 – set2 eller set2 – set1. 263 (define (combine-sets set-1 set-2) (cond (<set-1 er gjennomløpt> ; basistilfelle-1 <basisverdi-1>) (<set-2 er gjennomløpt> ; basistilfelle-2 <basisverdi-2>) (else ; almenntilfellet (<identifiser første element fra hver mengde> (cond (<elementet fra set1 er minst> ; almenntilfelle-1 (combine-sets ...)) (<elementet fra set2 er minst> ; almenntilfelle-2 (combine-sets ...)) (else (combine-sets ...)) ; elementene er like Oppgave: Erstatt pseudokoden for å lage hhv. snitt, union og differanse. NB! Ved ren sammenfletting (merge) kan mønsteret forenkles. Oppgave: Implementer fletting på enklest mulig måte. Oppgave: Kan differanse-operasjonen også forenkles, og i så fall hvordan? 264 Oppsumering av arbeidsmengden ved operasjoner på ordnede lister La M og N være to mengder. I utgangspunktet vil kostnadene ved en mengdesoperasjon være "kvadratisk", dvs mellom |M| |N|/2 og |M| |N|, |X| = størrelsen til X. idet vi sammenligner hvert element i M med hvert element i N. Ved å implementere mengdestypen slik at mengdeselementen ligger i en ordnet liste—f.eks. som tall, i stigende orden, kan vi redusere kostnadene ved mengdesoperasjoner fra kvadratisk til lineær, dvs fra |M| |N|/2 til |M| + |N|, idet vi sammenligner elementene i M med elementene i N, i en parallell gjennomløping av de to mengdene Søking i lister er allerede i utgangspunktet en lineær prosess, så her blir gevinsten ved å ordne mengden i beste fall en halvering av arbeidsmengen. Er sannsynligheten for funn liten får vi en halvering, men er sannynligheten stor, får vi ingen gevinst 265 SICP 2.3.3 Mengder representert ved binære trær Med hensyn til søking kan vi organisere mengder som binære trær. Et tre er bygget opp av noderf.eks. slik at hver node har en verdi og ingen, ett eller flere subtrær. I et binært tre har ingen node mer enn to subtrær, og i noen binære trær—som de vi skal bruke—har hver node nøyaktig to subtrær, når et tomt tre også regnes som et subtre. For et binært tre med stigende unike verdier, gjelder følgende krav: For hver node x skal alle noder til venstre for x ha lavere og alle nodene til høyre for x ha høyere verdi enn x. 266 Som vi ser av trærne under, som alle representerer mengden {1, 3, 5, 7, 9, 11}, gir dette kravet opphav til flere mulige ulike representasjoner av en og samme mengde, men for at et binært tre skal gi en effektiv organisering mht. søk, må det være balansert (noe vi ikke har tid til å gå inn på her). 132 A: '(11 (9 (7 (5 (3 (1 () ()) ()) ()) ()) ()) ()) B: '(7 (3 (1 () ()) (5 () ())) (9 () (11 () ()))) C: '(3 (1 () ()) (7 (5 () ()) (9 () (11 () ())))) D: '(5 (3 (1 () ()) ()) (9 (7 () ()) (11 () ()))) E: '(1 () (3 () (5 () (7 () (9 () (11 () ())))))) 267 Antall binære trær med 6 noder = Ved binær søking i en ordnet rekke får vi logaritmisk arbeidsmengde, 0 2 1 4 2 5 3 7 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 8 11 12 14 15 21 22 24 25 31 32 35 36 37 40 42 45 48 24 25 31 32 35 36 37 40 42 45 48 24 25 31 32 35 32 35 35 som i dette eksemplet der vi finner verdien 35 blant 22 verdier ved 4 ( log2 22) halveringer. Midtpunktet beregnes til n/2 når x = floor(x), dvs. nærmeste heltall <= x. En slik søkemåte forutsetter en struktur der vi har direkte aksess til de enkelte elementene typisk en vektor. I en liste er vi henvist til å søke sekvensielt, men med en trestruktur oppheves denne begrensningen. Riktignok må vi vandre gjennom en sekvens av noder, men søkeveien går via forgreninger, tilsammen ikke mer enn log2 n, hvis treet er balansert. 268 La l være logaritmen til et tall n mht. en base b, dvs. l = logbn. Da er l det tallet vi må opphøye b i for å få n, dvs. n = bl. F.eks. for n = 1000 og b = 10, har vi l = log101000 = 3 og 103 = 1000. Når vi beregner antall med binære halveringer, lar vi basen være 2. F.eks. for n = 512 og b = 2, har vi l = log2512 = 9 og 29 = 512. Hvis n ligger mellom to potenser av to, f.eks. n = 350 og dermed ligger mellom 28 = 256 og 29 = 512, må vi i verste fall foreta 9 halveringer for, om mulig, å finne det vi søker i et balansert. tre. (22 (8 (4 (2 () ()) (5 () (7 () ()))) (14 (11 () (12 () ())) (15 () (21 () ())))) (36 (31 (24 () (25 () ())) (32 () (35 () ()))) (42 (37 () (40 () ())) (45 () (48 () ()))))) Vi finner 35 i fire trinn ved å gå fra 22 < 35 til venstre fra 36 > 35 til venstre fra 31 < 35 til høyre fra 32 < 35 til høyre der vi finner 35 269 Vi definerer følgende konstruktor og selektorer: (define (make-tree entry left right) (list entry left right)) (define (entry tree) (car tree)) (define (left-branch tree) (cadr tree)) (define (right-branch tree) (caddr tree)) Også ved søking og innsetting, som ved snittoperasjonen over, avviker vi fra læreboken, i det vi lar den tredje av tre mulige tilfeller, nemlig likhet, være implisert i else-grenen. Søkealgoritmen blir da slik: (define (element-of-set? x set) (cond ((null? set) #f) ((< x (entry set)) (element-of-set? x (left-branch set))) ((> x (entry set)) (element-of-set? x (right-branch set))) (else #t))) ; x = (entry set) 270 Og innsettingsalgoritmen blir slik: (define (adjoin-set x set) (cond ((null? set) (make-tree x '() '())) ((< x (entry set)) (make-tree (entry set) (adjoin-set x (left-branch set)) (right-branch set))) ((> x (entry set)) (make-tree (entry set) (left-branch set) (adjoin-set x (right-branch set)))) (else set))) Dette kan gi ubalanse. Ovenstående algoritme gjør at alle nye verdier havner nederst Hvis elementene kommer i stigende orden, i treet, som blader. For å sikre at treet blir balansert, må vi ha får vi i realiteten en ren liste der hvert nytt element muligheten for å plassere nye verdier høyere opp i treet. Dette krever betydelig mer tenking og betydelig mer kode. havner i høyregren til nederste element. 271 La oss følge innsettingen av 9 i treet på side 279: (a) Treet er ikke tomt, og x = 9 < entry = 22. (b) Lag nytt tre med samme entry, med 9 lagt til i venstre gren og med samme høyregren (c) Treet er ikke tomt, og x = 9 > entry = 8. (d) Lag nytt tre med samme entry og samme venstregren med 9 lagt til i høyre gren (e) Treet er ikke tomt, og x = 9 < entry = 14. (f) Lag nytt tre med samme entry, med 9 lagt til i venstre gren og med samme høyregren (g) Treet er ikke tomt, og x (= 9) < (entry = 11). (h) Lag nytt tre med samme entry, med 9 lagt til i venstre gren og med samme høyregren (i) Treet er tomt (j) Lag nytt tre med 9 som entry og tom venstre- og høyregren. 272 Forelesning 7 Informasjonsteori og entropi Termen entropi tilhører i utgangspunktet termodynamikken, Fysikeren Rudolph Clausius (1822-88) laget der den, svært forenklet, betegner ordet entropi etter mønster av ordet energi fra reduksjon av energipotensialet innenfor et system gresk, energos, "i arbeid" (dvs. "arbeidende"), satt sammen av en, "i", "ved" + ergon, "arbeid". der det foregår enerigutveksling mellom delene. Delen tropi er da ment å være fra gresk tropos, F.eks. to innsjøer A og B, med A høyere enn B, forbundet med et rør fra bunnen av A til B danner et system der energipotensialet er vannmengden i A. "vending". Det greske ordet entropi betyr " Begrenser vi oss til å betrakte systemets varme, "det som er inneholdt i en endring", eller mer finner vi at spesifikt "det som er inneholdt i et forfall". vending mot noe". Det Classius tenkte på var kanskje noe sånt som etter hvert som temperaturforskjellene synker, stiger entropien inntil den når et maksimum der det ikke lenger finnes noen temperaturforskjeller innenfor systemet. 273 Generelt kan vi si at et system med maksimum entropi er et fullstendig amorft system der det ikke finnes noen some helst signifikante forskjeller i noe som helst henseende, eller om vi vil: et katotisk system — et system uten holdepunkter. I forhold til informasjonsteori kan vi se på entropi, ikke som tap av intern energi, men som behov for ekstern energi. Jo mer kaotisk et system er, desto mer energi—eller altså informasjonsbiter— kreves det for å få noe meningsfullt ut av det. 274 Her er det en tilsynelatende motsetning mellom En verden der det alltid er vinter termodynamikk og informasjonsteori. Universets krever ikke et eneste bit. varmedød, entropiens maksimum, inntrer når alt har fått samme temperatur, når ingenting lenger For a skille mellom vinter- og sommer, skjer. Motsetningen forsvinner når vi innser at trenger vi ett bit—som gir oss verdiene 0 og 1. ingenting er det samme som alt på en gang (cfr. uttrykket "dette sier jo alt og ingenting"), som er For å skille mellom de tre skandinaviske land, det stikk motsatte av nøyaktig én ting (alltid vinter). klarer vi oss med to bits—som i alle fall gir oss En pussighet: Han som lanserte begrepet uni- verdiene 01, 10, og 11. versets varmedød, Lord Kelvin (en kjenning av Clausius), var også opphavsmannen til den For å skille mellom de fire årstider, temperaturskala som starter ved det absolutte må vi ha to bits—som gir oss nullpunkt—og dette er, på en abstrakt måte, like verdiene 00, 01, 10, og 11. langt fra varmedøden som temperaturen ved universets begynnelse. (Nå var jo universet i utgangs- For å skille mellom årets maksimalt 53 uker, punktet nærmest uendelig lite, og varmen dermed klarer vi oss med seks bits, som gir tilnærmet uniformt distribuert, men universet var også nærmest uendelig ustabilt, og dermed fikk vi verdiene 000000 ... 110100 plus noen til The Big Bang (for å si del litt flåsete). Hvor stabilt (0 ... 52 desimalt). universet ville være med en uniform temperatur på 0o K, kan man jo bare spekulere på.) 275 Mer presist betegner termen entropi i informasjonsteorien den mengde informasjon en variabel størrelse kan sies å ha, eller med andre ord, det antall bits som må til for å kode variabelen entydig. Kodingen 01 for Danmark, 10 for Norge, 11for Sverige er (trivielt) entydig fordi 01 {10, 11}. 10 {01, 11}. 11 {01, 10}. Med 0 for Danmark hadde vi fremdeles hatt entydighet, fordi hverken 10 eller 11 begynner med 0, og dermed kunne vi ha nøyd oss med gjennomsnittlig 5/3 bits. På den andre siden: med to bits har systemet rom for et fjerde skandinavisk land, f.eks. Island, som kan kodes med 00. 276 Vi nedtegner resulatetene av en serie kast med en åttekantet "terning" (en oktaeder)5. Vi lar X være det variable utkommet av de enkelte terningkast. Siden X kan ha én av 8 mulige verdier, og ett bit har to mulige verdier, og 8 = 23, trenger vi 3 bits for å beskrive verdiene til X entydig, eller m.a.o. X har en entropi = 3. 000 5 001 010 011 100 101 110 111 En bearbeiding av et eksempel i Manning & Schütze, 2002, Foundations of Statistical Natural Language Processing, MIT Press 277 Så hva med den mer typiske sekskantede terningen, når vi nummererer sidene fra 0 til 5? Nå trenger vi tre bits for hver av 4 og 5, hhv. 100 og 101, så også i dette tilfellet synes X å måtte ha en entropi = 3. Men tillater vi at ulike verdier av X uttrykkes med ulike antall bits, kan vi klare oss med gjennomsnittlig færre antall bits, så lengde kodingen er entydig. F.eks. er følgende rekke av verdier innbyrdes entydig fordi ingen av de tresifrede verdiene inneholder noen av de tosifrede som første del. 00 01 100 101 110 111 Ingen andre koder enn 00 starter med 00, og ingen andre koder enn 01 starter med 01. På tilsvarende måte kunne vi ha kodet systemet (Danmark, Norge, Sverige) slik 0 for Danmark, 10 for Norge, 11for Sverige. Merk at både 2 og 3 i og for seg kun trenger to bits hver, 10 og 11, men dette ville ha gitt flertydighet. 278 Bit-strengen 00101100010110100110111100 kan bare leses på én måte I dette tilfellet er entropien til X = (22 + 34)/6 = 8/3 2,66667 bits. 279 Ovenstående angivelse av en variabels entropi er presis, bare om vi går ut fra at alle variabelens mulige verdier er like sannsynlige. Går vi ut fra at terningen er skjev eller har ujevnt fordelt masse, må entropien uttrykkes som en funksjon av mengden av de ulike kastenes sannsynligheter. F.eks. dersom sidene med 2 og 4 øyne kommer opp dobbelt så ofte som de øvrige, får vi følgende fordeling for terningens ulike sider i 8 kast: Lar vi de to hyppigst forekommende verdiene kodes med 2 bits og de øvrige med 3, vil vi i det lange løp bruke i snitt (2 sider 2 bits 2 ganger + 4 sider 3 bits 1 gang)/8 ganger = 20/8 = 2.5 side-bits per gang, hvilket vil si 2.5 bits per kode—hvilket vil si at entropien(X) = 2.5. 280 Generelt skal vi ha summen av produktene av bitantall og vekter delt på summen av vektene altså, når bi og vi er hhv. antall bits for, og vekten (hyppigheten) til, terning i: n n i=1 i=1 E(X) = (bivi) / vi Tilsvarende er regnestykket over ((3 1) + (2 2) + (3 1) + (2 2) + (3 1) + (3 1)) / (1 + 2 + 1 + 2 + 1 + 1). Et tegnsystem som fremviser tilsvarende regelmessighet er morsekoden, der de mest brukte tegnene er kodet med færrest prikker og/eller streker. 281 Frekvenssortering Ordning av mengder etter elementenes verdier kan gi gevinst både ved ulike mengdesoperasjoner og ved søking, men i noen tilfeller kan det være vel så lurt å ordne mengden etter elementenes etterspørselsfrekvens, slik at de elementene som etterspørres mest, legges først. I informasjonsteorien bruker vi frekvensene til tegnene i et system til å beregne den optimale kodingen av systemet, idet vi bruker færrest bits på de mest frekvente og flest bits på de minst frekvente tegnene. Dette gir en komprimeringsgevinst, men også en tidsgevinst, dvs. den optimale kodingen gir en ordning i den forstand at bitlengden til en kode bestemmer hvor lang tid det tar å lese den. Huffmann-koding gir både komprimering og tidsbesparelse. 282 SICP 2.3.4 Huffmann-koding David Huffmann utviklet en algoritme for en optimal fordeling avantall bits innenfor et alfabet i henhold til tegnenes bruksfrekvens.6 En Huffman-kode kan representeres ved et binært tre. - Hvert subtre inneholder - en liste over alle tegnene i subtreet. - disse tegnenes samlede vekt og - et venstre og et høyre subtre, hvorav ett eller begge kan være blader. - Hvert blad inneholder - et kodet tegn og - dets vekt. Vektene brukes til å organisere treet optimalt med hensyn til koding og avkoding av tegn, men vektene brukes ikke under selve kodingen og avkodingen. 6 Huffmanns leverte dette som en semetseroppgave i et kurs i informasjonsteori. 283 Gitt et 8-tegns alfabet med følgend relative bruksfrekvenser: A:8 B:3 C:1 D:1 E:1 F:1 G:1 H:1 C:1010 D:1011 E:1100 F:1101 G:1110 H:1111 Med følgende optimale koding A:0 B:100 får vi en tegnvis entropi på (181 + 133 + 614)/28 = 41/17 = 2 Det tilsvarende treet ser slik ut: (NB! uten tag'ene i Scheme-representasjonen) Mens hvert tre har en tegnmengde, har hver bladnode ett tegn—ikke en singleton tegnmengde. {A B C D E F G H} 17 / A 8 \ {B C D E F G H} 9 / {B C D} 5 / \ B 3 {C D} 2 / \ C 1 D 1 \ {E F G H} 4 / {E F} 2 / \ E 1 F 1 284 \ {G H} 2 / \ G 1 H 1 Koding For koding av et tegn følger vi de nodene som inneholder tegnet, inntil vi kommer til den aktuelle bladnoden. For hver venstregren vi følger, legger vi et 0-bit til koden, og for hver høyregren legger vi til et 1-bit. F.eks. for å kode C, ser vi fra rotnoden at C ligger i høyre subtre. Sett fra dettes rot, finner vi C i venstre subtre, sett fra dettes rot, finner vi C i høyre subtre, og endelig, sett fra dettes rot, finner vi C i venstre bladnode. Veien høyrevenstrehøyrevenstre gir 1010, som blir koden til C. Avkoding For å avkode fra en bitsekvens til et tegn følger vi grenen på tilsvarende måte. F.eks. med sekvensen 1100 følger vi veien høyrehøyrevenstrevenstre som bringer oss til bladnoden med tegnet E. 285 Generering av Huffman-tre Algoritmen for å generere treet er litt snedig. Den krever ikke all verdens kode, men en smule konsentrasjon. NB! datasekvensen på side 164. i læreboken kan virke noe forvirrende, ettersom parene i hver linje er ordnet etter synkende vekt, mens algoritmen forutsetter stigende vekt. Poenget er å få satt opp treet slik at - de mest brukte symbolene, altså de med høyes vekt, er mest tilgjengelig, hvilket vil si det samme som at - jo mindre brukt et symbol er, desto lengre fra roten ligger det. 286 Vi starter med ett flatt tre, dvs. en liste der - alle symbolene er paret med hver sin vekt, i form av bladnoder - forener bladnoder til to-tegns subtrær og forener to og to subtrær til større subtrær - inntil vi står igjen med ett fullvokst tre. For hver runde - forener vi de to gjenværende bladene og/eller subtrærne A og B som har lavest vekt, - i et subtre med en rotnode C som får en vekt lik summen av vektene til A og B, hvilket innebærer at C, får høyere vekt enn, og blir liggende over, både A og B. Dermed vil - stadig flere bladnoder bli inkorporert i trær, og - stadig flere subtrær bli inkorporert i overordnede trær, inntil vi til slutt står igjen med - en rotnode der - alle subtrær med underligggende subtrær og blader er forenet - og der alle subtrær har lavere vekt enn sine supertrær. NB! Her og i det følgende brukes termen node mer eller mindre synonymt med termen subtre. 287 I eksemplet på neste side vises de suksessive argumentene til prosedyren successive-merge, som binder algoritmen sammen. De blå nodene er de som vil bli slått sammen i løpende runde, mens rammene inneholder det akkumulerte resultatet av sammenslåingene. Legg merke til at bladnodene i den opprinnelige listen er ordnet etter stigende vekt, og at de nye noder som skapes gjennom sammenslåingene også ordnes etter stigende vekt etter de nodene som har samme vekt eller lavere og foran de nodene som har høyere vekt. Og siden vi i hver runde slår sammen de to første, letteste, nodene, slik at disse ved neste sammenslåing havner under noder med høyere vekt, får vi et binært tre ordnet fra bladene mot roten etter stigende vekt. Også her er bladnodene vist uten tag'er, så vi kan se på disse som kjennetegnet ved at første element ikke er et par —noe vi forsåvidt også kunne ha gjort i implementasjonen. 288 (successive-merge ((h 1) (g 1) (f 1) (e 1) (d 1) (c 1) (b 3) (a 8))) (successive-merge ((f 1) (e 1) (d 1) (c 1) ((h 1) (g 1) (h g) 2) (b 3) (a 8))) (successive-merge ((d 1) (c 1) ((h 1) (g 1) (h g) 2) ((f 1) (e 1) (f e) 2) (b 3) (a 8))) (successive-merge (((h 1) (g 1) (h g) 2) ((f 1) (e 1) (f e) 2) ((d 1) (c 1) (d c) 2)(b 3) (a 8))) (successive-merge (((d 1) (c 1) (d c) 2) (b 3) (((h 1) (g 1) (h g) 2) ((f 1) (e 1) (f e) 2) (h g f e) 4) (a 8))) (successive-merge ((((h 1) (g 1) (h g) 2) ((f 1) (e 1) (f e) 2) (h g f e) 4) (((d 1) (c 1) (d c) 2) (b 3) (d c b) 5) (a 8))) (successive-merge ((a 8) ; 1 ; 2 ; 3 ; 4 ; 5 ; 6 ; 7 ((((h 1) (g 1) (h g) 2) ((f 1) (e 1) (f e) 2) (h g f e) 4) (((d 1) (c 1) (d c) 2) (b 3) (d c b) 5) (h g f e d c b) 9) )) (successive-merge ((((a 8) ((((h 1) (g 1) (h g) 2) ((f 1) (e 1) (f e) 2) (h g f e) 4) (((d 1) (c 1) (d c) 2) (b 3) (d c b) 5) (h g f e d c b) 9) (a h g f e d c b) 17))) 289 ; 8 Scheme-representasjon ; "tag" bladnoden for typeidentifikasjon ; sml derivasjonseksemplet i avsnitt 2.3. (define (make-leaf symbol weight) (list 'leaf symbol weight)) (define (leaf? object) (eq? (car object) 'leaf)) (define (symbol-leaf x) (cadr x)) (define (weight-leaf x) (caddr x)) (define (make-code-tree left right) (list left right (append (symbols left) (symbols right)) (+ (weight left) (weight right)))) ; venstre subtre ; høyre subtre ; alle symboler i det nye subtreet ; det nye subtreets samlede vekt (define (left-branch tree) (car tree)) (define (right-branch tree) (cadr tree)) (define (symbols tree) (if (leaf? tree) (list (symbol-leaf tree)) (caddr tree))) ; Mens hvert tre har tegnmengde ; har hver bladnode et enkelt tegn (define (weight tree) (if (leaf? tree) (weight-leaf tree) (cadddr tree))) 290 ;; adjoin-set plasserer en ny node, et nytt subtre, i listen med subtrær på løpende nivå. (define (adjoin-set x set) (cond ((null? set) (list x)) ((< (weight x) (weight (car set))) (cons x set)) (else (cons (car set) (adjoin-set x (cdr set)))))) ; Plasser x i henhold til stigende vekt ; x er tyngre enn alle andre subtrær, så ; plasser x sist ; Alle trær heretter er tyngre enn x, så ; plasser x her. ; Løpende subtre er like lett som eller lettere en x, så ; kopier inn løpende node, og ; let videre (define (make-leaf-set pairs) ; Konverter en liste med symbol-vekt-par ; til en liste med tag'ede blader (if (null? pairs) '() (let ((pair (car pairs))) ; første gjenværende symbol-vekt-par (adjoin-set (make-leaf (car pair) (cadr pair)) ; put nytt tag'et blad (make-leaf-set (cdr pairs)))))) ; i konvertert liste ; UKEOPPGAVE (define (successive-merge leaf-set) ...) (define (generate-huffman-tree pairs) (successive-merge (make-leaf-set pairs))) 291 (define (encode-symbol sym tree) ; UKEOPPGAVE ...) (define (encode message tree) (if (null? message) '() (append (encode-symbol (car message) tree) (encode (cdr message) tree)))) (define (decode bits tree) (define (decode-1 bits cur-branch) (if (null? bits) '() (let ((next-branch (choose-branch (car bits) cur-branch))) (if (leaf? next-branch) (cons (symbol-leaf next-branch) (decode-1 (cdr bits) tree)) (decode-1 (cdr bits) next-branch))))) (decode-1 bits tree)) (define (choose-branch bit branch) (cond ((= bit 0) (left-branch branch)) ((= bit 1) (right-branch branch)) (else (error "bad bit -- CHOOSE-BRANCH" bit)))) 292 Forelesning 8 Imperativ—tilstandsendrende, verditilordnende, mutativ, destruktiv—programmering Fra en platonsk posisjon kunne vi si noe sånt som dette: Ved beregningen av en funksjons verdi, når alle variabler er bundet, og verden står stille, En annen ting er at vi i den sannselige materiell verden, bruker materien til langt mer enn regning. Vi spiser, sover, arbeider, sloss, parer oss, morer oss, osv. og alt dette er fundamentalt sett endring. er beregningen egentlig bare en avdekning av For noen av disse formål har vi utviklet stadig mer avansert evig og uforanderlig relasjoner mellom tall. Når vi bruker hjelpemidler som småsten, kuleramme, penn og papir eller datamaskiner, der vi hele tiden endrer de sanselige tingenes tilstand, øver vi vold på de idéelle tingenes tilstand. teknologi, og teknologi består i sitt vesen i tilstandsendring, på samme måte som det menneskelige liv består i endring. Altså: En datamaskin er til syvende og sist ikke interessant for oss, med mindre den gjør noe, men for å få gjort mer enn å rotere, blinke og tute, må datamaskinen kunne regne. Fra denne posisjonen vil man kunne betegne verditilordning, som destruktiv, og det gjør man da også. 293 Hvorfor ta opp vertitilordning og tilstandsendring i et kurs om funksjonell programmering? a. Vi må ha mer enn en overflatisk kjenneskap til forskjellen. Paradigmet for imperativ programmering er iterasjon med verditilordning. Paradigmet for funksjonell programmering er rekursjon med argument passing. b. Visse operasjoner er svært tungvinte, eller kanskje umulige, hvis vi insisterer på funksjonell programmering på alle implementasjonsnivåer. Også språk som på høyeste nivå er strengt funksjonelle, bruker tilstandsendringer på lavere nivåer, nettopp for å få til slike operasjoner. c. Visse funksjonelle objekter bærer med seg essentiell intern tilstandsinformasjon. Dette gjelder bl.a. tabeller for lagring av ferdig utregnede resultater (memoisering) og parene i det vi kaller strømmer, en form for (potensielt) uendelige lister der hvert objekt er sin egen funksjon. (litt uklart dette nå, men blir klarere etter hvert) 294 EKSEMPEL 1 — INPUT Input til et program kommer utenfra, fra en verden programmet ikke har kontroll over, og er sitt vesen tilstandsbasert, ettersom det bringer inn verdier som ikke kune forutsees (beregnes) idet programmet startet. I språket Haskell brukes monader bl.a. for å håndtere IO. Begrepet monade er vanskelig. 295 EKSEMPEL 2 — VILKÅRLIGE TALL Vilkårlighet—generering av random-tall, er også essensielt tilstandsbasert. Riktignok kan man ikke programmere vilkårlighet Programmert vilkårlighet er en kontradiksjon. men man kan programmere pseudo-vilkårlighet, noe som for mange formål er godt nok. En fornuftig (pseudo-)random-generator vil kjenne sin egen tilstand, og holde denne for seg selv. Alternativt, i et strengt funksjonelt program, ville random-generatoren selv, og de variabler den brukte for generering av nye tall, måtte sendes rundt til alle de prosedyrer som direkte eller indirekte brukte den. 296 Men allikevel: En random-generator er i realiteten Enhver fornuftige randomgenerator er modulus-basert. en rekursiv funksjon r med en Dvs. vi definerer f ved bl.a. en m, slik at tilhørende oppdateringsfunksjon f, slik at 0 Xk < m og 0 f(Xk) < m, for alle k 0. Dermed kan sekvensen inneholde maks m ulike tall, og r(xi+1) = f(r(xi) ). det lar seg vise at det da må finnes et tall p slik at Eller, sagt på en annen måte: Xk = Xk+p, for alle k 0, spesifikt slik at X0 = Xp, hvilket betyr at sekvensen repeteres for hvert p-te tall. En random-generator er i realiteten Vi sier at X0 … Xp er sekvensens periode og at en sekvens X0, X1, X2, … med en p er sekvensens periodelengde. tilhørende oppdateringsfunksjon f, slik at Vi kunne dermed i prinsippet ha representert enhver fornuftig randomgenerator ved en endelig liste, men i Xk+1 = f(Xk). praksis ville dette ha krevd for mye plass. I et strengt funksjonelt program (Et ekstremt tilfelle er Multiply With Carry som kan ha må vi sende f og Xk rundt omkring, periodelengder på opp i mot 22000000 (et tall med over syv fra den ene berørte prosedyren til den andre. hundre tusen desimale sifre—langt, langt mer enn antall atomer i universet, som er 2265). Her er f definert ved en mengde av tilstandsvariabler, slik at f(Xk) er bestemt av (Vi ser mer på random-tall i neste forelesning.) både X og k.) 297 Scheme har følgende prosedyrer for verditilordning uttalt sett bæng set! (set! a b) binder variabelen a til verdien b, til fortrengsel for den verdien a hadde. uttalt sett kar bæng set-car! (set-car! a b) binder car-delen i paret a til verdien b. til fortrengsel for den verdien (car a) hadde. uttalt sett kuddr bæng set-cdr! (set-cdr! a b) binder cdr-delen i paret a til verdien b. til fortrengsel for den verdien (cdr a) hadde. I tillegg finnes de destruktive prosedyrene vector-set! og string-set! Særlig bruken av set-cdr! kan gi opphav til noen nokså tricky situasjoner, som når to lister løper sammen, eller en liste løper sammen med seg selv. 298 Et par hvis innhold kan endres ved verditilordning, kalles et muterbart par. I læreboka, og i R5RS, er alle par muterbare, mens det i Racket skilles mellom ikke-muterbare og muterbare par, slik at disse er to forskjellig typer, distingvert ved at alle prosedyrer for muterbare par er prefikset med m mpair?, mcons, mcar, mcdr, mlist, etc. I forelesningene holder vi oss til læreboka. For å løse oppgaver med muterbare par, kan man velge R5RS som språk i DrRacket (anbefales), eller bruke Racket sine muterbare par. For de det måtte interessere: Rackets har rutinebiblioteker som er utilgjengelig fra R5RS, for bl.a. sortering, filtrering o.l. og behandling av filer og directories. 299 Vi har et kar (en veskebeholder), representert i Scheme ved et par der car-delen angir kapasiteten og cdr-delen angir det aktuelle innholdet, dvs. volumet til vesken i karet. Her er to implementasjoner av tapping / påfylling av beholderene funksjonell imperativ. (define (tapp-eller-fyll beholder delta) (define (tapp-eller-fyll! beholder delta) (let ((kapasitet (car beholder)) (let ((kapasitet (car beholder)) (innhold (+ (cdr beholder) delta))) (cond ((< innhold 0) (innhold (+ (cdr beholder) delta))) (cond ((< innhold 0) ; prøver å tappe mer ; enn det er igjen (cons kapasitet 0)) (set-cdr! beholder 0)) ((> innhold kapasitet); prøver å fylle på mer ; enn det er plass til ((> innhold kapasitet) (cons kapasitet kapasitet)) ; prøver å fylle på mer ; enn det er plass til (set-cdr! beholder kapasitet)) (else (cons kapasitet innhold))))) Her får vi tilbake et nytt kar, ; prøver å tappe mer ; enn det er igjen (else (set-cdr! beholder innhold))))) Her endrer vi innholdet i det opprinnelige karet. med summen av eller differansen mellom opprinnelig og påfylt eller tappet veske. 300 Til dette bildet hører det et nytt syn på forholdet mellom en variabel og dens verdi: Det er ikke lenger snakk om en enkel, umiddelbar binding mellom variabel og verdi, men om en relasjon mellom - en variabel og - et sted med - plass til - en verdi. (Andre termer er lokasjon, beholder eller beholder i en lokasjon.) 301 Setningssekvensen (define v 4) (set! v 266) gir opphav til en tilstandssekvens som kan visualisere på ulike måter: Peker til beholder v 4 v Adresse i RAM 266 v: 7 0:06B32A90 1:094356BF 2:654BF95D 3:A5E5431B 4:0800A20A 5:00025F0E 6:FF56CB88 7:00000004 8:0000A500 9:65498E5A A:0266F6A5 v: 7 0:06B32A90 1:094356BF 2:654BF95D 3:A5E5431B 4:0800A20A 5:00025F0E 6:FF56CB88 7:0000010A 8:0000A500 9:65498E5A A:0266F6A5 Figur 1 Figur 2 v er bundet til en peker til en beholder. v er bundet til lokasjon 7. (Verdiene er angitt heksadesimalt, slik at f.eks. 10A16 = 26610 og 25F0E16 = 15540610. De heksadesimale sifrene er Uansett visualiseringsmåte er poenget at 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F.) - variabelen bevarer sin identitet uavhengig av sin verdi, i kraft av at - den ikke refererer direkte til en verdi, men - til noe som kan inneholde en verdi, men som dermed også - kan få sitt innhold endret. 302 Et eksempel på grov misbruk av verdittilordning 1. (define a 3) - Etter 1. er a er bundet til 3 i den globale omgivelse G. 2. (define (make-proc b) - Etter 3. har prosedyren p en umiddelbar omgivelse A, (lambda (c) (+ a b c))) 3. (define p (make-proc 5)) 4. (p 7) ; ==> 15 5. (set! a 8) 6. (p 7) ; ==> 20 7. (set! p (lambda (d) 'who-what-where?)) 8. (p 7) ; ==> 'who-what-where? beskrevet i 2, der b er bundet til 5, med G som superomgivelse. - For utførelsen av kallet i 4 opprettes det en omgivelse B, der er c er bundet til 7, med A som superomgivelse. - For utførelsen av kallet i 6 opprettes det en omgivelse C, der er c er bundet til 7, med A som superomgivelse. - Men pga. endringen av verdien til a i 5, gir kallene i linje 4 og 6 ulike resultater, selv om p kalles med de samme variablene som argumenter. - I 7 destrueres den opprinnelige p, og A mister sin referanse og blir til søppel. - I 8 opprettes en omgivelse D der d er bundet til 7, men siden koden til p nå er endret, får vi nok et annet resultat. 303 Er det stor avstand mellom de ulike kallene på p og mellom kallene på p og endringer av verdien til a, får vi programmer der tilstandsendringene er vanskelig å overskue. For å unngå dette, (a) må enhver muterbar variabel tilhøre en prosedyre og ligge i dennes lokale omgivelser, og (b) alle globale variabler må være konstanter—som f.eks. og e, eller ikke-muterbare prosedyrer—typisk primitiver. 304 SICP 3 Modularitet, objekter og tilstander En bankkonto og dens saldo Anta vi har en bankkonto med en startkapital som vi har belastet med en serie uttak. Vi har notert uttakene i en liste og kjører nå denne mot startkapitalen for å finne saldoen. (define (beregn-saldo saldo uttaksliste) (if (null? uttaksliste) saldo (beregn-saldo (- saldo (car uttaksliste)) (cdr uttaksliste)))) 30 (beregn-saldo 120 '(20 40 30)) (beregn-saldo 120 '(20 40 30 20 30 40 20)) -80 305 Vi har gått i minus. Med en mer forsiktig tilnærming, kunne vi sette vi opp en liste over planlagte uttak, og sjekke hvor mange av disse vi kan realisere før saldoen går under null. (define (mulige-uttak saldo uttak) (if (or (null? uttak) (< (- saldo (car uttak)) 0)) '() (cons (car uttak) (mulige-uttak (- saldo (car uttak)) (cdr uttak))))) (mulige-uttak 120 '(20 40 30 20 30 40 20)) (20 40 30 20) —————————————————— 306 Endring og identitet Den funksjonelle løsningen er deterministisk, i likhet med alle funksjonelle beregnnger, og lite egnet for administrasjon av bankkonti. Det vi trenger er et system som holder styr på endringene i vår bankkonto over tid. og til dette trenger vi språklige mekanismer som tar vare på at et objekt forblir ett og det samme samtidig som det endres. Først prøver vi med en globale variabel, uten å endre dens verdi. Eksempel 1. (define saldo 100) (define (uttak beløp) (- saldo beløp)) (uttak 60) 40 (uttak 60) 40 ; som om vi ikke hadde tatt ut noe tidligere. 307 Dette er åpenbart ikke det vi ønsker, så vi tar i bruk den destruktive operasjonen set! Eksempel 2. (define saldo 100) (define (uttak! beløp) (set! saldo (- saldo beløp)) saldo) (uttak 60) 40 (uttak 60) -20 Og, la oss, før vi går videre, forbedre prosedyren slik at den ikke tillater overtrekk. Eksempel 3 (define saldo 100) (define (uttak beløp) (if (>= (- saldo beløp) 0) (begin (set! saldo (- saldo beløp)) saldo) "Uttaket mangler dekning")) (uttak 60) 40 (uttak 60) "Uttaket mangler dekning" 308 Sårbarhet og uoversiktlighet Problemet med de tre ovenstående løsningene er at de gjør programmet både sårbart og uoversiktlig. Den forbedringen som ligger i Eksempel 3, hjelper ikke mot sårbarheten, ettersom saldo er en global variabel som endres, og den gjør helle ikke programmet mer oversiktlig, ettersom saldo kan endres hvor som helst. Det vi trenger er en eksklusivt kobling mellom saldoen og uttaksprosedyren, slik at uvedkommende prosedyrer ikke kan forgriper seg på kontoen, og forbindelsen mellom saldoen og uttaksprosedyren blir åpenbar. 309 En måte å knytte saldoen til prodedyren på, er å pakke prosedyren inn i et lokalt let-uttrykk. Eksempel 4. (define konto (let ((saldo 100)) (lambda (beløp) (if (>= (- saldo beløp) 0) (begin (set! saldo (- saldo beløp)) saldo) "Uttaket mangler dekning")))) (konto 60) 40 Bortsett fra verdiendringen er det ingen (konto 60) "Uttaket mangler dekning" prinsippiell forskjell på Eks. 4 og dette: (define foo Poenget er at så lenge prosedyren uttak eksisterer, (let ((bar 2)) så eksisterer også dens lokale definisjonsomgivelse. (lambda (x) (* bar x)))) (foo 7) 14 Saldoen kan bare aksesseres, og evt. endres, innefor denne omgivelsen, og forbindelsen mellom saldoen og uttaksprosedyren er tydelig. 310 Videreutvikling av bankkontoprosedyren Det er en åpenbar praktisk begrensning ved Eksempel 4, idet kontoen uttak er den enste som finnes. Dette fikser vi ved å definer en konstruktor, og, for å unngå at alle konti starter med en saldo på kr 100, lar vi saldoen være argument til konstruktoren. Den får da plass i prosedyrens lokale omgivelser, helt på linje med saldoen i let-uttrykket. Eksempel 5. (define (lag-konto saldo) (lambda (beløp) (if (>= (- saldo beløp) 0) (begin (set! saldo (- saldo beløp)) saldo) "Uttaket mangler dekning"))) (define konto (lag-konto 100)) (konto 60) ; => 40 (konto 60) ; => "Uttaket mangler dekning" 311 Det er stadig slik at så lenge prosedyren konto finnes, så finnes også dens omgivelser. Ved hjelp av lag-konto, kan vi nå lage ikke bare én, men så mange konti vi vil. (define konto-1 (lag-konto 100)) (define konto-2 (lag-konto 100)) (konto-1 40) ; => 60 (konto-2 60) ; => 40 (konto-1 50) ; => 10 (konto-2 50) ; => "Uttaket mangler dekning" Selv om konto-1 og konto-2 begge tilfeldigvis fikk samme startkapital = 100, er de to forskjellige objekter, og etter at det er gjort ett uttak fra konto-1 og konto-2 på hhv. 40 og 60 har de forskjellige saldi. 312 Videreutvikling av bankkontoprosedyren Vi kan utvide kontoens handlingsreportoir: Eksempel 6 (define (lag-konto saldo) (define (uttak beløp) (if (>= (- saldo beløp) 0) (begin (set! saldo (- saldo beløp)) saldo) "Uttaket mangler dekning")) (define (innskudd beløp) (set! saldo (+ saldo beløp)) saldo) (define (ekspedér beskjed) (cond ((eq? beskjed 'uttak) uttak) ((eq? beskjed 'innskudd) innskudd) (else (error "Ukjent beskjed -- LAG-KONTO" beskjed)))) ekspedér) Her får vi, i stedet for en enkel uttaksprosedyre, en prosedyre som håndterer både uttak og innskudd, i henhold til ulike beskjed-argumenter 313 (define konto (lag-konto 100)) ((konto 'uttak) 60) ; => 40 ((konto 'uttak) 60) ; => "Uttaket mangler dekning" ((konto 'innskudd) 70) ; => 110 ((konto 'uttak) 60) ; => 50 ((konto 'saldo) 60) ; gir kjøreavbrudd med feilmeldingen : Ukjent beskjed -- LAG-KONTO saldo Her opptrer prosedyrene uttak, innskudd og ekspedér og parameteren saldo i de samme lokale omgivelsene, og et kall på lag-konto returnerer en instans av prosedyren ekspedér i disse omgivelsene med saldo bundet til verdien til det aktuelle argumentet til lag-konto. 314 Vi kunne selvsagt ha hatt en cond-clause i ekspedér som behandlet meldingen 'saldo, men det har vi altså ikke. SICP 3.1.3 Verditilordningens ulemper Med verdtitilordning holder ikke lenger susbtitusjonsmodellen for evaluering av uttrykk vi har ikke lenger noen garanti for at samme funksjon med samme argumenter alltid returnerer samme verdi. Eksempel: fakultetsberegningen uten verdittilordning (define (fakultet n) med verdittilordning. (define (fakultet n) (let ((prod 1) (teller 1)) (define (iter prod teller) (if (> teller n) ; mutandis ; " (define (iter) (if (> teller n) prod prod (iter (begin (* prod teller) (set! prod (* prod teller)) ; (a) (+ teller 1)))) (set! teller (+ teller 1)) ; (b) (iter))))) (iter 1 1)) (iter))) 315 Vi merker oss at - når beregningene utføres ved hjelp av verditilordning, så er - resultatet avhengig av rekkefølgen i utførelsen av deloperasjonene. Hver enkelt verditilordning endrer programmets tilstand, og resultatet av en operasjon er betinget av programmets tilstand. Det er derfor avgjørende, når vi teller fra 1, at produktet (a) oppdateres før telleren (b) i iterasjonen. Hadde vi talt fra 0, måtte vi ha byttet rekkefølgen, men den ville stadig ha vært like viktig. For den funksjonelle løsningen derimot, er det uten betydning i hvilken rekkefølge argumentene til iter evalueres Om tellerargumentet til det rekursive kallet evalueres før eller etter produktargumentet, vil uansett begge argumentene evalueres hver for seg, med de verdiene teller og produkt var bundet til ved det løpende kallet på iter. 316 Bankkontoprosedyren—med en lokal saldo som endres i takt med uttak og innskudd (define (lag-konto saldo) (define (uttak beløp) (if (>= (- saldo beløp) 0) (begin (set! saldo (- saldo beløp)) saldo) "Uttaket mangler dekning")) (define (innskudd beløp) (set! saldo (+ saldo beløp)) saldo) (define (ekspedér beskjed) (cond ((eq? beskjed 'uttak) uttak) ((eq? beskjed 'innskudd) innskudd) (else (error "Ukjent beskjed -- LAG-KONTO" beskjed)))) ekspedér) Fordelen med verditilordning i dette tilfellet er åpenbar: Vi har her har å gjøre med et foranderlige objekt i en foranderlig verden, og noe slikt kan vi rett og slett ikke representere i et strengt funksjonelt paradigme. 317 SICP 3.1.2 Vertitilordningens fordeler Det er fremdeles all grunn til å fastholde et strengt funksjonelt paradigme når vi arbeider med uforanderlige matematiske objekter, og selv når vi arbeider med foranderlige objekter, kan vi gjøre alle beregninger funksjonelt, og begrense tilstandsendringen til avgrensede lokale omgivelser. Vi skal imidlertid nå se et eksempel på at verditilordning også kan være en nyttig i en strengt matematisk sammenheng. Det dreier seg om bruk av vilkårlige tall i implementeringen av en teknikk kalt Monte Carlo-simulering. Las Vegas og Philadelphia er varianter av denne. 318 Monte Carlo-simulering går ut på utføre en stor mengde vilkårlig valgte eksperimenter og trekke slutninger på grunnlag av fordelingen av disses resultater i forhold til et gitt kriterium og en kjent sannsynlighetsfordeling. F.eks. gjelder følgende relasjon (påvist av E. Cesaro) for sannsynligheten for at to vilkårlig heltall x og y ikke har noen felles divisorer hvilket innebærer at de heller ikke har noen felles faktorer. P(GCD(x, y) = 1) = 6/2. P = Probability. GCD = Greatest Common Divisor Sannsynligheten for at største felles divisor for x og y = 1, altså, at x og y ikke har noen felles divisor, er 6/2. En annen betegnelse er relativt prime. Lar vi M betegne forholdet S/E, der Monte-Carlo = Successes/Experiments S er antall suksesser, dvs. eksperimenter som tilfredstiller det gitte kriteriet, og E er antall eksperimenter totalt, har vi i dette tilfellet M = 6/2 hvilket innebærer at 6/M. 319 Anta at vi har en random-funksjon rand som ikke tar noen argumenter, og som returnerer et vilkårlig heltall. Vi kan da skrive følgende program for å beregne vha. Monte Carlo-simuleringer. (define (cesaro-testen) (= (gcd (rand) (rand)) 1) ; Ret. #f el. #t avh. av om de to vilkårlige ; tallene har noen felles divisor eller ikke (define (monte-carlo antall-forsøk eksperiment) (define (iter gjenværende-forsøk suksesser) ; Ferdig, så (cond ((= gjenværende-forsøk 0) ; returner forholdet. (/ suksesser antall-forsøk)) ; Vellykket ekseperiment, så ((eksperiment) (iter (- gjenværende-forsøk 1) (+ suksesser 1))) ; øk suksesstelleren. ; Mislykket ekseperiment, så (else (iter (- gjenværende-forsøk 1) suksesser)))) ; fortsett med uendret suksessteller. (iter antall-forsøk 0)) (define (estimer-pi antall-forsøk) (sqrt (/ 6 (monte-carlo antall-forsøk cesaro-testen)))) NB! her er cesaro-testen og monte-carlo er definert helt uavhengig av hverandre. 320 Det å få en datamaskin til generere vilkårlige tall, er problematisk, ettersom det synes å måtte innebære å få maskinen til å gjøre noe den selv ikke har kontroll over. Når et utfallet av et terningskast forekommer oss å være vilkårlig, er det jo nettop fordi vi oppfatter de faktorene som bestemmer utfallet å ligge utenfor vår kontroll. Det vi ønsker er en funksjon som, Det motsatte kaller vi juks. om den kalles tilstrekkelig mange ganger, genererer en sekvens av statistisk sett uniformt distribuerte tall. For å nærme oss en slik fordeling lar vi hvert nytt tall genereres på basis av det foregående i henhold til en formel. Det er altså ingen egentlig vilkårlighet her, ettersom samme formel med samme verdier alltid gir samme resultat. Det beste vi kan oppnå, er en tallserie med den ønskede statistiske distribusjon. 321 Poenget for oss er at prosedyren rand må kjenne det tallet den genererte ved foregående kall. Anta det finnes en funksjon rand-update som tar som argument et tidligere randomgenerert tall og returnerer et nytt. Vi kan da definere rand slik at prosedyren til enhver tid har med seg det foregående randomgenererte tallet i sine lokale omgivelser. (define rand ; x er det til enhver tid (let ((x (random-init))) (lambda () (set! x (rand-update x)) x))) 322 ; foregående genererte tall. Men hva om vi ikke hadde mulighet for verdittilordning, Vi måtte i så fall sende dette tallet som argument både til randomprosedyren, og til alle de prosedyrer som direkte eller indirekte brukte denne. Det at forrige randomtall må sendes med gjennom alle kall, fører til at vi ikke kan skille det aktuelle ekseperimentet fra den generelle Monte Carlo-algoritmen. Her må vi, for å beregne , ha en spesifikk algoritme for testing av vilkårlige tallpar. 323 (define (estimer-pi antall-forsøk) (sqrt (/ 6 (tallpar-test antall-forsøk random-init)))) (define (tallpar-test antall-forsøk eksperiment initiell-x) (define (iter gjenværende-forsøk positive-forsøk forrige-x) (let ((x1 (rand-update forrige-x))) (let ((x2 (rand-update x1))) (cond ((= gjenværende-forsøk 0) (/ positive-forsøk antall-forsøk)) ((= (gcd x1 x2) 1) (iter (- gjenværende-forsøk 1) (+ positive forsøk 1) x2)) (else (iter (- gjenværende-forsøk 1) positive-forsøk x2)))) (iter antall-forsøk 0 initiell-x)) 324 Forelesning 9 Muterbare strukturer Pekere og objekter Vi har til nå sett på tilordning av atomære verdier, typisk tall, til variabler som opptrer på egne vegne. Poenget med pekere er i at de refererer til andre objekter, og det er pekerne som gir struktur til våre lister og trær. Noen ganger skiller vi explisitt mellom objekter og pekere. Andre ganger bruker vi pekernavnet som navn for det refererte objektet. De fleste pekere og objekter er anonyme, typisk cdr-verdiene i lister, og vi vet ikke hvor de er, medmindre vi leter dem opp. Endringen av en struktur skjer oftest ved endring av disse anonyme pekerne, og derfor må vi passe godt på hva vi gjør når vi utfører slike endringer. 325 Et par er egentlig et par av pekere I figur 1 er variablene x og y i og for seg ikke lister, men pekere til lister. Det samme gjelder car-delen i første element i den listen x peker på. Her her peker car-delen på listen med de to symbolene a og b. Men også i de parene der car-delen, ikke peker på en liste, er car-delen egentlig en peker til Figur 1 en beholder som inneholder et symbolet. 326 Hver for seg kan de objektene som car- og cdr-delen i et par peker på, bestå av en atomær verdi, et par eller en annen sammensatt verdi som en tegnstreng eller en vektor. I figur 1peker x på et par. Figur 2 Dette er også er en liste, fordi cdr-delen i paret peker på et annet par. Men om vi endrer cdr-delen, til f.eks. et tall, som i figur 2, så er ikke lenger det x peker på, en liste. Garbage collection I Figur 2 finnes det ikke lenger noen referanse til paret med c i car-delen, og dette vil derfor vil bli fjernet av GB, men da forsvinner også den eneste referansen til det etterfølgen objektet, som dermed også vil bli fjernet. Etter utførelsen av (set-cdr! x 7), har vi x ==> ((a b) . 7) 327 Figur 4 viser situasjonen etter at vi, med utgangspunkt.i Figur 1 (gjengitt her som Figur 3), har endret verdien til car-delen i første element i den listen x peker på, til det samme som det y peker på. Figur 3 (= Figur 1) Figur 4 Etter utføreslen av (set-car! x y), har vi x ==> Også her slår GB til og fjerner først paret med a i car-delen, og deretter paret med b i car-delen. ((e f) c d) 328 Figur 5 Her har vi, med utgangspunkt i Figur 1, Figur 6 Her ser vi resultatet av kontruksjonen av paret z endret verdien til cdr-delen med utgangspunkt i Figur 1. i første element i den listen x peker på, Merk at, bortsett fra z, får vi ingen nye objekter. til det samme som det y peker på. Dette gir x Dette gir ==> ((a b) e f) z ==> ((e f) c d) 329 Figur 9 La set-to-wow! = (lambda (x) (set-car! I og med at car og cdr i z1 peker på samme par, (car x) ’wow)) ; ikke helt som i boka I z2 refererer car og cdr til ulike lister. vil en endring av det car peker på, også endre det cdr peker på. Dermed får vi Dermed får vi z1 ==> ((a b) a b) z2 ==> ((a b) a b) (set-to-wow! z1) (set-to-wow! z2) z1 ==> ((wow b) wow b) z2 => ((wow b) a b) 330 Mutering av prosedyrebjekter I SICP 2.1.3 representeres sammensatter objekter (i dette tilfellet par) utelukkende vha. prosedyrer. Dette kan vi gjøre også med muterbare objekter, ettersom det bak abstraksjonsbarrieren ligger lokale tilstandsvariabler som vi kan endre ved verditilordning. Først ser vi på implementasjonen av par fra avsnitt 2.1.3. (define (cons x y) (define (dispatch m) (cond ((eq? m 'car) x) ((eq? m 'cdr) y) (else (error "Undefined operation — CONS" m)))) dispatch) (define (car z) (z 'car)) ; en prosedyre som tar en prosedyre som argument ; og sender meldingen 'car til dette (define (cdr z) (z 'cdr)) ; en prosedyre som tar en prosedyre som argument ; og sender meldingen 'cdr til dette 331 Så ser vi hvordan denne kan utvides til å omfatte mutatorene set-car! og set-cdr! (define (cons x y) (define (set-x! v) (set! x v)) (define (set-y! v) (set! y v)) (lambda (m) ; Her returnerer vi beskjedtolkeren direkte, uten å gå via en definisjon (cond ((eq? m 'car) x) ((eq? m 'cdr) y) ((eq? m 'set-car!) set-x!) ((eq? m 'set-cdr!) set-y!) (else (error "Undefined operation — CONS" m))))) (define (car z) (z 'car)) (define (cdr z) (z 'cdr)) (define (set-car! z v) ((z 'set-car!) v)) (define (set-cdr! z v) ((z 'set-cdr!) v)) I læreboken returnerer de tilsvarende mutatorene verdien til z etter mutasjonen. I henhold til R5RS er returverdien til primitivene set!, set-car! og set-cdr! uspesifisert. 332 Muterbare binære trær Innsetting av et element i et binært tre Et binært tre har et datum og et venstre og et høyre subtre. Et subtre kan være tomt. Hvis begge subtrærne er tomme, har vi et blad. Minimumsabstraksjonen omfatter en konstruktor og tre selektorer. (define (make-BT d L R) (list d L R)) (define (BT-datum T) (car T)) (define (BT-L-sub T) (cadr T)) (define (BT-R-sub T) (caddr T)) 333 (se også forelesning 6) I en funksjonell løsning kan vi plasserer et nytt datum i et binært tre ved å traversere treet, mens vi i hver forgrening lager et helt nytt subtre, til vi enten finner x, eller finner et tom subtre, der vi setter inn x. Funksjonell løsning (define (adjoin-BT x tree) (define (place-x T) ; Returner en ny bladnode med x som datum (cond ((null? T) (make-BT x '() '())) ; Returner en kopi av T med ((< x (BT-datum T)) ; uendret datum, (make-BT (BT-datum T) (place-x (BT-L-sub T)) ; x i venstre subtre, og (BT-R-sub T))) ; uendret høyre subtre ; Returner en kopi av T med ((< (BT-datum T) x) ; uendret datum, (make-BT (BT-datum T) (BT-L-sub T) ; uendret venstre subtre, og (place-x (BT-R-sub T)))) ; x i høyre subtre. ; x er allerede i T. (else T))) (place-x tree)) 334 I en mutative løsning må vi, i tillegg til konstruktoren og selektorene, ha noen mutatorer—f.eks: (define (set-BT-datum! T d) (set-car! T d) T) (define (set-BT-L-sub! T L) (set-car! (cdr T) L) T) (define (set-BT-R-sub! T R) (set-car! (cddr T) R) T) Merk at hver mutator returnerer T etter at T er endret. Dette innebærer, hvis vi tillater at tre-argumentet er tomt, at den kallende prosedyren må sørge for at den aktuelle tre-variabelen tilordnes returverdien. 335 Vi har her en liste av 3-par som holdes sammen av cdr-pekerne i første og andre par, og tre-strukturen er gitt ved car-pekerne i andre og tredje par. Innsettingsalgoritmen kan da se slik ut ; her trenger vi ikke å forutsette at T null. (define (place-x! T) ; returner et nytt tre bestående av én bladnode (cond ((null? T) (make-BT x '() '())) ((< x (BT-datum T)) ; x er i eller skal inn i venstre subtre, så (set-BT-L-sub! T (place-x! (BT-L-sub T)))) ; sett venstre subtree til resultatet av ; at x plasseres i venstre subtre, ; og returner resultatet av den mutative operasjonen. ((> x (BT-datum T)) ; x er i eller skal inn i høyre subtre, så (set-BT-R-sub! T (place-x! (BT-R-sub T)))) ; sett høyre subtree til resultatet av ; at x plasseres i høyre subtre, ; og returner resultatet av den mutative operasjonen. ; x ar allerede I T, så returner T. (else T))) Merk at sluttresultatet her er det samme som med den funksjonelle løsningen. Fordelen med denne destruktive løsningen er at vi ikke bruker mer plass enn det opprinnelige treet plus den nye noden. I den funksjonelle løsningen bruker vi langt mer plass, i det vi hele tiden lager nye subtrær, og siden funksjonen ikke er halerekursiv, vil ingen av disse frigjøres før det første kallet returnerer. 336 Konvertering av en ordnet liste til et binært tre. Anta vi har et binært tre som vi balanserer periodevis i en batch-prosess. Vi kan da først konvertere treet til en ordnet liste (treet er alltid ordnet, om det er balansert eller ikke) og deretter konvertere denne til et balansert tre. (define (enum-tree tree) For konverteringen fra tre til liste kan vi bruke (cond ((null? tree) '()) enum-tree i forlesningsnotat # 4, side 179. ((not (pair? tree)) (list tree)) (else Vi konverterer en ordnet liste til et binært tre, ved (append (enum-tree (car tree)) (enum-tree (cdr tree)))))) suksessive todelinger av listen inntil vi bare har ett element igjen. For hver todeling av listen lager vi et subtre med a c d h j m p s t u w - første element i andre listehalvdel som datum a c d h j - konverteringen av første listehalvdel som venstre subtre og - konverteringen av resten av andre listehalvdel som høyre subtre. 337 m p s t u w m a c d h j p s t u w Figur 21. Konvertering fra ordnet liste til binært tre 338 Ordnet liste binært tre Forskjellen mellom en destruktiv og en funksjonell løsning ligger i den suksessive todelingen, som vist på neste side. (define (ordered-list->BT items) <split-prosedyren> ; se neste side (cond ((null? items) '()) ; Ingenting igjen, så returner det tomme treet. ((null? (cdr items)) (make-BT (car items)'()'())) ; Bare ett element igjen, så returner et blad. (else ; Mer enn ett element igjen. (let ((halves (split (quotient (length items) 2) items '()))) ; Ta tak i halvdelene, og (make-BT ; Returner et tre der (cadr halves) ; første element i andre halvdel er datum, (ordered-list->BT (car halves)) ; konverteringen av første halvdel er venstre subtre, og (ordered-list->BT (cddr halves))))))) ; konverteringen av resten av andre halvdel er høyre subtre. 339 Ordnet liste binært tre. Funksjonell vs destruktiv splitting Funksjonell (define (split leng ; leng= antall elementer som skal legges til første halvdel. first-half ; Vi bygger denne element for element til vi når midten—når leng = 0. rest) ; Resten av inputlista—som tilslutt vil være andre halvdel. (if (zero? leng) ; Hvis midten er nådd: (cons (reverse first-half) rest) ; returner paret av første og andre halvdel. (split (- leng 1) ; og ellers: tell det elementet vi nå legger til første halvdel, Destruktiv (cons (car rest) first-half) ; legg første element i resten av input til første halvdel, (cdr rest)))) ; og pop dette elementet fra input. Her splitter vi listen ved å sette cdr-pekeren til siste par i første halvedel til null, men for å få dette til må vi stoppe på plassen før midten. (define (split! leng curr-item (if (zero? leng) (let ((R (cdr curr-item))) ;leng= antall elementer som skal inn i første halvdel, minus 1. ;Har vi nådd midten, går første halvdel til og med curr-item, ; mens andre halvdel starter etter dette, så vi kan nå (set-cdr! curr-item '()) ;koble andre halvdel fra første*, og (cons items R)) ; return paret av de to halvdelene. (split! (- count 1) (cdr curr-item))));Har ennå ikke nådd midten og fortsetter mens vi teller ned. * I og med denne setningen, har vi klippet den opprinnelige lista i to, slik at den etter at hele treet er satt opp,være delt opp i en mengde ett-elementslister. 340 Quick-sort —Destruktiv løsning En sentralt del i den klassiske quicksortalgoritmen er partisjoneringen. Man velger et partisjoneringskriterium, en pivot-verdi, og lar to indekser løpe mot hverandre til de møtes. Underveis flyttes verdier fra venstre til høyre halvdel og vice versa, slik at når indeksene møtes, så er - alle verdiene til venstre for møtepunktet pivot, og - alle verdiene til høyre for møtepunktet > pivot. Prosessen gjentas i et binært rekursjonstre, - en for den delen som ligger til venstre for møtepunktet, og - en for den delen som ligger til høyre for møtepunktet. Hver enkelt gren terminere når dens del bare inneholder ett element. 341 Quick-sort —Funksjonell løsning Her gjøres partisjoneringen ganske enkelt ved filtrering. (define (qsort seq) (if (null? seq) '() (append (qsort (filter (lambda (x) (<= x (car seq))) (cdr seq))) (list (car seq)) (qsort (filter (lambda (x) (> x (car seq))) (cdr seq)))))) 342 Destruktuktive listeoperasjoner Under bruker vi følgende ikke-destruktive hjelperutine. (define (last‐pair L) (define (iter L) (if (null? (cdr L)) ; for å kunne returnere siste par, må vistoppe ett par før dette, L ; slik at vi kan returnere en peker til det. (iter (cdr L)))) (if (null? L) (error "last‐pair: expected a non‐empty list") (iter L)) 343 Destruktuktiv append: Her kobles listene sammen ved at cdr-pekeren i siste element i den ene settes til første element i den andre. (define (append! L1 L2) (if (null? L1) (error "append!: expected a non‐empty list as first argument") (set‐cdr! (last‐pair L1) L2)) L1) Sammenlign med ikke-destruktiv append (define L1 '(a b c)) (define L2 (append! L1 '(d e f))) L1 ===> (a b c d e f) L1 ===> (a b c) L2 ===> (a b c d e f) L2 ===> (a b c d e f) 344 (define L1 '(a b c)) (define L2 (append L1 '(d e f))) Destruktuktiv mapping: Her avbildes listen på seg selv, ved at den gitte prosedyren anvendes på elementenes car-verdier. (define (map! proc L) (define (iter! L) (if (not (null? L)) (begin (set‐car! L (proc (car L))) ; sett den nye verdien inn i det paret vi har (iter! (cdr L))))) (iter! L) L) 345 Destruktuktiv filtrering: Destruktiv filtrering må bestå i at de elementene i den gitte listen som ikke tilfredstiller den gitte testen, klippes ut av fra argumentlisten, ved at cdr-pekerne til de respektive foregående parene settes til resultatet av filtreringen av den etterfølgende listen. For å få til dette må vi hele tiden teste elementet etter det løpende, og for å få testet første element, må vi da enten lete oss frem til første tilfredstillende lement, eller vi kan cons'e et midlertidig hode på listen, før filtreringen, for så å returnerer (cdr hode), etter filtreringen. 346 (define (filter! test L) ; Denne "klipper ut" utilfredstillende par av listen og (define (iter! M) ; skjøter sammen de gjenværende bitene vha set‐cdr!. (cond ((null? (cdr M))) ; Her er vi ferdig, og alle endringer er gjort ((test (cadr M)) (iter! (cdr M))) ; Dette paret tilfredstiller testen og blir med. (else ; Dette paret tilfredstiller ikke testen, så (set‐cdr! M (cddr M)) (iter! M)))) ; klipp og skjøt ved å flytte cdr til paret etter (let ((M (cons 'head L))) ; For å få testet første element, trenger vi et listehode foran dette. (iter! M) (cdr M))) ; Returner resultatlisten uten listehodet. Legg spesielt merke til hva som skjer når første element forsvinner fra listen. men (define L (list 1 2 3 4 5 6 7 8)) (define L (list 1 2 3 4 5 6 7 8)) (define M (filter! odd? L)) (define M (filter! even? L)) L ==> (1 3 5 7) L ==> (1 2 4 6 8) M ==> (1 3 5 7) M ==> (2 4 6 8) La q peke på andre par i L før filtreringen. Da peker Her peker L på samme par både før og etter q på samme par etter filtreringen, selv om paret ikke filtreringen, og (eq? (cdr L) M) ==> #t. lenger er i L, og (eq? (cdr L) (cdr p)) ==> #t 347 Muterbare listers identitet og likhet — noen truismer - Identiteten til en liste er gitt ved dens lokasjon. - Hvis første par i M er det samme som første par i L, så har L og M samme identitet (define L '(1 2 3 4 5 6 7 8)) (define M L) L ==> (1 2 3 4 5 6 7 8) M ==> (1 2 3 4 5 6 7 8) (eq? L M) ==> #t Fra (eq? L M) følger (eq? (cdr L) (cdr M))osv. Hvis L og M er identiske så er de også like, og det vi gjør med den ene, vil alltid også gjelde den andre. (set-cdr! M '(9 10 11 12 13)) L ==> (1 9 10 11 12 13) M ==> (1 9 10 11 12 13) (eq? L M) ==> #t 348 Følgende gir hverken identitet eller likhet mellom M og L. (define L '(1 2 3 4 5 6 7 8)) (define M (filter! even? L)) L ==> (1 2 4 6 8) M ==> (2 4 6 8) I noen situasjoner ønsker vi å fastholde identiteten til et muterbart objekt. Dette får vi til ved å pakke objektet inn i et prosedyreobjekt. (define (make-M M) (lambda (msg) (cond ((eq? msg 'm) M) ((eq? msg 'f) (lambda (pred) (set! M (filter! pred M)) M)) (else 'whatever)))) (define M (make-M '(1 2 3 4 5 6 7 8))) (define N M) (M 'm) ==> (1 2 3 4 5 6 7 8)) ((M 'f) even?) ==> (2 4 6 8)) (eq? M N) ==> #t 349 Men ovenstående er langt fra trygt. (a) Hvis listeargumentet er en variabel (ikke en literal verdi), vil prosedyreobjektets interne liste være kjent utenfor objektet. (b) Prosedyreobjekte tillater oss å hente ut den interne liste. I begge tilfeller vil eventuelle eksterne endringer også gjøres gjeldende internt. (define L '(1 2 3 4 5 6 7 8)) (define M (make-M L)) (filter! even? L) (M 'm) ==> '(1 2 4 6 8) (define M (make-M '(1 2 3 4 5 6 7 8))) (define L (M 'm)) (filter! even? L) (M 'm) ==> '(1 2 4 6 8) 350 Følgende er tryggere: (define (make-M L) (define M (copy-list L)) (lambda (msg) (cond ((eq? msg 'm) (copy-list M)) ((eq? msg 'f) (lambda (pred) (set! M (filter! pred M)) (copy-list M))) (else 'whatever)))) (a) Vi lar den interne liste M være en kopi av listeargumentet til makeM, og (b) når omverdene spør etter M, sender vi ut en kopi. copy-list ser slik ut: (define (copy-list L) (if (null? L) '() (cons (car L) (copy-list (cdr L))))) 351 Muterbare stacker og køer som prosedyreobjekter ;;Stack (define (make‐stack) (define S '()) (lambda (m) (cond ((eq? m 'stack) S) ((eq? m 'top) (car S)) ((eq? m 'push!) (lambda (item) (set! S (cons item S)))) ((eq? m 'pop!) (set! S (cdr S))) ((eq? m 'in‐stack) (lambda (item) (and (member item S) #t))); Don’t return list (else (error 'STACK "unknown message" m))))) Om vi ønsker filtrering kan vi legge følgende inn i meldingsbehandleren: ((eq? m 'filter!) (lambda (predikat) (set! S (filter! predikat S)))) (define S (make‐stack)) ((S 'push) 1) ((S 'push) 2) ((S 'push) 3) ((S 'push) 4) ((S 'push) 5) ((S 'push) 6) (S 'stack) ==> (1 2 3 4 5 6) ((S 'filter) even?) (S 'stack) ==> (2 4 6) 352 ;;Queue ; A variation on the queue described in SICP 3.3.2 (define (make‐queue) (define Q (cons '() '())) ; The internal representation of the queue is a pair of pointers ; to the front and the rear element in the queue, respectively. (define (front‐ptr) (car Q)) (define (rear‐ptr) (cdr Q)) (define (front) (car (front‐ptr))) (define (set‐front‐ptr! p) (set‐cdr! p) (cdr (front‐ptr))) (set‐car! Q p)) (define (set‐rear‐ptr! p) (if (not (null? p)) (set‐cdr! p (rear‐ptr))) (set‐cdr! Q p)) (define (empty?) (null? (front‐ptr))) (define (pop!) (set‐front‐ptr! (cdr (front‐ptr))) ; Move the front-pointer to the second queue element, if any, (if (empty?) (set‐rear‐ptr! '()))) ; and update the rear-pointer, if the queue became empty. 353 ( make‐queue fortsatt ) (define (append! A) ; A is the list of items to be appended to the queue. (let ((A (copy‐list A)))) ; Don't allow the appended list to be known externally. (if (not (null? A)) ; Only act if there are any items to append. (begin (if (empty?) ; If the queue is empty, (set‐front‐ptr! A) ; let the the appended items become the entire queue. (set‐cdr! (cdr Q) A)) ; Otherwise connect the rear element to the appended (set‐rear‐ptr! (last‐pair A)))))) ; and, in any case, set the rear-ptr to the last appended. (lambda (m) (cond ((eq? m 'queue) (cdr Q)) ((eq? m 'front) (front)) ((eq? m 'pop!) (pop!)) ((eq? m 'append!) append!) (else (error "QUEUE: unknown message" m))))) 354 For en prioritetskø trenger vi et predikat for å sammenligne prioritet og en innsettingsprosedyre, men ikke append!. (define (make‐queue higher‐priority?) ; Pass the priority predicate to the constructor. (define (insert! element) (define (iter! p) (cond ((null? (cdr p)) ; We have searched to the end of the queue, so (set‐rear‐ptr! (list element))) ; append element and update rear pointer ((higher‐priority? element (cadr p)) ; We have found where to put the new element, (set‐cdr! p (cons element (cdr p)))) ; so insert it here. (else (iter! (cdr p))))) ; Keep searching (cond ((empty?) ; The queue is empty so (set‐front‐ptr! (cons element (rear‐ptr)))) ; set new element as the one and only element ((higher‐priority? element (front)) ; The new element has higher priority than any other, so (set‐front‐ptr! (cons element (front‐ptr)))); place it at front. (else (iter! (front‐ptr))))) ; Place element in queue ...) 355 Tids- og plassbesparelse ved destruktiv programmering Fordelene ved en destruktiv i forhold til en funksjonell implementasjon, når vi behandler lister, er bl.a. at vi kan spare plass og tid. I en funksjonell løsning, medfører alle endringer av en liste, bortsett fra når vi cons'er et element på listen, at det allokeres plass til en ny liste. Plassbesvarelsen oppnås ved at vi ved hjelp av set!, set‐car! og set‐cdr!, unngår allokering av en ny liste. Tidsbesparelsen får vi i append-operasjoner der nest siste ledd i the appendee er kjent og ellers ved at arbeidsmengden til garbage collectoren. 356 Tabeller Som vi er kjent med fra oblig 2: - En assossiasjonsliste er en liste av par der car-delen forutsettes å være en nøkkelverdi. - Til denne listetypen hører primitiven assoc som tar en nøkkelverdi og en liste som argument og returnerer det første eventuelle paret i lista der car-delen er lik den gitte nøkkelverdien, eller #f dersom nøkkelverdien ikke ble funnet. (assoc 5 '((3 hei) (5 hallo) (7 yo))) (5 hallo) (assoc 9 '((3 hei) (5 hallo) (7 yo))) #f (assoc 'yo '((hei 3) (hallo 5) (yo 7))) (yo 7) (assoc 5 '(3 5 7)) feilmelding: ikke-par funnet i lista 357 SICP bruker termen tabell om en assossiasjonsliste med hode — som f.eks. denne (define table '(*table* (a . 1) (b . 2) (c . 3))) Figur 26 Fordelen med å bruke et eget hode, er at - listen ikke forsvinner, når vi fjerner eneste gjenværende element , og dessuten - slipper vi å teste for om listen er tom, når vi skal sette noe inn. 358 For å finne en verdi, leter vi først opp det aktuelle paret i tabellens datadel (etter hodet) vha. den assossierte nøkkelen, og returnerer verdien, hvis nøkkelen ble funnet, eller #f. (define (lookup key table) (let ((record (assoc key (cdr table)))) ; let i tabellens datadelen, som ligger etter tabellhodet. (and record (cdr record)))) ; ekvivalent med (if record (cdr record) #f) 359 Ved å konstrukere en assossiasjonsliste av assossiasjonslister får vi en todimensjonal tabell. (define *people* (list (list *table*) (cons 'france (list (cons 'paris 54) (cons 'lyon 33))) (cons 'texas (list (cons 'paris 17) (cons 'houston 120))))) 'france og 'texas er nå hoder til hver sin endimensjonale tabell. (define (find-state state table) (let ((s (assoc state table))) (and s (cdr s)))) (define (find-city state city table) (let ((s (find-state state table))) (and s (let ((c (assoc city (cdr s)))) (and s (cdr s)))))) En grafisk representasjon av tabellen er vist i Figur 27. 360 Følgende uttrykk gir følgende svar fra REPL *people* ((table-head) (france (paris . 54) (lyon . 33)) (texas (paris . 17) (Houston . 120))) (find-state 'france *people*) (france (paris . 54) (lyon . 33)) (find-city 'texas 'paris *people*) (paris . 17) Figur 27 361 Endring av innholdet i en tabell For å legge inn data i tabellen kan vi bruke én enkelt prosedyre insert! som tar som argument en tabell, en stat, en by og et innbyggertall. Finnes både staten og byen i tabellen, settes innbyggertallet til den funne byen til det gitte inbyggertallet. Finnes staten, men ikke byen, legges byen med det gitte inbyggertallet inn i staten. Finnes hverken staten eller byen, legges staten inn i tabellen med den gitte byen med det gitte innbyggertallet som eneste by. 362 Vi legger inn noen nye data. (insert! *people* 'norway 'oslo 21) (insert! *people* 'norway 'bergen 6) (insert! *people* 'texas 'st-paul 12) (insert! *people* 'france 'lyon 29) En utskrift av tabellen viser endringene *people* ((table-head) (norway (oslo . 21) (bergen . 6)) (france (paris . 54) (lyon . 29)) (texas (st-paul 12) (paris . 17) (Houston . 120))) Figur 28 363 En dataabstraksjon for tabeller Vi definere en allmenn tabelltype ved en prosedyre med - tabellen som en lokal tilstandsvariabel, - lokale rutiner for å hente frem og legge inn data, samt (bare enkeltvise entries—ikke hele linjer) - 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!) ((eq? m 'table) (lambda () table)) (else (error "Unknown operation -- TABLE" m)))) dispatch)) Den siste meldingen 'table er lagt inn i demonstrasjonsøyemed, slik at vi kan studere hvordan tabellen endres underveis. 364 Prosedyren lookup må først slå opp linjen med den gitte linjenøkkelen (line-key) i tabellen, og hvis dette gir en faktisk linje (ikke #f), kan prosedyren slå opp kolonnen med den gitte kolonnenøkkelen i den funne linjen, og hvis dette gir en faktisk kolonne (ikke #f), kan prosedyren returnere kolonnens data. I prinsippet kan dette gjøres slik ; merk at kallet (assoc line-key ...) gjøres 3 ganger ; her, (if (assoc line-key (cdr table)) (if (assoc col-key (cdr (assoc line-key (cdr table)))) (cdr (assoc col-key (cdr (assoc line-key (cdr table)))) ; her og ; her #f) #f) men her er det både mer oversiktlig og mer effektivt å bruke lokale hjelpevariabler (define (lookup line-key col-key) (let ((line (assoc line-key (cdr table)))) ; andre arg evalueres bare hvis line #f. (and line (let ((entry (assoc col-key (cdr line)))) (and entry (cdr entry)))))) 365 ; andre arg evalueres bare hvis entry #f. En assosiasjonliste forutsettes ikke å være ordnet (men den kan selvsagt være det), og det enkleste er da å legge inn nye entries først på linjen og nye linjer først i tabellen—som in en stack, i begge tilfeller ved hjelp av cons. (define (insert! line-key col-key value) ; bure vel ha hett insert/update! (let ((line (assoc line-key (cdr table)))) (if line (let ((entry (assoc col-key (cdr line)))) (if entry (set-cdr! entry value) ; fant ikke kolonnenøkkelen, (set-cdr! line (cons (cons col-key value) (cdr line))))) ; så vi legger inn en ny entry ; først på linjen ; fant ikke linjenøkkelen, (set-cdr! table (cons (list line-key ; en ny linje (cons col-key value)) ; med én enkelt entry (cdr table)))))) 'ok) ; først i tabellen ; en rent informativ returverdi. det vesentlige her er effekten: innsettingen av ny entry 366 Vi kan nå bruke den generelle tabellen for å implementere tabellen **people**. I den spesifikke tabellen vi startet med, hadde vi find-state for å få tak i hele linjer, og find-city for å få tak i enkeltvise data. I databstraksjonen tilsvares find-city av lookup, men vi har ingen prosedyre for å hente ut hele linjer. Det hadde imidlertid vært fort gjort å legge inn dette og annen allment interessant funksjonalitet inn i tabellen. Legg dessuten merke til at både innlegging av nytt element og en endring av verdien til et eksisterende element nå gjøres vha. insert!. 367 (define **people** (make-table)) ((**people** 'table)) (table) ((**people** 'insert!) 'france 'paris 54) ((**people** 'insert!) 'france 'lyon 33) ((**people** 'insert!) 'texas 'paris 17) ((**people** 'insert!) 'texas 'Houston 120) ((**people** 'lookup) 'texas 'Houston) ((**people** 'insert!) 'texas 'ts-paul 12) ((**people** 'insert!) 'norway 'oslo 19) ((**people** 'insert!) 'norway 'bergen 3) ((**people** 'table)) (table (norway (bergen . 3) (oslo . 19)) (texas (ts-paul . 12) (Houston . 120) (paris . 17)) (france (lyon . 33) (paris . 54))) ((**people** 'insert!) 'texas 'Houston 87) ((**people** 'table)) (table ... (texas (ts-paul . 12) (Houston . 87) (paris . 17)) ...) 368 Datadrevet programmering System for generiske talloperasjoner på hele, rasjonelle og relle tall. Vi har en todimensjonal tabell med binære operatorprosedyrer der - radindeksene angir operasjonene og - kolonneindeksene angir argumenttypene med tilhørende - selector get - installator put og - applikator apply-generic Den siste tar - en operator-id (symbol) og - et valgfritt antall argumenter (som i prosedyren opptrer som en liste med argumenter), henter frem operatoren fra den tilsvarende plassen i tabellen og anvender operatoren på argumentene. 369 Prosedyretabellen add sub mul div |(integer integer)|(rational rational) |(real real) | | | | | | |(lambda (x y) |(lambda (x y) |(lambda (x y) | (tag (+ x y))) | (tag (add-rat x y)))| (tag (add-real | | | | | | |(lambda (x y) |(lambda (x y) |(lambda (x y) | (tag (- x y))) | (tag (sub-rat x y)))| (tag (sub-real | | | | | | |(lambda (x y) |(lambda (x y) |(lambda (x y) | (tag (* x y))) | (tag (mul-rat x y)))| (tag (mul-real | | | | | | |(lambda (x y) |(lambda (x y) |(lambda (x y) | (tag (/ x y)))| (tag (div-rat x y)))| (tag (div-real | | | Intern tallrepresentasjon For hver talltype definerer vi en pakke bestående av selektorer og operatorer samt prosedyrer for å installere operatorene i prosedyretabellen. 370 x y))) x y))) x y))) x y))) | | | | | | | | | | | | | | | | | | Installasjonsprosedyrene har samme form for alle talltyper. (define (tag x) (attach-tag talltype x)) (put 'add '(talltype1 talltype2) (lambda (x y) (tag (legg-sammen x y)))) (put 'sub '(talltype1 talltype2) (lambda (x y) (tag (trekk-fra x y)))) (put 'mul '(talltype1 talltype2) (lambda (x y) (tag (multipliser x y)))) (put 'div '(talltype1 talltype2) (lambda (x y) (tag (divider x y)))) (put 'make talltype (lambda (argumenter) (tag <konstruktorkall>))) (put 'tag talltype (lambda (x) (tag x))) (put 'base-form talltype (lambda (x) (konverter-til-ett-tall x))) put er standardprosedyren for innlegging av et element i en tabell (se under). attach-tag er definert under. De spesifikke installasjonsprosedyrene for de hele, rasjonelle og reelle tallene er vist sist i notatet. Merk at ingen av disse har prosedyrer for operander av ulike typer, dvs. talltype1 og talltype2 er alltid de samme, —alle operatorprosedyrene er monotype. 371 Tabellen er en meldingsbehandler, laget av prosedyren make-table, med interne metoder for oppslag og innlegging av elementer. (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)) Tabellen opprettes slik (define proc-table (make-table)) men denne variabelen brukes bare av de to prosedyrene put og get. 372 put og get danner grensesnittet mellom tabellen og resten av systemet put tar tabellindeksene og den tilhørende operatoren som argumenter, dvs. - en tag for operasjonstypen - en liste med tagger for operandtypene og - en operatorprosedyre. og legger operatorprosedyrene i tabellen. get tar tabellindeksene som argumenter, dvs. - en operator-tag, og - en liste med operand-tagger, slår opp i den tilsvarende raden og kolonnen i tabellen og returnerer den prosedyren den eventuelt finner, eller #f, ved negativt funn. Når tabellen er opprettet, definerer vi get og put slik: (define get (proc-table 'lookup)) (define put (proc-table 'insert!)) 373 Til systemet hører også: ; Brukes av installasjonsprosedyrene (define (attach-tag type-tag contents) (cons type-tag contents)) Og selektorene (define (type-tag datum) (car datum)) ; Brukes av apply-generic (define (contents datum) (cdr datum)) ; Brukes av apply-generic Systemets sentrale prosedyre er: (define (apply-generic op . args) ; Map ut typetaggene fra operandlisten (let* ((type-tags (map type-tag args)) ; Slå opp i prosedyretabellen med operatoren og ; typetaggene som hhv. rad og kolonnenøkler (proc (get op type-tags))) ; Vi fant en prosedyre i tabellen (if proc ; anvend prosedyren på argumentenes innhold (apply proc (map contents args)) (error "No method for these types ...")))) 374 Som eksempel kaller vi apply-generic for å addere to rasjonelle tall (apply-generic 'add (rational 2 3) (rational 3 4)) Mappingene (map type-pag ((rational 2 3) (rational 3 4))) i let-setningen gir (rational rational) Kallet (get add (rational rational)) i let-setningen gir (lambda (x y) (tag (add-rat x y))) Denne er definert internt i rational-pakken, så vi substituer tag med attach-tag og får (lambda (x y) (attach-tag 'rational (add-rat x y))) (x y)) Mappingene (map contents ((rational 2 3) (rational 3 4))) i kallet på apply gir ((2 3) (3 4)) Kallet (apply proc (map contents args)) gir ((lambda (x y) (attach-tag 'rational (add-rat x y))) ((2 3) (3 4))) (rational 7 6) 375 Nå skal ikke brukerne av systemet selv behøve å kalle apply-generic, så vi generaliserer aritmetikken vha. følgende prosedyrer. (define (add x y) (apply-generic 'add x y)) (define (sub x y) (apply-generic 'sub x y)) (define (mul x y) (apply-generic 'mul x y)) (define (div x y) (apply-generic 'div x y)) Om vi nå antar at vi også har lagt inn i systemet utskriftsmetoder som REPL kan benytte, har vi hatt ett system som for bruker ikke skiller seg fra Schemes eget. Abstraksjonshierarkiet har nå følgende lag ————add————sub————mul————div———— bruker ser bare tall—ingen tag'er ——————————apply-generic—————————— eksplisitt tag'ede operatorer og operander ——————————tabellprosedyre————————— operander strippet for tag'er ——typespesifikk implementasjon av aritmetiske operasjoner— Merk at dette ikke gir rom for literaler. Alleverdier må opptre som eksplisitt konstruerte objekter 376 Installasjonsprosedyrene for de hele, rasjonelle og reelle tallene Heltallene For heltallene bruker vi ganske enkelt Scheme-primitivene for de aritmetiske operasjonene. (define (install-integer-package) <installer prosedyrene +, -, * og /> 'installed-integers) Vi trenger dermed ingen egen konstruktor og nøyer oss med (lambda (x) (tag x)) 377 De rasjonelle tallene Internt bruker vi den representasjon vi kjenner fra før. (define (install-rational-package) (define (numer x) (car x)) (define (denom x) (cdr x)) (define (make-rat n d) (let ((g (gcd n d))) (cons (/ n g) (/ d g)))) (define (add-rat x y) (make-rat (+ (* (numer x) (denom y)) (* (numer y) (denom x))) (* (denom x) (denom y)))) (define (sub-rat x y) (make-rat (- (* (numer x) (denom y)) (* (numer y) (denom x))) (* (denom x) (denom y)))) (define (mul-rat x y) (make-rat (* (numer x) (numer y)) (* (denom x) (denom y)))) (define (div-rat x y) (make-rat (* (numer x) (denom y)) (* (denom x) (numer y)))) <installasjonsprosedyrene> (put 'make 'rational (lambda (n . d) (tag (make-rat n (if (null? d) 1 (car d)))))) 'installed-rationals) 378 De reelle tallene Scientific Notation Et reelt tall i Scientific Notation består av - mantissen eller signifikanden M, - eksponenten E og - basen, eller roten (radix) R. Basen er typisk 10, 2 eller 16, mens eksponenten er en funksjon av størrelsen på det relle tallet (hvor mye større eller mindre enn 0 tallets absoluttverdi er) — nærmere bestemt slik at 1 M < R. For relle tall er antall desimaler uendelig, og poenget med Scientific Notation er at vi lar mantissen være så stor som vi har plass til i f.eks. fire, åtte eller seksten bytes, mens desimalpunktets plassering er bestemt av eksponenten og basen. Av den grunn kalles denne representasjonen også float-point—i motsetning til fixed-point representasjon. For et reelt tall r får vi r = M RE. Anta at mantissen gir plass til 8 desimale sifre, og at basen er 10. For følgende tre tall - lysets hastihet i vakuum 299792458 meter/sekund - 100 - bølgelengden til fargen blå får vi da: = 2.99792458 108 meter/sekund 3.14159265 475 nanometer = = 3.14159265 4.75 10-7 meter En nanometer er en milliarddels meter. 379 I vår representasjon har vi bare med mantiseen og eksponenten, mens vil lar basen være 10. Representasjonen er på ingen måte optimal, men den bidrar til å teste prinsippende for typeblanding. (define (install-real-package) (define (mantissa r) (car r)) (define (exponent r) (cdr r)) (define (fix-form x) (* (mantissa x) (expt 10 (exponent x)))) (define (make-real n . more) ; desimaltall, hvis more er tom, eller mantisse og (eksponent) (define (make-real m . e) (cond ((not (null? e)) (cons m (car e))) ; lag par av m og første (og eneste) element i e ((or (integer? m) (rational? m) (real? m)) ; ett ikke-komplekst tall (let* ((exponent (log-10 m))) (cons (/ (exact->inexact m) (expt 10 exponent)) exponent))) (else (error "Illegal arg." m ", MAKE-REAL")))) (define (add-real x y) (make-real (+ (fix-form x) (fix-form y)))) (define (sub-real x y) (make-real (- (fix-form x) (fix-form y)))) (define (mul-real x y) (make-real (* (mantissa x) (mantissa y) (expt 10 (+ (exponent x) (exponent y)))))) (define (div-real x y) (make-real (* (/ (mantissa x) (mantissa y)) (expt 10 (- (exponent x) (exponent y)))))) <installasjonsprosedyrene> (put 'make 'real 'installed-reals) (lambda (n . more) (tag (make-real n . more)))) 380 Følgende er nokså fritt i forhold til læreboka. Blanding av talltyper Det vi har gjort så langt, gir ingen mulighet for å blande talltyper. Kaller vi add med f.eks. et heltall og et rasjonelt tall for hhv. x og y, får vi en feilmelding. (error "No method for these types ...")))) Det er imidlertid ingenting i veien for å legge inn prosedyrer for blandet aritmetikk i tabellen, men vi må i så fall også ha underliggende prosedyrer for typekonvertering (coercion, casting,). I den forbindelse snakker vi også om promovering, fordi vi i en operasjon på to tall av ulike typer, må løfte tallet med den laveste typen opp til nivået til tallet med den høyeste typen. 381 Skal vi f.eks. regne ut z=xy når x er et heltall og y er et rasjonelt tall, kan vi promovere x til et rasjonelt tall, utføre operasjonen og returnere z som et rasjonelt tall. 5 + 3/7 = 5/1 + 3/7 = 57/71 + 31/71 = (35 + 3)/7 = 38/7. For å få til denne typen operasjoner gjør vi som følger: Vi legger promoveringsprosedyrer i prosedyretabellen med oparsjonene og operandypene som hhv. rad- og kolonneindekser F.eks.vil (rational integer) være en kolonneindeks. Disse vil da være tilgjengelig for apply-generic på linje med de monotype operasjonene. 382 Med promoveringsprosedyrene unngår vi å installere hele pakker for alle mulige blandingspar. En promoveringsprosedyre - løftere den typemessig lavestliggende operanden opp til den høyeste, og - genererer et kall på den aktuelle generiske operatoren—add, sub, mul eller div, med de aktuelle operandene som argumenter, hvorav det ene nå er promovert, slik at de to har samme type. For innleggingen av disse i tabellen bruker vi en generisk prosedyre put-mixed som tar en operand-tag og to type-tag'er som argumenter. Siden add, sub, mul og div er definert vha. apply-generic, vil det kallet som er generert av promoveringsprosedyren, føre til - et nytt kall på apply-generic, - denne gangen med like operand-tager, slik at - apply-generic kan hente den aktuelle - monotype prosedyren fra prosedyretabellen. 383 Figuren viser en kallsekvens som starter med kallet (add (integer 5) (rational 3 . 7)) 384 For implementasjonene må vi legge følgende inn i grensesnittene for alle tallpakkene (put 'tag <type> (lambda (x) (tag x))) og hver av de følgende inn i sine respektive pakker (put 'base-form 'integer (lambda (x) x)) (put 'base-form 'rational (lambda (x) (/ (numer x) (denom x)))) (put 'base-form 'real (lambda (x) (fixed x))) Her har vi forenklet—nærmest jukset—for å få testet systemet i praksis, idet basisformene er rene Scheme-representasjoner som f.eks. ((lambda (x) (/ numer x) (denom y)) ( 3 4)) ¾ ((lambda (x) (* (mantissa x) (expt 10 exponent x))))) (123 -2 10)) 0.0123 385 Og vi trenger en egen generisk blandingspakke. (define (install-mixed-types-package) ; Ret. #t hvis første operand er den laveste av de to* (define (low-first? type-1 type-2) (let* ((types '(integer rational real)) (type-1-tail (member type-1 types)) ; talltypene i stigende orden ; lengre desto lavere operand (type-2-tail (member type-2 types))) (if (and type-1-tail type-2-tail) (> (length type-1-tail) (length type-2-tail)) (error "Unknown types," type-1 type-2 ", LOW-FIRST?")))) ; evaluer tag'en for å få tilsvarende høynivåoperator (define (tag->operator tag) (if (member tag '(add sub mul div) ; Eks: (eval '+) #<primitive:+> (eval tag) (error "Unknown tag " tag "TAG->OPER")))) * Med "høyden" til et tall forstås her den plassen tallet har i talltypehierarkiet, med de komplekse talene øverst og de naturlige tallene nederst 386 (define (put-mixed oper-tag type-1 type-2) ; Legg inn én versjon av denne for hver typekombinasjon (let ((operator (tag->operator oper-tag))) ; En av høynivåoperatorene add, sub, mul og div ; Legg prosedyren (lambda-uttrykket) i tabellen med (put oper-tag ; Radnøkkel = operator tag (list type-1 type-2) ; Kolonnenøkkel = operandtyper (if (lowest-first? type-1 type-2) ; Finn ut hvilken av operandene som skal løftes opp ; x og y har typebestemte, men utag'ede formater (lambda (x y) (operator ; Promover x til y's type, med tag, etter ((get 'make type-2) ((get 'base-form type-1) x)) ((get 'tag type-2) y)) ; først å ha strippet x til S-format ; Tag utag'et y. ; x og y har typebestemte, men utag'ede formater (lambda (x y) (operator ((get 'tag type-1) x) ; Tag utag'et x. ((get 'make type-1) ; Promover y til x's type etter ((get 'base-form type-2) y)))))))) ; først å ha strippet y til Scheme-format Det er det som er rammet inn, som legges i tabellen —den ene eller den andre prosedyren, avhengig av typerangeringen. Merk at begge prosedyrene har variabelen operator i sine omgivelser. 387 ; Kroppen i installasjonsprosedyren ; Map operatortaglisten til tabell-prosedyrer. (map (lambda (o-tag) ; Map de to type-tag-listene til tabell-pros. (map (lambda (x-tag y-tag) (put-mixed o-tag x-tag y-tag) ; med løpende operator-tag som radnøkkel, (put-mixed o-tag y-tag x-tag)) ; og løpende operand-tag'er som kol.nøkkel. '(integer integer rational) '(rational real real ))) '(add sub mul div)) Den indre løkka gir 6 typepar som i den yttre løkka alle kobles med hver av de 4 operatorene ... add ... sub ... mul ... div ... (integer rational) (integer real) (rational real) (rational integer) (real rational) Dermed får vi til 6 nye prosedyrer i hver rad i prosedyretabellen—alle med formen (lambda (x y) (operator (tag operand-1) (tag operand-2))) 388 (real integer) Et eksemple på hvodan prosedyrene low-first? og tag->operator virker: (se side 406) types = (integer rational real) type-1 = real type-2 = rational type-1-tail = (real) type-2-tail = (rational real) (> (length type-1-tail) (length type-1-tail)) #f 'add tag (member 'add '(add sub mul div)) #t #<procedure add> (eval 'add) I omgivelser til den installerte prosedyren er verdien til operator en ferdig beregnet generisk operator. (put-mixed 'add 'real rational) ... (put 'add '(real rational) (lambda (x y) (#<procedure add> ((get 'tag real) x) (make real ((get 'base-form rational) y)))))) 389 Siden add ligger i det ytterste grensesnittet, definert vha. apply generic, (se side 400) gir prosedyrekallet i tabellprosedyren opphav til et nytt kall på appy-generic—og denne gangen har x og y samme type. Her håndgår vi kallesekvensen i figuren over (add '(integer 5) '(rational 3 . 7)) Ved hjelp av radindeksen add og kolonneindeksen (integer rational) henter apply-generic frem den aktuelle anonyme blandingsprosedyren. Denne er definert ved prosedyre-argumentet, lambda-uttrykket, til put i put-mixed, og her gir vi den av bekvemmelighetshensyn navnet add-real-rat. add-real-rat kalles med de samme aktuelle argumenter som add fikk, men uten typetag'er. (add-real-rat (5 . -1) (3 . 7)) ; typetag'ene ble fjernet av apply-generic 390 I den lokale omgivelsen til add-int-rat har vi variablene oper-tag = add, type-1 = real, type-2 = rational, operator = <høynivåprocedyren add>, Hvilket gir (low-first? real rational) #f De aktuelle argumentene sendes nå til <høynivåprocedyren add>, etter følgende behandling arg.1 ((get 'tag 'real) (5 . -1)) (real 5 . -1) arg.2 ((get 'make 'real) ((get 'base-form 'rational) (3 . 7))) (real 4.28571428571 . -1) Dette gir kallsekvensen (add (real 5 . -1) (real 4.28571428571 . -7)) (apply-generic 'add (real 5 . -1) (real 4.28571428571 . -1)) (<add-real-real> (5 . -11) (3 . -1)) (real 2.1428571428571 -1) 391 392 Forelesning 10 Gjennomgåelse av Oblig 2 Breadth First Search ;; BFS ABSTRACTION ;; Unexpanded node (define (lookup‐neighbors nodenavn) (cddr (assoc nodenavn (towns‐and‐roads)))) ;; Queue (define (front‐queue Q) (car Q)) (define (rest‐queue Q) (cdr Q)) (define (init‐queue start) (list (list start #f))) ;; Enqueued node (define (get‐node‐name node) (car node)) (define (get‐pred‐name node) (cadr node)) (define (make‐queued pred node‐name) ; A node in Q and on S is a two-element list containing (list node‐name (get‐node‐name pred))) ; the name of the node and the name of its predecessor. 393 ;; BFS ALGORITHM (define (expand‐node Q S node) ; Get node's neighbors, filter out the ones already on S, (append Q ; and append the rest to Q—each with node as predecessor. (map (lambda (neigh) (make‐queued node neigh)) (filter (lambda (neigh) (not (assoc neigh S))) (lookup‐neighbors (get‐node‐name node)))))) (define (traverse Q S target‐name) (define (iter Q S) (cond ((eq? (get‐node‐name (front‐queue Q)) target‐name) ; We found the target so we (cons (front‐queue Q) S)) ; finish the traversal with the complete stack. ((assoc (get‐node‐name (front‐queue Q)) S) ; This node is already on S, so (iter (rest‐queue Q) S)) ; we proceed to next node in Q. (else ; This node is not on S, so we proceed with (iter (expand‐node (rest‐queue Q) S (front‐queue Q)); the node's neighbors added to Q (cons (front‐queue Q) S))))) ; and the node itself pushed onto S. (iter Q S)) If there were no initial target check we would have needed this clause at the top of the cond-statement: ((null? Q) (error 'TRAVERSE "target not in graph")) 394 (define (retrace S) ; S now begins with target and ends with start. (define (iter S P) (cond ((not (get‐pred‐name (car S))) ; No predecessor means we reached start, (cons (car S) P)) ; so we push start onto P and return P. ((eq? (get‐pred‐name (car P)) (get‐node‐name (car S))); We found pred. of current node, so (iter (cdr S) (cons (car S) P))) ; we continue the path building from here. (else (iter (cdr S) P)))) ; This node is not on the path (map get‐node‐name (iter (cdr S) (list (car S))))) (define (BFS start target) (retrace (traverse (init‐queue start) () target))) I Uniform Cost Search og A* vil traverse retrace i utgangspunktet være identiske med ovenstående, bortsett fra at vi i UCS og A* skal ha med veilengden. 395 396 Uniform Cost Search ;; UCS ABSTRACTION ;; Unexpanded node (define (lookup‐neighbors nodenavn) (cddr (assoc nodenavn (towns‐and‐roads)))) (define (lookup‐distance pred node) (cdr (or (assoc (cons pred node) (distances)) (assoc (cons node pred) (distances))))) ;; Queue —— as in BFS ;; Expanded node (define (get‐node‐name node) (car node)) (define (get‐pred‐name node) (cadr node)) (define (get‐distance‐from‐start node) (caddr node)) (define (make‐queued pred node‐name) ; A node in Q and on S is a three-element list (let ((pred‐name (get‐node‐name pred))) ; containing (list node‐name ; the name of the node pred‐name ; the name of its predecessor (+ (get‐distance‐from‐start pred) ; the distance from start to the predecessor plus (lookup‐distance pred‐name node‐name))))); the distance between the node and its predecessor 397 ;; UCS HEURISTIC (define (higher‐priority? x y) (< (get‐distance‐from‐start x) (get‐distance‐from‐start y))) ;; UCS ALGORITHM (define (enqueue node Q) ; Put node in Q in the positon determined by its priority (cond ((null? Q) (list node)) ; Insert node at the end of Q ((higher‐priority? node (car Q)) (cons node Q)); Insert the node here (else (cons (car Q) (enqueue node (cdr Q)))))) ; Curr. element still has a higher priority than node. (define (expand‐node Q S node) ; Get the node's neighbors, filter out the ones already on S, (define (iter Q neighbors) ; and place each of the remaining in Q acc. to their priority. (if (null? neighbors) ; Every neighbor not in S is now in Q, Q ; so return Q (iter (enqueue (make‐queued node (car neighbors)) Q); Enqueue this neighbor, (cdr neighbors)))) ; and proceed from the next (iter Q (filter (lambda (neigh) (not (assoc neigh S))) (lookup‐neighbors (get‐node‐name node))))) 398 Veilengden Veilengden = (get‐distance‐from‐start <øverste node på stacken, dvs. målnoden>) (define (UCS start target) (let ((S (traverse (init‐queue start) () target))) (cons (retrace S) (get‐distance‐from‐start (car S))))) Hvis avstandene er et desimaltall, kan vi bruke en avrundingsprosedyre for å få pene tall F.eks. denne: Scheme's representasjon av relle tall (define (round‐to‐desimals d x) kan gi evraundingsfeil, slik at f.eks. 13.0 (let ((factor (expt 10 d))) blir 12.999999999999999 (/ (round (* x factor)) factor))) eller 13.000000000000001. Dette fordi relle tall er representert ved et endelig antall bits—f.eks. 32, som i Racket. (define (UCS start target) (let ((S (traverse (init‐queue start) () target)) (road‐length (round‐to‐desimals 2 (get‐distance‐from‐start (car S)))))) (cons (retrace S) road‐length))) 399 400 A* ;; A* ABSTRACTION ;; Coordinates (define (x‐coord coords) (car coords)) (define (y‐coord coords) (cdr coords)) ;; Unexpanded node (define (lookup‐coordinates node) (cadr (assoc node (towns‐and‐roads)))) (define (lookup‐neighbors nodenavn) (cddr (assoc nodenavn (towns‐and‐roads)))) (define (lookup‐distance pred node) <as in UCS>) ;; Expanded node —— as in UCS 401 ;; A* HEURISTIC (define (estimate‐min‐dist node target) (define (square x) (* x x)) (let ((node‐coords (lookup‐coordinates node)) (target‐coords (lookup‐coordinates target))) (sqrt (+ (square (‐ (x‐coord node‐coords) (x‐coord target‐coords))) (square (‐ (y‐coord node‐coords) (y‐coord target‐coords))))))) (define (higher‐priority? x y target) (define (known+estimated‐distance node) (+ (get‐distance‐from‐start node) (estimate‐min‐dist (get‐node‐name node) target))) (< (known+estimated‐distance x) (known+estimated‐distance y))) 402 ;; A* ALGORITHM enqueue og expand er som i UCS, bortsettt fra at begge prosedyrene må ha target som argument. higher‐priority må få koordinatene til target fra enqueue for å kunne estimere avstanden dit, og da må også enqueue få de sammme koordinatene fra expand. (define (enqueue node Q target) (cond ((null? Q) (list node)) ; Insert at end of Q ((higher‐priority? node (car Q) target) (cons node Q)) ; Insert node here (else (cons (car Q) (enqueue node (cdr Q) target))))) ; Priority is still not high enough ((define (expand‐node Q S node target) (define (iter Q neighbors) (if (null? neighbors) ; Every neighbor not in S is now in Q, Q ; so return Q (iter (enqueue (make‐queued node (car neighbors)) Q target) ; Enqueue this neighbor, (cdr neighbors)))) ; and proceed from the next (iter Q (filter (lambda (neigh) (not (assoc neigh S))) (lookup‐neighbors (get‐node‐name node))))) Hva med å legge den estimerte mininumsavstanden til mål inn i alle grafens noder før søket? Hvor mye ville dette ha kostet i forhold til det kanskje å måtte beregne min-avst.flere ganger for samme node? 403 (define (traverse Q S target-name) (define (iter Q S) (cond ((eq? (get-node-name (front-queue Q)) target-name) (cons (front-queue Q) S)) ((assoc (get-node-name (front-queue Q)) S) (iter (rest-queue Q) S)) (else (iter (expand-node (rest-queue Q) S (front-queue Q) target-name) (cons (front-queue Q) S))))) (iter Q S)) 404 For den destruktive implementasjonen av BFS bruker vi de destruktive implementasjonene av stakk og kø samt append! fra forrige forelesning. (define (expand‐node! Q S node) (append! Q <lookup, filter and map as before>)) (define (traverse! Q S target‐name) (define (iter!) (let ((Q‐front (Q 'front))) (cond ((eq? (get‐node‐name Q‐front) target‐name) ; Found target and ((S 'push!) Q‐front)) ; can terminate traversal ((S 'in‐stack?) (get‐node‐name Q‐front)) ; Node already on S, so (Q 'pop!) ; proceed to next node in Q. (iter!)) (else ; This node is not on S, so (Q 'pop!) (expand‐node! Q S Q‐front) ; we add its neighbors to Q ((S 'push!) Q‐front) ; and push the node on S. (iter!)))))) (iter!)) 405 (define (retrace S) ; S now begins with target and ends with start. (define P (make‐stack)) (define (iter!) (let ((S‐top (S 'top))) ; Secure top of S before S is popped (cond ((not (get‐pred‐name S‐top)) ; No predecessor means we reached start, ((P 'push!) S‐top)) ; so push start onto P. ((eq? (get‐pred‐name (P 'top)) (get‐node‐name S‐top)) ; Found pred. of curr. node (S 'pop!) ; Detach top from stack ((P 'push!) S‐top) ; push to stored by let onto path (iter!)) ; and check out the subsequent nodes. (else (S 'pop!) ; This node is not on the path, so just leave it (iter!)))) ; and check out the subsequent nodes. ((P 'push! (S 'top)) (S ' pop!) (iter! S P) For den destruktive implementasjonen av UCS og A* må vi erstatte køens append-prosedyren med en prioritestkøinnsettingsprosedyre. Dette lar vi være en ukeoppgave. 406 For sammenligningens skyld, setter vi opp den funksjonelle og den destruktive løsningen ved siden av hverandre. (define (traverse Q S target‐name) (define (traverse! Q S target‐name) (define (iter Q S) (define (iter!) (let ((Q‐front (front‐queue Q))) (let ((Q‐front (Q 'front))) (cond ((eq? (get‐node‐name Q‐front) target‐name) (cond ((eq? (get‐node‐name Q‐front) target‐name) (cons Q‐front S)) ((S 'push!) Q‐front)) ((assoc (get‐node‐name Q‐front) S) ((S 'in‐stack?) (get‐node‐name Q‐front)) (iter (rest‐queue Q) S)) (Q 'pop!) (iter!)) (else (else (iter (expand‐node (rest‐queue Q) S Q‐front) (Q 'pop!) (cons Q‐front S))))) (expand‐node! Q S Q‐front) ((S 'push!) Q‐front) (iter!)))))) (iter Q S)) (iter!)) 407 (define (retrace! S) (define (retrace S) (define P (make‐stack)) (define (iter S P) (define (iter!) (let ((S‐top (S 'top))) (cond ((not (get‐pred‐name (car S))) (cond ((not (get‐pred‐name S‐top)) (cons (car S) P)) ((P 'push!) S‐top)) ((eq? (get‐pred‐name (car P)) (get‐node‐name (car S))) ((eq? (get‐pred‐name (P 'top)) (get‐node‐name S‐top)) (iter (cdr S) (cons (car S) P))) (S 'pop!) ((P 'push!) S‐top) (iter!)) (else (iter (cdr S) P)))) (else (S 'pop!) (iter!)))) ((P 'push! (S 'top)) (S ' pop!) (iter (cdr S) (list (car S)))) (iter! S P) 408 For "moro" skyld simulere vi et imperativt løkkemønster. Følgende to implementasjonene virker essensielt likt, men det kan være vel verdt å finne ut hvorfor. (define (while test act terminate) (define (while test act terminate) (if (test) (define (iter) (begin (act) (while test act terminate)) (if (test) (terminate))) (begin (act) (iter)) (terminate))) (iter)) For å få dette til å virke, må vi pakke inn argumentene til while i lambda-uttrykk. F.eks. slik: (define (fac n) (let ((i 1) (f 1)) (while (lambda () (< i n)) (lambda () (set! i (+ i 1)) (set! f (* f i))) (lambda () f)))) Her sendes den beregnede verdien fac i retur, men vi kan godt også/eller bruke terminate-delen til andre formål, og verdien til variabelen fac vil uansett være korrekt når løkken terminerer. 409 Vi legger traverseringen og tilbakesporing i to løkker i prosedyren BFS. Traverseringen opererer på Q og S, mens tilbakesporingen opererer på S og P. Siden operasjonen push, pop og expand‐node! endrer de aktuelle strukturene umiddelbart, må vi passe på hvilken rekkefølge operasjonene utføres i, og ta tak i verdier som i neste omgang vil være forsvunnet. Dette i kontrast til når vi lar de relevante variabler og strukturer oppdateres ved evalueringen av argumentene til rekursive kall. (define (BFS start target) (define Q (make‐queue (list start #f))) (define S (make‐stack)) (define P (make‐stack)) 410 ;; Traverse (while (lambda () (not (eq? (get‐node‐name (Q' front)) target))) ; test (lambda () ; act (let ((node (Q 'front))) ; catch the front node (Q 'pop) ; before we pop it from Q (if (not ((S 'in‐stack?) (get‐node‐name node))) (begin ((S 'push) node) (expand‐node! Q S node))))) (lambda () ((S 'push) (Q 'front)))) ; terminate ((P 'push) (S 'top)) ;; Backtrack (while (lambda () (get‐pred‐name (S 'top))) ; test (lambda () ; act (if (eq? (get‐pred‐name (P 'top)) (get‐node‐name (S 'top))) ((P 'push) (S 'top))) (S 'pop)) (lambda () ((P 'push) (S 'top)))) ; terminate (map! get‐node‐name (P 'list))) 411 Hvorfor virker dette, (define (fac n) (let ((i 1) (f 1)) (while (lambda () (< i n)) (lambda () (set! i (+ i 1)) (set! f (* f i))) (lambda () f)))) når while er definert slik? (define (while test act terminate) (define (iter) (if (test) (begin (act) (iter)) (terminate))) (iter)) test, act og terminate er alle prosedyrer, og som sådan har de med seg omgivelsene der de ble laget. Den innerste omgivelsen er prosedyren fac med variablene n, i og f, så når test, act og terminate utføres, er det verdiene til disse variablene som sjekkes og endres. 412 INF2810: Obligatorisk oppgave 3 våren 2010 Et spill med en tenker, T, en gjetter, G, og en dommer, R (for referee). Det tenkes og gjettes på bits, dvs. tall i mengden {0, 1}. T er programmets bruker, representert ved et prosedyreobjekt som bl.a. leser brukerens tanker7, mens G og R er prosedyreobjekter—kontrollert helt og holdent av programmet . For hver runde rapporterer T og G til R hva de henholdsvis tenker og gjetter. R sammenligner tallene, og hvis det G gjettet = det T tenkte, får G et poeng, og hvis ikke, får T et poeng. G fører statistikk over T sine tanker 7 Ikke en tankeleserprosedyre, vel å merke, men en prosedyre som leser det bruker taster inn. 413 ved hjelp av en mengde objekter, S, med ett objekt for hver mulig tankerekke av en gitt lengde m. Hvis m = 3 får vi 8 ulike tankerekker fra 000 til 111, hvis m = 4 får vi 16 ulike tankerekker fra 0000 til 1111, og generelt får vi 2m ulike tankerekker à m tanker. Vi kaller tankerekkene objektene i S tilstander, og tilstandsobjekter. 414 Hvert tilstandsobjekt inneholder én peker til hvert av objektene for de to mulige etterfølgende tilstandene. F.eks. har tilstanden 1011 etterfølgerne 0110 og 0111. Merk at tallene på objektene er rene merkelapper og ikke sier noe om objektenes innhold. Siden en tenkerekke i S har en begrenset lengde (i dette tilfellet 4) faller venstre bit ut og et nytt høyre-bit kommer inn, når vi går fra én tilstand til neste, og siden det er to mulige høyre-bits, har hver tilstand to mulige etterfølger-tilstander. 415 I tillegg har hvert tilstandsobjekt en vekt, dvs. et tall for hvor mange ganger tilstanden har forekommet. gjetteren kjenner til enhver tid løpende tilstand og gjetter alltid på det bit'et som fører til den tyngste, dvs. oftest forekommende, Hva som faktisk blir neste tilstand, fra den ene runden til den neste, er imidlertid bestemt av hva tenkeren tenker. F.eks. om løpende tilstand = 1011, og 0110 har forekommet oftere enn 0111, slik at gjetteren gjetter 0, hvis tenkeren tenker 1, blir neste tilstand 0111. av de to etterfølgertilstandene. De to øverste linjene viser tanker og gjetninger, mens de to nederste viser akkumulerte poeng. Som vi ser, leder G med 7 poeng etter 25 runder. 416 Vi har her har latt G starte i tilstand 000—som om T’s tanker idet spillet startet, var 0 0 0. Alternativt kunne vi ha bestemt utgangstilstanden vha. en random–funksjon. Tenkeren skal være et prosedyreobjekt med variablene name 'Thinker. score initielt = 0, ; tenkerens poeng og håndtere følgende meldinger på følgende måter: name Returner tenkerens navn. score Returner tenkerens poeng. think Be om og les et bit fra bruker og returner det leste bit'et. add-point! Øk tenkerens poeng med 1. 417 Gjetteren skal være et prosedyreobjekt med variablene name 'Guesser. score initielt = 0. state løpende tilstand i tilstandsmengden—i utgangspunktet første tilstand i den ; gjetterens poeng tilstandsmengden som returneres av konstruktoren make-states-set og håndtere følgende meldinger på følgende måter: name Returner gjetterens navn. score Returner gjetterens poeng. remember! Returner en prosedyre som tar sist tenkte tanke som argument. - endrer løpende tilstand til neste i henhold til den gitte tanken og - gir beskjed til den nye løpende tilstandens om å øke sin vekt med 1. guess Returner den mest sannsynlige av de to mulige neste tankene i henhold til vektfordelingen mellom de to nestetilstandene i forhold til løpende tilstand. add-point! Øk gjetterens poeng med 1. 418 Vi tar meldingene remember! og guess én gang til: remember! Returnér en prosedyre som - tar sist tenkte tanke som argument, - endrer løpende tilstand til neste i henhold til den gitte tanken (argumentet), og - gir beskjed til den nye løpende tilstanden om å øke sin vekt med 1. guess Returnér den mest sannsynlige av de to mulige neste-tankene — et bit — i henhold til vektfordelingen mellom de to tilstandene som følger etter løpende tilstand, dvs. det bit’et som gir den av de to tilstandene som har forekommet oftest, så langt. 419 En tilstand skal være et prosedyreobjekt med variablene weight initielt = 0, next-0 initielt = #f. next-1 initielt = #f. og håndtere følgende meldinger på følgende måter: select-next Returner en prosedyre som tar en tanke (0 eller 1) som argument og returnerer den tilsvarende etterfølgertilstanden. heaviest-next Returner det bit'et som gir den av tilstandens to etterfølgertilstander med høyest vekt (merk at returverdien er et bit, ikke et tilstandsobjekt). increase-weight! Øk tilstandens vekt med 1. weight Returner tilstandens vekt. set-pointers! Returner en prosedyre som tar to nestepekere som argumenter og tilordner disse de henholdsvise interne variablene next-0 og next-1. 420 Dommeren skal ha variablene thinker <argument til konstruktoren> guesser <argument til konstruktoren> decisive-lead <argument til konstruktoren> ; vinneravstanden og styre spillet ved hjelp av den lokale prosedyren play som tar argumentene round - en rundeteller, thought - sist tenkte tall og guess - sist gjettede tall. Argumentene til play oppdateres for hver runde i spillet ved at - round økes med 1, - meldingen 'think sendes til thinker, som returnerer en tanke, og - melding 'guess sendes til guesser, som returnerer en gjetning. 421 Dommerprosedyren play utfører følgende: - sjekker om gjetningen er lik tanken, - sender meldingen add-point! til den spilleren som vant runden, - skriver en stillingsrapport til skjermen, - sjekker om differansen mellom spillernes poeng er lik vinneravstanden og - utroper en vinner eller - lar spillet fortsette, ved å kalle seg selv med oppdateringene av round, thought og guess som argumenter. 422 Tilstandsmengden skal representeres i en graf, som skal settes opp på to ulike måter, i to ulike deloppgaver. Grafen skal inneholde så mange tilstander som minnelengden tilsier, lenket sammen via pekere. For konstruksjon av tilstandsmengden brukes den globale prosedyren make-states-set som oppretter og strukturerer tilstandsmengden i henhold til den aktuelle algoritmen (se under) og returnerer (en peker til) første objekt i tilstandsmengden. 423 NB! I denne grafen er forbindelsene mellom objektene gitt direkte som pekervariabler, ikke indirekte via assossiasjonslister slik som i oblig 2. 1. Konstruksjon av tilstandsmengden vha av en liste. Først lager vi en temporær liste med tilstandsobjekter. Figur 3. Deretter løper vi gjennom listen og setter nestepekerne til tilstandsobjektene på plass ved hjelp av nedenstående beregninger. Figur 4. 424 Referansen til et tilstandsobjekt i den temporære listen er tilstanden tolket som et tall slik at f.eks. tilstanden 101 har listereferansen 1012 = 510 (101 binært = 5 desimalt). Gitt en tilstand s og en tanke t, så er den tilsvarende etterfølgeren = 2s mod 2m + t. Her gir vi først plass til t på slutten av tankerekken Regnestykket 2s mod 2m gir det vi kaller bit-forskyvning mot venstre eller, noe kortere, og på engelsk, bit-shift-left. Scheme-koden for dette er i skallet under navnet BSL. ved å gange s med med 2 (= 102), for så å fjerne den tanken det ikke lenger er plass til, fra begynnelsen av tankerekken, ved å ta resten etter å ha delt resultatet på 2m. F.eks. er 102 × 1012 = 10102, 10102 mod 10002 = 0102 og 0102 + 12 = 0112. Desimalt: 2 × 5 = 10, 10 mod 8 = 2, 2 + 1 = 3. 1 0 1 1 0 1 0 0 1 0 425 0 1 1 2. Konstruksjon av tilstandsmengden ved binære forgreninger. I eksemplet under har vi en 4-bits hukommelse. Vi setter opp tilstandsmengden som et binært tree, Ser vi på venstre og høyrepekerne som bit'ene 0 og 1 med objektet for tilstand 0000, som rot, slik at finner vi f.eks. tilstand 0100 ved å gå roten har seg selv som sitt venstre subtre. først til venstre—tilbake til roten selv, så én gang til høyre og så to ganger til venstre. Figur 5. 426 Treet gir selv pekerne til etterfølgertilstandene for alle objektene over bladnivået. For å finne etterfølgertilstandene til bladene, lager vi først én liste med bits for hvert blad, (merk at vi ikke opererer med tall her) tilsvarende veien fra roten til bladet. Vi gjør dette i en rekursiv prosedyre med binære forgreninger, der bitlistene bygges underveis. Gitt et blad b med veien v fra roten til b, gir Figur 4. append (cdr v) 0) og (append (cdr v) 1). veiene fra roten til de to etterfølgerne til b 427 Her har merkelappene desimale tall. Legg merke til det mønstret enumereringen følger: Her ser vi også regnestykkene som binder tallene sammen: 428 Forelesning 11 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)) 429 (define (lookup line-key col-key) (let ((line (assoc line-key (cdr table)))) ; andre term evalueres bare hvis line #f. (and line (let ((entry (assoc col-key (cdr line)))) ; andre term evalueres bare hvis entry #f. (and entry (cdr entry)))))) (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) ; fant ikke kolonnenøkkelen, (set-cdr! line (cons (cons col-key value) (cdr line))))) ; så vi legger inn en ny entry ; først på linjen ; fant ikke linjenøkkelen, (set-cdr! table ; en ny linje (cons (list line-key (cons col-key value)) ; med én enkelt entry ; først i tabellen (cdr table)))))))) 430 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 431 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 får vi (cons 'computed known-result) (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. 432 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. 433 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. ; (if record (cdr record) #f) (and record (cdr record)))) (define (insert! key value table) (let ((record (assoc key (cdr table)))) ; Fant vi en tidligere utregning av denne (if record (set-cdr! record value) ; Erstatt tidligere med aktuell verdi * (set-cdr! table ; Ingen record her, så ; sett inn ny med aktuell verdi (cons (cons key value) (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. 434 En memoiseringsprosedyre for en hvilken som helst ett-arguments-prosedyre f kan skrives slik: (define (memoize f ) (let ((table (make-table))) ; Det er resultatene av eventuelle kall på f som skal memoiseres. ; Vi lager en omgivelse der både tabellen og f inngår. (lambda (x) (let ((result (lookup x table))) (or result (let ((result (f x))) ; Enten fant vi et tidligere utregnet resultat, og returnerer dette, * ; 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). 435 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 ; fib = f i memoize (lambda (n) (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. 436 437 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. 438 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. 439 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 (pakk-ut (lambda () (+ 2 3))) 5 (pakk-ut (+ 2 3)) 5 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. (define pakket-sum (lambda () (+ 2 3))) pakket-sum #<procedure:pakket-sum> (pakket-sum) 5 (pakk-ut pakket-sum) 5 440 (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) ; ==> /: division by zero (test a (/ 1 a)) 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 441 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.) 442 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). 443 Men nå finnes det en spesialform for innpakking i Scheme, nemlig En annen utsett-fremtving-relasjon delay. finner vi mellom quote og eval. Assossiert til denne er metaforparet (eval (quote (+ 2 3))) 5 løftegivning og -avtvinging. (eval '(+ 2 3)) 5 Vi sier at resultatet av en utsatt—delayed—evaluering, er et løfte om en fremtidig evaluering, og at Det er altså klare likheter mellom relasjonene dette løftet, om nødvendig, kan quote — eval, lambda — kall og avtvinges. delay (define løfte (delay (+ 2 3))) — force, men det er også viktige forskjeller løfte #<struct:promise> mellom dem. (promise? løfte) #t (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. 444 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) Her vises utskriften fra REPL. yo5 (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. 445 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))) 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. 446 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. 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. 447 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))))) 448 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)))))) 449 (14) 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) 450 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. 451 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. 452 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). 453 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. 454 Vi definerer formen hverken-eller (define-syntax hverken-eller ; Navnet på spesialformen (syntax-rules () ; Intet reservert ord ; Mønstret som skal gjenkjennes ((hverken-eller test1 test2) ; Malen (kroppen) som skal utføres (not (or test1 test2))))) (hverken-eller (= (+ 2 2) 4) (= (+ 2 2) 4)) ==> #f begge er sanne (hverken-eller (= (+ 2 2) 4) (= (+ 2 2) 5)) ==> #f det første er sant og det andre er usant (hverken-eller (= (+ 2 2) 3) (= (+ 2 2) 4)) ==> #f det første er usant og det andre er sant (hverken-eller (= (+ 2 2) 3) (= (+ 2 2) 5)) ==> #t 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. 455 Følgende definisjon av en eksklusiv eller, gir ingen besparelse: ; Navnet på spesialformen (define-syntax enten-eller ; Intet reservert ord (syntax-rules () ((enten-eller test1 test2) (or (and test1 (not test2)) ; Mønstret som skal gjenkjennes ; 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. 456 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). ; Navnet på spesialformen (define-syntax hverken ; Reservert ord (syntax-rules (eller) ((hverken test1 eller test2) (not (or test1 test2))))) (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. 457 ; Mønstret som skal gjenkjennes ; Malen (kroppen) som skal utføres Definisjonen av cons-stream følger samme mønster. ; Navnet på spesialformen (define-syntax cons-stream ; Ingen andre reservert ord (syntax-rules () ((cons-stream obj stream) (cons obj (delay stream))))) ; Mønstret som skal gjenkjennes ; 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') ; (13') (define (tell fra) (cons-stream fra (tell (+ fra 1)))) (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)))))) 458 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))))) 459 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 p (force (cdr p)) p Et avtvunget løfte er fortsatt et løfte. 460 1 (delay 2))) ==> (1 . #<promise:xxx>) ==> 2 ==> (1 . #<promise:2>) Neste gang prosedyren kalles, returneres ganske enkelt det tildligere beregnede resulatet. (define (memo-proc lambda-wrapped-expression) (let ((already-run? #f) (result #f)) ; begge disse verdiene gjelder bare frem til første kall, ; 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 (begin (set! already-run? #t) (set! result (lambda-wrapped-expression))); utfør det innpakede uttrykket result))) Merk forskjellen mellom - 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. 461 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! 462 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)) 463 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) 464 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. 465 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. 466 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. 467 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. 468 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. I læreboken startes det på 1 000, men vi starter før, for å kunne kjenne igjen primtallene. 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)))) 469 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. 470 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. 471 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. 472 Forelesning 11 Memoisering , utsatt evaluering og strømmer Først litt repetisjon: Utsatt evaluering Gitt (define (p x) (if test (x) something-else)) la E være et Scheme-uttrykk, og la L = (lambda () E). Da vil, ved kallet (p L), L bli evaluert uten forbehold, mens E bare blir evaluert hvis test #t. Spesialformene if, cond, and og or evalueres etter normal orden, som i praksis vil si at "argumentene" til disse formene gjøres til gjenstand for utsatt evaluering. 473 Memoisering: Tabellisering: Vi pakker inn en prosedyre p i en prosedyre q som har samme aritet som p og (ariteten til p er det antall argumenter p tar) en resultattabell i sin lokale omgivelse. la A være et sett med aktuelle argumenter til q. Ved første kall (q A) vil ikke tabellen ha noe entry med A som nøkkel, og dermed utføres kallet(p A), og resultatet lagres i tabellen med A som oppslagsnøkkel, før det returneres. Ved alle etterfølgende kall (q A) finner vi det tidligere beregnede resultatet i tabellen ved oppslag på A, og vi returnerer dette. 474 Hvis ariteten til p = 0 bruker vi en enkelt variabel r for resultatet, (vi bruker ikke en 0-dimensjonal tabell) men siden r alltid vil være der, og verdien #f kunne være et mulig resultat, trenger vi i tillegg et flagg som heises når resultatet beregnes. Utsatt evaluering vha. lambda Her består argumentet til q , her kalt memo-proc, i et argumentløst lambda-uttrykk med det aktuelle uttrykket, f.eks. kallet på p, som kropp. (define (memo-proc lambda-wrapped-expression) (let ((already-run? #f) (result #f)) ; begge disse verdiene gjelder ; bare frem til første kall (lambda () (if already-run? result (begin (set! already-run? #t) (set! result (lambda-wrapped-expression))); utfør det innpakede uttrykket result))) 475 Utsatt evaluering vha. spesialformen delay. Her skjer noe tilsvarende som beskrevet over, bortsett fra at spesialformen delay tar imot selve uttrykket , f.eks. et kall på p, uten lambda-innpakning. Vi kaller returverdien fra delay et løfte, og det som loves er en evaluering av uttrykket, om det skulle kreves. Vi kunne ha definert vår egen delay, slik: (define-syntax delay (syntax-rules () ((delay expression) (memo-proc ; se under (lambda () expression))))) Men merk at løfte er noe annet enn en prosedyre, hvilket vil si at returverdiene fra den forhåndsdefinerte delay og vår egen versjon av delay er av ulike typer. For å få innfridd et løfte, må vi bruke prosedyren force. Vi kan ikke ganske enkelt kalle det. 476 Strømmer cons-stream er en spesialform definert vha. delay. (define-syntax cons-stream (syntax-rules () ((cons-stream obj stream) (cons obj (delay stream))))) cons-stream tar to argumenter x og y og returnerer paret med ferdig evaluert x i car-delen og et løfte om evalueringen av y i cdr-delen. Parets car-del aksesseres ved selektoren car, men mht. strømabstraksjonen, gir vi selektoren et eget navn: stream-car. Parets cdr-del aksesseres ved løfteavkrevingsprosedyren force, men mht. strømabstraksjonen, gir vi selektoren et eget navn: stream-cdr. 477 Strømmen av heltall (define (int-stream n) (cons-stream n (int-stream (+ n 1)))) (define integers (int-stream 1)) 1 1 2 1 2 3 4 5 1 2 3 4 1 2 3 1 2 3 4 5 6 1 2 3 4 5 6 7 <- n <<<<<<<- 1 2 3 4 5 6 7 promises of n + 1 2 3 4 ... 3 4 5 ... 4 5 6 ... 5 6 7 ... 6 7 8 ... 7 8 9 ... 8 9 10 ... Strømmen av primtall (define (prime-stream S) (if (prime? (stream-car S) (cons-stream (stream-car S)) (prime-stream (stream-cdr S)) (prime-stream (stream-cdr S)))) (define primes (prime-stream integers) 2 2 2 3 2 3 5 3 5 7 478 <- prime? 1 <2 <3 4 <5 6 7 promises of integers 2 3 4 ... 3 4 5 ... 4 5 6 ... 5 6 7 ... 6 7 8 ... 7 8 9 ... 8 9 10 ... Strømmen av kvadrerte primtall (define (square-stream S) (cons-stream (square (stream-car S)) (square-stream (stream-cdr S)) (square-stream primes) <- square 4 promises of primes 4 <- 2 3 5 7 ... 4 9 <- 3 5 7 11 ... 4 9 25 <- 5 7 11 13 ... 4 9 25 49 <- 7 11 13 17 ... 4 9 25 49 121 <- 11 13 17 19 ... 4 9 25 49 121 169 <- 13 17 19 23 ... 9 25 49 121 169 289 <- 17 19 23 29 ... 479 (square-stream (prime-stream (int-stream 1))) <- square <- prime? <- int 4 4 4 4 4 4 9 9 25 9 25 49 9 25 49 121 9 25 49 121 169 promises of n + 1 1 <- 1 2 3 4 ... 4 <- 2 <- 2 <- 2 3 4 5 ... 9 <- 3 <- 3 <- 3 4 5 6 ... 4 <- 4 5 6 7 ... 5 <- 5 6 7 8 ... 6 <- 6 7 8 9 ... 7 <- 7 8 9 10 ... 8 <- 8 9 10 11 ... 9 <- 9 10 11 12 ... 10 <- 10 11 12 13 ... 11 <- 11 12 13 14 ... 12 <- 12 13 14 15 ... 13 <- 13 14 15 16 ... 14 <- 14 15 16 17 ... 15 <- 15 16 17 18 ... 16 <- 16 17 18 19 ... 17 <- 17 18 19 20 ... 25 49 121 169 289 <- 5 <- <<- <- 7 11 13 17 480 <<- <<- <- Typiske listeoperasjoner overført på strømmer Til dataabstraksjonen for strømmer hører: (define stream-nil '()) (stream-null? stream) (cons-stream ; null?. Returnerer #t hvis strømmen er tom. første-element resten) (stream-car strøm) ; returnerer første element umiddelbart (stream-cdr strøm) ; fremtvinger produksjonen av neste element fra strømmen Ved hjelp av disse kan vi definer strøm-utgaver av de fleste standardoperasjoner for lister. (define (stream-ref S n) (if (= n 0) (stream-car S) (stream-ref (stream-cdr S) ( - n 1)))) (define (stream-map proc S) (if (stream-null? S) stream-nil (cons-stream (proc (stream-car S)) (stream-map proc (stream-cdr S))))) 481 (define (stream-filter pred S) (cond ((stream-null? S) stream-nil) ((pred (stream-car S)) (cons-stream (stream-car S) (stream-filter pred (stream-cdr S)))) (else (stream-filter pred (stream-cdr S))))) stream-map og stream-filter skal selv produserer strømmer, enten den tomme strømmen eller et strøm-par (med direkte tilgjengelig car-del og et løfte i cdr-delen) enten ved bruk av cons-stream, eller ved kall på en strømgenererende prosedyre. Dette gjelder også lignende prosedyrer som vi definerer selv. Her skal alle konsekventer og alternativer i en prosedyre med flere mulige utfall, angitt ved if-, cond-, and- og or-uttrykk, returnere strømmer (nokså selvfølgelig, men allikevel mulig å overse). 482 Følgende prosedyre produserer ingen strøm, men en serie av hendelser: (define (stream-for-each proc S n) (if (or (stream-null? S) (zero? n)) 'done (begin (proc (stream-car S)) (stream-for-each proc (stream-cdr S) (- n 1))))) For å kontrollere at vi har satt opp den ene eller den andre strømmen riktig, kan det være greit å ha en rutine som denne. (define (stream->list S n) (if (or (stream-null? S) (zero? n)) '() (cons (stream-car S) (stream->list (stream-cdr S) (- n 1))))) NB! Langt de fleste av de strømmene vi skal se på, er uendelige, noe som vil gitt en evig løkke dersom stream->for-each og stream->list ikke hadde hatt en teller (her n). 483 Her er enda et par nyttige rutiner: (ikke i læreboka) list->stream gjør, som navnet tilsier, det motsatte av stream->list. (define (list->stream L) (if (null? L) stream-nil (cons-stream (car L) (list->stream (cdr L))))) stream-tail gjør nesten det samme som stream-ref, bortsette fra at den returnerer første par, snarere enn første verdi, i en gitt avstand fra begynnelsen av strømmen, (define (stream->tail S n) (if (= n 0) S (stream-tail (stream-cdr S) (- n 1)))) og i og med at returnerer et par, returnerer den også resten av lista—halen—fra og med n'te par. 484 Uendelige strømmer (egentlige er dette det generelle, mens endelige strømmer er, som sådanne, spesielle) Enumererte tall (define (integers-from n) (cons-stream n (integers-from (+ n 1)))) (define integers (integers-from 1)) Fibonacci-tall Fibonaccifunksjonen (define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) Fibonaccistrømmen (define (fibgen this next) (cons-stream this (fibgen next (+ this next)))) (define fibs (fibgen 0 1)) (stream->list fibs 10) (0 1 1 2 3 5 8 13 21 34) 485 Når fibonaccifunksjonen implementeres i en prosedyre, må vi sjekke for basistilfellet, og i den iterative varianten må vi, for å få med basistilfellet, hele tiden beregne leddet etter det vi skal returnere. (define (fib-iter this prev i) (if (= i 0) prev (fib-iter (+ this prev) this (- i 1)))) (map (lambda (n) (fib-iter 1 0 n)) ’(0 1 2 3 4 5)) 0 1 1 2 3 5 | utregnet 3 + 5 prev this I strøm-varianten trenger vi ikke å sjekke forbasistilfellet, eller mer presist: vi starter med basistilfellet, i første kall på strømgeneratoren, og deretter utvikles strømmen idet vi aksesserer dens enkelte ledd, uten sjekk for noe termineringskriterium. (define (fibgen this next) (cons-stream this (fibgen next (+ this next)))) (stream->list 0 1 1 2 3 5 | utsatt 3 + 5 this next (fibgen 0 1) 5) 486 Tallrekker med ut-filterte faktorer La oss se på en strøm som filtrerer ut fra heltallsstrømmen alle tall som inneholder en gitt faktor. Vi trenger da et delelighetspredikat. (define (divisible? x y) (= (remainder x y) 0)) For å se hvordan det tar seg ut, lager vi først en spesifikk strøm med alle tall som ikke har 7 som faktor. (define no-sevens (stream-filter (lambda (x) (not (divisible? x 7))) integers)) (inf-stream->list no-sevens 19) (1 2 3 4 5 6 8 9 10 11 12 13 15 16 17 18 19 20 22) Her er 7, 14 og 21 ikke med. 487 Så generaliserer vi dette til en heltallsstrøm, der vi lar faktoren være en variabel. (define (no-factor-f f) (stream-filter (lambda (x) (not (divisible? x f))) integers)) (define no-sevens (no-factor-f 7)) Vi kan generalisere ytterligere slik at generatoren, i stedet for den spesifikke strømmen integers, også tar matestrømmen som argument. (define (no-factor-f f int-stream) (stream-filter (lambda (x) (not (divisible? x f))) int-stream)) (define no-sevens (no-factor-f 7 integers)) (define no-threes-or-fives (no-factor-f 5 (no-factor-f 3 integers))) (stream->list no-threes-or-fives 10) (1 2 4 7 8 11 13 14 16 17) 488 Om vi nå sier at ingen tall i strømmen skal ha som faktor noe tidligere tall fra strømmen får vi følgende prosedyre (define (unique-factors S) (cons-stream (stream-car S) ; Vi har nå brukt faktoren (stream-car S) og ønsker ikke å se mer til den, (unique-factors (no-factor-f (stream-car S) ; så vi fjerner den fra (stream-cdr S)))))) ; den etterfølgende strømmen Det prinsippet vi her har implementert, er kjent som Eratosthenes' sil. - Om vi, gitt heltallsstrømmmen fra og med 2, - først filtrerer bort alle tall etter 2 som er delelig med 2 og - deretter filtrerer bort alle tall etter det første gjenværende som er delelig med dette, osv, så står vi igjen med strømmen av de tall som ikke er delelig med noen av sine forgjengerealtså primtallene. 489 Lærebokas versjon ser slik ut: (define (sieve S) ; Første gjenværende, her kalt p (cons-stream (stream-car S) ; Filtrer ut (sieve (stream-filter ; de tall som har (lambda (x) (not (divisible? x (stream-car S)))) ; p som faktor (stream-cdr S))))) (define primes (sieve (stream-cdr integers))) 490 ; fra de gjenværende tallene etter p. Implisitte strømmer Forskjellen mellom implisitte og eksplisitte strømmer kan illustreres ved følgende: (define (gjenta) (cons-stream 'jada (gjenta))) ; eksplisitt (define repetér (cons-stream 'jada repetér)) ; implisitt Disse oppfører seg helt likt, bortsett fra at den første kalles, mens den andre bare nevnes. (car (gjenta)) (car (stream-cdr (gjenta))) (car (stream-cdr (stream-cdr (gjenta)))) (stream->list (gjenta) 3) jada jada jada (jada jada jada) (car repetér) (car (stream-cdr repetér)) (car (stream-cdr (stream-cdr repetér))) (stream->list repetér 3) jada jada jada (jada jada jada) 491 Parvis summering av to strømmer Vi bruker den eksplisitte formen når vi ønsker noe mer enn rene gjentagelser, men det betyr ikke at implisitte strømmer er trivielle. Her er en tilsynelatende triviell implisitt strøm: (define enere (cons-stream 1 enere)) ; produserer 1 1 1 1 ... Denne kan vi kombinere med følgende eksplisitte strøm til å gi oss enumereringen av heltallene: (define heltall (cons-stream 1 (add-streams enere heltall)) når add-streams er definert slik: (define (add-streams s1 s2) (stream-map + s1 s2)) Eks: (add-streams enere enere) ; produserer 2 2 2 2 ... 492 Strømmen heltall er definert over som - tallet 1 fulgt av - de parvise summene av 1 og neste heltall. (define heltall (cons-stream 1 (add-streams enere heltall)) For å skjønne hva som skjer, ser vi på realiseringen av strømmen og dens addender: enere 1 1 1 1 1 1 1 1 + + + + + + + + heltall 1 2 3 4 5 6 7 8 —————————————————————————————————————————— heltall 1 2 3 4 5 6 7 8 9 Når add-streams kalles første gang med heltall som argument, er første ledd i heltall allerede produsert. Ved andre gangs kall på add-streams, er andre ledd i heltall produsert, osv. 493 Dette inspirere til en ny måte å definere fibonacci-tallene på (define fibs (cons-stream 0 (cons-stream 1 (add-streams fibs (stream-cdr fibs))))) Her er mønstret litt mer komplisert enn for heltallstrømmen. - Vi starter vi med to kjente tall i stedet for ett, og - i stedet for å legge sammen parvis enere og suksessive heltall, så legger vi sammen parvis løpende og neste fibonaccitall. fibs 00 11 12 23 34 55 86 137 218 349 + + + + + + + + + + (stream-cdr fibs) 11 12 23 34 55 86 137 218 349 55A —————————————————————————————————————————— ——————————————————————— fibs 00 11 12 23 34 55 86 137 218 349 Når add-streams kalles første gang med fibs som første og (stream-cdr fibs) som andre argument, er både første og andre ledd i fibs allerede produsert. Ved andre gangs kall på add-streams, er tredje ledd i fibs produsert, osv. 494 55A 89C Skalering av strømmer Følgende strøm tar en tallstrøm som input og skalerer denne ved å multiplisere hvert element i med en gitt faktor. (define (scale–stream stream factor) (stream–map (lambda (x) (* factor x)) stream)) (scale–stream heltall 2) 2 4 6 8 ... (define double (cons–stream 1 (scale–stream double 2))) double 1 2 4 8 16 32 64 ... Vi kan visualisere hva som foregår i strømmen double slik: 2 2 2 2 2 2 2 ... ... 1 2 4 8 16 32 64 ... —————————————————————————————————————————— 2 4 8 16 32 64 128 ... 1 495 496 Primtallene som en implisitt strøm — et alternativ til Eratosthenes sil Vi bruker heltallsstrømmen som basis hele veien og filtrere vekk de tallene som ikke er primtall fra denne ved hjelp av den primtallsstrømmen vi genererer. Vi lar da det første primtallet 2 være car–element i utgangspunktet og bruker heltallene fra 3 som inputstrøm for genereringen av resten. (define primes (cons–stream 2 (stream–filter prime? (heltall–fra 3)))) Poenget her er at testen i prime? utføres i forhold til den selvsamme strømmen vi genererer, og ikke er basert på en ekstern metode à la Miller–Rabin–testen. (define (prime? n) (define (iter ps) (cond ((> (square (car ps)) n)) ; primtall ((= (remainder n (car ps)) 0) #f) ; ikke primtall (else (iter (stream-cdr ps))))) ; kanskje primtall (iter primes)) 497 Det tallet som filtreres bort i iterasjonen i prime?, hentes selv fra primtallsstrømmen, men hvordan kan en strøm som bare produserer primtall, gi fra seg et ikke-primtall for filtrering? Poenget er at primes er basert på heltallsstrømmen, slik at det tallet som iter henter fra primes er neste heltall, før det er filtrert ut, men dette kommer ikke videre til den som aksesserer primes, med mindre det slipper gjennom testen i primes?. 498 For å forstå poenget med første ledd i cond-setningen i iter, kan vi se på en alternativ utforming (define (prime? n) (define (iter ps) (cond ((> (car ps) (/ n (car ps)))) ; primtall ((= (remainder n (car ps)) 0) #f) ; ikke primtall (else (iter (stream-cdr ps))))) ; kanskje primtall (iter primes)) Testen (> (car ps) (/ n (car ps))) er i realiteten den samme som (> (square (car ps)) n)) Gitt to heltall a og b, hvis a2 > b så er a > b/a, og omvendt. Betydningen av testen er da som følger: Hvis a er et primtall og a < b/a, så finnes det kanskje et primtall c > a, slik at c b/c, men hvis a > b/a, så kan det ikke finnes noen slik c. 499 157, 163 og 173 er alle primtall, men hva med tallene i mellom (vi sjekker bare oddetallene)? 3 | 159, 7 | 161, 3 | 165, (x | y betyr "x deler y" "y er delelig med x") ingen av 2, 3, 5, 7, 11 deler 167, og 167 / 13 < 13, og dermed er det ingen tall større enn 13 som deler 167, ergo er 167 et primtall. Deretter ser vi at 13 | 169 og 3 | 171, ergo er 167 det eneste primtallet mellom 163 og 173. 500 Forelesning 12 Utnyttelser av strøm–paradigmet Iterasjon i imperative og funksjonelle språk C, C++, Java, ... Scheme int sigma(int n) (define (sigma n) { int S = 0; (define (loop i S) for (int i = 1; i <= n; i++) (if (<= i n) S += i; (loop (+ i 1) (+ S i)) return S; S)) } (loop 1 0)) int fib(int n) (define (fib n) { int a = 1, b = 0; (define (loop i a b) for (int i = 1; i <= n; i++) if (<= i n) { int f = a + b; (loop (+ i 1) b = a; (+ a b) a = f; a) } b)) return b; (loop 1 1 0)) } 501 Med strømmer kan vi gjøre det slik (define sigmas (cons-stream 0 (add-streams integers sigmas))) (define fibs (cons-stream 0 (cons-stream 1 (add-streams (stream-cdr fibs) fibs)))) Vi tar med add-streams for å vise at det ikke ligger noen tellere bak kulissene (define (add-streams S1 S2) (cons-stream (+ (stream-car S1) (stream-car S2)) (add-streams (stream-cdr S1) (stream-cdr S2)))) 502 Strømmen skiller seg vesentlig fra de andre implementasjonene ved at den, ikke har noe basistilfelle. Det er alltid ett ledd til. Dette kommer tydelig frem i kvadratrotstilnærmingen i SICP 1.1.7 (noe komprimert her). (define (square-root x) (define (loop y) (if (>= (abs (- (* y y) x)) 0.001) ; Er vi ennå ikke nær nok? (loop (/ (+ (/ x y) y) 2)) ; så looper vi videre. y)) ; Vi er nær nok og returnerer siste gjetning. (loop 1.0)) Strømversjonen har ingen test for om vi har kommet nær nok. (define (sqrt-stream x) (define guess-stream (cons-stream 1.0 (stream-map (lambda (y) (/ (+ (/ x y) y) 2)) guess-stream))) guess-stream) 503 Det finnes tilstandsbaserte sekvenser der enkel iterasjon, uansett paradigme (imperativt eller funksjonelt), gir en lite hensiktsmessig løsning, fordi vi ønsker både å ha lokal kontroll over de relevante variablene og å holde tilstandsinformasjonen skjult, samtidig som vi vil at output fra sekvensen skal være globalt tilgjengelig. Men fremfor alt ønsker vi å unngå at tilstandsvariabler sendes frem og tilbake mellom oppdateringsprosedyrer og brukerprosedyrer. Stjerneeksemplet er en randomgenerator, der løpende random-nummer er en funksjon av foregående. 504 Konvererende alternerende rekker I avsnitt 1.3.1 i SICP, arbeidet vi oss frem - fra en spesifikk prosedyre for summering av heltall - til en generell summeringsprosedyre med prosedyreargumenter, og ett av eksemplene var rekken 1 1 1 1 = + + + + 8 13 57 911 1315 (1) Summering av rekken opp til et gitt ledd ble implementert slik: (2) (define (pi/8-sum n) (define (iter a sum) (if (> a n) sum (iter (+ a 4) (+ sum (/ 1.0 (* a (+ a 2))))))) (iter 1.0 0)) 505 I det følgende er vi interessert i alternerende rekker, dvs. rekker der leddene har alternerende fortegn r1 – r2 + r3 – r4 + … + rk – rk+1, k er et oddetall Vi kan lager en alternerende rekke med utgangspunkt i (1) Først lager vi en rekke som går mot /4 ved å gange alle leddene i (1) med 2, og så deler vi opp de enkelte leddene i differanser. 2 = 4 8 = = 2 13 + 1 1 + 1 3 2 57 + 1 1 + 5 7 2 911 2 1315 + (3) 1 1 1 1 + + 9 11 13 15 (4) + Scheme-implementasjonene ser slik ut (5) (define (pi/4-sum n) (define (iter sign a sum) ; sign er tallet 1.0 med alternerende fortegn (if (> a n) sum (iter (- sign) (+ a 2) (+ sum (* sign (/ 1.0 a)))))) (iter 1.0 0)) 506 I læreboken implementeres rekken (4) som en strøm på denne måten: (6) (define (pi-summands n) (cons-stream (/ 1.0 n) (stream-map – (pi-summands (+ n 2))))) og de suksessivt bedre og bedre pi-tilnærmingene implementeres som strømmen av de partielle summene av (6) (vi kommer straks tilbake til partielle summer) (define pi-stream (scale-stream (partial-sums (pi-summands 1)) 4)) Men (6) gir ikke den heldigste løsningen. Problemet er at stream-map, som hele tiden kaller seg selv, også kalles om igjen for hvert kall på pi-summands 507 (7) Vi ser på et enklere eksempel med de alterende heltallene (1, -2, 3, -4, ...). (define (s n) (cons-stream n (stream-map - (s (+ n 1))))) Her er utviklingen av de 4 første leddene i strømmen (når formen <uttrykk> representerer et løfte): (1 . <(strm-map1 - (s 2))>) (1 . (strm-map1 - (2 (1 . (-2 . <(strm-map1 - <(strm-map2 - (s 3))>)>)) (1 . (-2 . (strm-map1 - <(strm-map2 - (s 3))>))) (1 . (-2 . (strm-map1 - (strm-map2 - (s 3))))) (1 . (-2 . (strm-map1 - (strm-map2 - (3 (1 . (-2 . (strm-map1 - (-3 . (1 . (-2 . (--3 . <(strm-map1 - <(strm-map2 - <(strm-map3 - (s 4))>)>)>)))) (1 . (-2 . (--3 . (strm-map1 - <(strm-map2 - <(strm-map3 - (s 4))>)>))))) (1 . (-2 . (--3 . (strm-map1 - (strm-map2 - <(strm-map3 - (s 4))>)))))) (1 . (-2 . (--3 . (strm-map1 - (strm-map2 - (strm-map3 - (s 4)))))))) (1 . (-2 . (--3 . (strm-map1 - (strm-map2 - (strm-map3 - (4 . <strm-map4 - (s 4))>)))))) (1 . (-2 . (--3 . (strm-map1 - (strm-map2 - (-4 (1 . (-2 . (--3 . (strm-map1 - (--4 (1 . (-2 . (--3 . (---4 . <(strm-map2 - (s 3))>))) . <(strm-map3 - <(strm-map2 - <(strm-map3 - (strm-map1 - (s 4))>))))) (s 4))>)>)))) . <(strm-map3 - <strm-map4 - (s 4))>)>))))) (strm-map2 - <(strm-map3 - <strm-map4 - (s 4))>)>))))) (strm-map2 - (strm-map3 - <strm-map4 - (s 4))>)))))) 508 Her er en alternativ implementasjon, der vi bruker et par enkle hjelpestrømmer. (define alter-ones (cons-stream 1 (stream-map - alter-ones))) Også her mappes en strøm av tall på prosedyren – (minus), slik at leddene får fortegnene –, – –, – – –, – – – –, ... = –, +, –, +, ... men siden stream-map brukes på en implisitt strøm, og ikke kalles av en strømgenererende prosedyre, får vi ingen map-forgreninger. (define (every-second n) (cons-stream n (every-second (+ n 2)))) Er n initelt et partall får vi en strøm av partall, og er n initelt et oddetall får vi en strøm av oddetall. Siden (/ n) ==> 1/n kan vi med alter-ones og every-second definere /4-summandene slik: (define (pi/4-summands) (stream-map / (stream-map * alter-ones (every-second 1)))) 1 1 + 1 3 1 1 + 5 7 1 1 1 1 + + 9 11 13 15 509 Her er et annet alternativ, der vi bruker hjelpeprosedyren sign-proc. ; "fortegnsprosedyren" til n (define (sign-proc n) (if (< n 0) - +)) Prosedyren returnerer fortegnsprosedyren til n, dvs. enten – eller + avhengig av om n er negativ eller ikke. (define (pi/4-summands) (define ss (cons-stream 1 (stream-map (lambda (s) (- ((sign-proc s) s 2))) ss))) (stream-map / ss)) 1 1 + 1 3 1 1 + 5 7 1 1 1 1 + + 9 11 13 15 Også her utføres mappingen på en implisitt strøm. Utviklingen av de 4 første leddene i ss ser slik ut, når L er lambda-uttrykket: (1 . <(stream-map L (stream-cdr r)>) (1 . (3 . <(stream-map L (stream-cdr (stream-cdr r)))>)) (1 . (3 . (5 . <(stream-map L (stream-cdr (stream-cdr (stream-cdr r)))>)) (1 . (3 . (5 . 7 . <(stream-map L (stream-cdr (stream-cdr (stream-cdr (stream-cdr r)))>)) 510 Gitt en rekke R, kan vi definere en strømmen P, for partial sums, der Pi er summen av alle leddene i R, til og med Ri, dvs. P1 = R1, P2 = R1 + R2, og generelt Pi = R1 +, ..., + Ri = Pi–1 + Ri. (define P (cons-stream (stream-car R) (add-streams P (stream-cdr R)))) Generelt, med R som argument: (define (partial-sums R) (define P (cons-stream (stream-car R) (add-streams P (stream-cdr R)))) P) Vha. partielle summer kan vi definere strømmen av pi-tilnærminger slik (define pi-stream (partial-sums (scale-stream pi/4-summands 4)) (stream–>list pi–stream 5) 2.666666666666667 3.466666666666667 2.8952380952380956 3.3396825396825403) (stream-ref pi-stream 999) 3.141092653621038 (stream-ref pi-stream 99999) 3.141587653589818 511 Euler fant en formel for å aksellerere rekken av partielle summer S1, S2, ..., av alternerende konvergent rekker R1, R2, ..., der R1 0. Gitt Sn–1, Sn og Sn+1, ser leddene i den aksellererte rekken A2, A3, ..., slik ut: (Sn+1 – Sn)2 An = Sn+1 – –––––––––––––––––– Sn–1 – 2Sn + Sn+1 Dette gir oss følgende euler–transformasjon i Scheme (define (euler–transform s) (let ((sn-1 (stream–car s)) (sn ; Sn–1 (stream–car (stream–cdr s))) ; Sn (sn+1 (stream–car (stream–cdr (stream–cdr s))))) ; Sn+1 (cons–stream (– sn+1 (/ (square (– sn+1 sn)) (+ sn-1 (* –2 sn) sn+1))) (euler–transform (stream–cdr s))))) 512 (stream–>list (euler–transform pi–stream) 5) (3.1666666666666675 3.1333333333333337 3.1452380952380956 3.13968253968254 3.1427128427128435) Siden den aksellererte rekken også er alternerende, kan vi alksellerer denne igjen. (stream->list (euler-transform (euler-transform pi-stream)) 5) (3.142105263157895 3.1414502164502167 3.1416433239962656 3.1415712902014277 3.1416028416028423) (stream->list (euler-transform (euler-transform (euler-transform pi-stream)))) 5) (3.141599357319005 3.141590860395881 3.1415932312437636 3.141592438436833 3.14159274345584) 513 Vi kan generalisere dette ved å lage en strøm av strømmer, kalt et tableau, der hver enkelt strøm i strøm-strømmen er en transformasjon av den foregående strømmen. (define (make-tableau transform s) (cons-stream s (make-tableau transform (transform s)))) Fra tablaeuet kan vi lage strømmen av første element i hver av strømmene. (define (accelerate-stream transform s) (stream-map stream-car (make-tableau transform s))) (stream->list (accelerate-stream euler-transform pi-stream) 7) (4.0 3.166666666666667 3.142105263157895 3.141599357319005 3.1415927140337785 3.1415926539752927 3.1415926535911765) Sammenlign dette med ledd nummer én million i den opprinnelige -strømmen. (stream-ref pi-stream 1000000) 3.1415936535887745 514 Uendelig strømmer av par Vi vil ha sekvensen av parene (i, j), slik at i j. Dette har vi sett før, under gjennomgåelsen av prosess-sekvenser i 4. forelesning. Setter vi en øvre grense for i og j, kan vi enkelt lage en slik liste ved et par iterasjoner. (define (pairs n) (define (iter-i i) (define (iter-j j) ; her løper i fra og med 1 til og med n. ; her er i konstant, mens j løper fra og med i til og med n. (if (> j n) '() (cons (list i j) (iter-j (+ j 1))))) (if (> i n) '() (append (iter-j i) (iter-i (+ i 1))))) (iter-i 1)) (pairs 4) ((1 1) (1 2) (1 3) (1 4) (2 2) (2 3) (2 4) (3 3) (3 4) (4 4)) Ved hjelp av strømmer burde vi klare å definere den uendelige sekvensen av slike par. Det vi vil ha er alle parene i og over diagonalen i følgende uendelig matrise. (1, 1) (1, 2) (1, 3) (1, 4) ... (2, 1) (2, 2) (2, 3) (2, 4) ... (3, 1) (3, 2) (3, 3) (3, 4) ... (4, 1) (4, 2) (4, 3) (4, 4) ... ... 515 Først overfører vi iterasjonsmønstret for lister til endelige strømmer, idet vi former den innerste iterasjonen som en mapping. (define (pairs n) (define (pair-stream i-s) ; tilsvarer if (> i n) (if (stream-null? i-s) stream-nil (stream-append ; Lag (i, j)-parene for løpende i = (stream-car i-s). (stream-map ;j er det løpende elementet i i-s. (lambda (j) ;Siden i-s er argument til pair-stream, er (list (stream-car i-s) ;(stream-car i-s) konstant under mappingen j)) ; i-s mappes fra og med (stream-car i-s) og til endes. i–s) (pair-stream (stream-cdr i-s))))) ; (i, j)-parene for neste i. ; heltallsstrømmen fra 1 til n (pair-stream (stream–interval 1 n))) Vi bruker samme strøm, i-s, for j'ene og i'ene. I andre argument til stream-append, der pair-stream kalles rekursivt, rykker vi frem til neste i-ledd, mens stream-map aksesserer alle j-leddene fra og med løpende i-ledd til slutten av strømmen. 516 Testen stream-null? og den tilhørende konsekventen stream-nil kan vi sløyfe, siden stream-map tar seg av denne. (define (pairs n) (define (pair-stream i-s) (stream-append (stream-map (lambda (j) (list (stream-car i-s) j)) i–s) (pair-stream (stream-cdr i-s)))) (pair-stream (stream–interval 1 n))) ; Lag (i, j)-parene for løpende i = (stream-car i-s). ;j er det løpende elementet i i-s. ;(stream-car i-s) er konstant under mappingen ; i-s mappes fra og med (stream-car i-s) og til endes. ; (i, j)-parene for neste i. ; heltallsstrømmen fra 1 til n 517 Vi kan definere stream–append — parallelt til append for lister—slik: (define (stream-append s1 s2) (if (stream-null? s1) s2 (cons-stream (stream-car s1) (stream-append (stream-cdr s1) s2)))) Men denne prosedyren virker bare for endelige strømmer. Siden stream–append må løpe gjennom hele den første argumentstrømmen, før den kan skjøte på den andre, kommer vi aldri frem til den andre argumentstrømmen, hvis den første er uendelig. For å løse dette problemet, kan vi benytte en form for tvinning der vi vekselvis plukker elementer fra den ene og den andre inputstrømmen. (define (interleave s1 s2) (if (stream–null? s1) s2 (cons–stream (stream–car s1) (interleave s2 (stream–cdr s1))))) Den eneste forskjellen mellom interleave og stream–append består i at interleave roterer rekkefølgen mellom argumentstrømmene ved rekursjonen. 518 Hvis vi ikke bryr oss om ordningen av parene, kan vi, for endelige strømmer, uten videre erstatte stream–append med interleave. (define (pairs max) (define (pair-stream i-s) (interleave (stream-map (lambda (j) (list (stream-car i-s) j)) i-s) ; rekursjonen. Evig løkke hvis i-s er uendelig (pair-stream (stream-cdr i-s)))) (pair-stream (stream-interval 1 max))) (fin-stream->list (pairs 5)) ((1 1) (2 2) (1 2) (3 3) (1 3) (2 3) (1 4) (4 4) (2 4) (3 4)) Men med uendelige strømmer går dette galt. Begge argumentene til interleave må evalueres før interleave kan utføres, og ett av disse er et rekursivt kall på pair-stream. Dermed fortsetter pair-stream å kalle seg selv uten noen termineringstest, og noen slik test skal vi heller ikke ha siden vi opererer på uendelige strømmer. 519 Det som mangler her, er nettop det som gjør en strøm til en strøm — et umiddelbart tilgjengelig ledd paret med et løfte om noe mer: (define (pair-stream i-s) ; tar vi med denne, går det bra (cons-stream (list (stream-car i-s) (stream-car i-s)) ; første ledd i resultatstrømmen (interleave (stream-map (lambda (j) (list (stream-car i-s) j)) (stream-cdr i-s)) (pair-stream (stream-cdr i-s))))) (stream->list (pair–stream integers) 10) ((1 1) (1 2) (2 2) (1 3) (2 3) (1 4) (3 3) (1 5) (2 4) (1 6)) Vi ser at det rekursive kallet på pair-stream ikke utføres før neste ledd i resultatstrømmen aksesseres. 520 Vi kan nå generalisere dette til en strøm av par av hvilke som helst to inputstrømmer. Over bruker vi samme strømmen for i'ene og for j'ene. Det vi nå vil ha, er en egen strøm på j-plassen. (define (pair-stream s1 s2) (cons-stream (list (stream-car s1) (stream-car s2)) ; første par (= paret på diagonalen, hvis s1 = s2). (interleave (stream-map (lambda (x) (list (stream-car s1) x)) (stream-cdr s2)) (pair-stream (stream-cdr s1) (stream-cdr s2))))) (stream->list (pair–stream odds odds) 10) ((1 1) (1 3) (3 3) (1 5) (3 5) (1 7) (5 5) (1 9) (3 7) (1 11)) (Disse parstrømmene blir først virkelig interessante når vi skal bruke dem i ukeoppgavene.) 521 Random-sekvenser er strømmer En random-generator er en sekvens x0, x1, x2, … med en tilhørende oppdateringsfunksjon f, slik at xn = f(xn – 1). I et strengt funksjonelt program må vi sende f og xn – 1 rundt omkring, fra den ene prosedyren til den andre. Dette er lite heldig, så vi bryter vi med det funksjonelle paradigme, og lager et prosedyreobjekt med x som intern tilstandsvariabel. En randomsekvens har imidlertid et vesentlig trekk felles med en strøm, idet den ikke har noe basistilfelle. Har vi først har startet en randomgenerator, er det alltid et tall til å hente. 522 I en random-strøm kjenner hvert ledd sin forgjenger, på samme måte som en rendomgenerator til enhver tid kjenner sitt sist genererte tall. La rand-init være startverdien i en randomsekvens, og la prosedyren rand-update som produsere de etterfølgende tallene i sekvensen. Vi kan da definere en randomgenerator som et prosedyreobjekt med lokal tilstand slik (define rand (let ((x rand-init)) (lambda () (set! x (rand-update x)) x))) og vi kan definere den tilsvarende randomstrømmen slik (define rand-stream (cons-stream rand-init (stream-map rand-update rand-stream))) 523 Randomgeneratoren og randomstrømmen virker på samme måte i den forstand at har rand rand-stream får tak i forrige genererte random-tall x i sin omgivelse, og forrige genererte random-tall ved å aksessere seg selv. R ==> 2 17 87 map rand-update R ==> 17 87 2 ri (ru 17 87 9 25 36 21 79 9 25 36 21 79 9 25 2) (ru 17) (ru 87) (ru 36 21 79 ... 9) (ru 25) (ru 36) (ru 21) ... Men rand-stream er ikke mer tilstandsbasert enn en hvilken som helst annen strøm som utvikles ved rekursiv gjenbruk av seg selv. Eks: (define enere (cons-stream 1 enere)) (define heltall (cons-stream 1 (add-streams enere heltall))) enere 1 1 1 1 1 1 1 1 + + + + + + + + heltall 1 2 3 4 5 6 7 8 —————————————————————————————————————————— heltall 1 2 3 4 5 6 7 8 9 Som alle andre funksjoner gir rand-update alltid samme resultat med samme argument. 524 Forelesning 13 (se SICP 3.1.2 og forelesning 8) En strøm av monte-carlo-verdier Vi kan implementere monte-carlo-simulering som en strøm som tar en strøm av eksperimenter (tester) som argument. I prosedyreimplementasjonen av monte-carlo-simuleringen i kapittel 3.2 starter vi med et gitt antall eksperimenter og får ikke noe simuleringsresultat før alle eksperimentene er utført. Med en strøm av eksperimenter, derimot, gir hvert ledd i strømmen et simuleringsresultat, dvs. gitt en monte-carlo-strøm, har vi for hvert ledd k resultatet av k eksperimenter. Med samme initielle randomtall vil par nummer 1000 i randomstrømmen ha samme verdi, som det randomgeneratoren returnerer fra kall nummer 1000. 525 Men, siden antall forsøk ikke er kjent for strømmen på forhånd, må strømmen selv telle opp alle forsøk—både vellykkede og mislykkede. (define (monte-carlo test-stream passed failed) (define (next passed failed) (cons-stream (/ passed (+ passed failed)) (monte-carlo (stream-cdr test-stream) passed failed))) ; hvert ledd har en boolesk verdi—#t eller #f. (if (stream-car test-stream) (next (+ passed 1) failed) (next passed (+ failed 1)))) Den argumentløse testprosedyren experiment, som brukes i prosedyreimplementasjonen, og som ved gjentatte kall gir en boolesk sekvens, er i strømimplementasjonen erstattet med strømmen test-stream, som gir samme sekvens som prosedyreimplementasjonen, hvis det initielle randomtallet er det samme. 526 Cesaro-testen går som kjent ut på at sannsynligheten, P, for at to vilkårlige tall a og b ikke har noen felles primtallsfaktorer P = 6/2. For å produsere eksperimentstrømmen til monte-carlo-simuleringen definere vi en strøm som henter to og to elementer fra en annen gitt strøm og anvender en gitt binær funksjon på disse: (define (map-successive-pairs f S) (cons-stream (f (stream-car S) (stream-car (stream-cdr S))) ; dette og neste (map-successive-pairs f (stream-cdr (stream-cdr S))))) ; to videre Kaller vi denne med cesaro-testen og randomstrømmen som argumenter, får vi i retur den eksperimentstrømmen vi trenger for at monte-carlo skal gi oss en tilnærming til . 527 (define cesaro-stream (map-successive-pairs (lambda (r1 r2) (= (gcd r1 r2) 1)) rand-stream)) Eksempel: pairs (7 8 3 6 18 5 7 1 25 15 10 23 11 3 12 3 44 26 17 20 18 21 6 27 ... ) (map-successive-pairs (lambda (x y) (> (gcd x y) 1) pairs) ===> test-stream n-passed n-failed n-tests n-passed —————— n-tests (#t #f #t #t #f #t #t #f #f #t #f #t ... ) 1 1 2 3 3 4 5 5 5 6 6 7 ... ) ... ) ... ) ... ) 0 1 1 1 2 2 2 3 4 4 5 5 —————————————————————————————————————————————————————————————————————————————————————— 1 2 3 4 5 6 7 8 9 10 11 12 1 0.5 0.66. 0.75 0.6 0.66 0.7 0.625 0.55.. Det vi helst skal frem til er 6/2 0.6079 — hvilket tar langt mer enn 12 forsøk. 528 0.6 0.54.. 0.58.. Presentasjon av oblig 4 Først et motiverende eksempel Den sentrale delen av oppgaven består i å mappe en strøm av randomtall til en strøm av tegn eller ord, men la oss først se på et enklere eksempel der vi mapper fra en randomstrøm til en strøm av spørsmål og svar. Mappingprosedyren får to tall fra randomstrømmen og bruker disse til å plukke ut et spørsmål og ett svar fra to tilsvarende lister (se map-succesive-pairs over). 529 Spørsmål Svar En noe forvirrende dialog Hvor dro du? Reisefølge? Hvor lenge? Når kom du? Hvordan var det? Skarnes Færøyene Kuala Lumpur Ja Nei Samboer Kontoret To dager En uke Nokså lenge Husker ikke I morges Forrige uke 27. april Sinnsykt bra Relativt kjempekjedelig Greit Reisefølge? I morges Hvor dro du? Husker ikke Hvor dro du? Kontoret Når kom du? Greit Reisefølge? To dager Hvor lenge? Kuala Lumpur Når kom du? Ja Reisefølge? Husker ikke Hvor dro du? Husker ikke Randomtallene for dialogen er: 1, 11, 0, 10, 0, 6, 3, 16, 1, 7, 2, 2, 3, 3, 1, 10, 0, 10. På de odde plassene er spørsmålsindeksene og på de mellomliggende plassene er svarindeksene. 530 For å få ut noe mer fornuftig, legger vi en liste med mulige svar etter hvert spørsmål, og henter indeksene til løpende svar fra svarlisten til løpende spørsmål. Spørsmål Svar Dialog Hvor dro du? (0 1 2 10) Reisefølge? (3 4 5 6 10) Lenge? (7 8 9 10) Når kom du? (10 11 12 13) Hvordan var det? (14 15 16) 0 Skarnes 1 Færøyene 2 Kuala Lumpur 3 Ja 4 Nei 5 Samboer 6 Kontoret 7 To dager 8 En uke 9 Nokså lenge 10 Husker ikke 11 I morges 12 Forrige uke 13 27. april 14 Sinnsykt bra 15 Relativt kjempekjedelig 16 Greit Hvor dro du? Kuala Lumpur Reisefølge? Samboer Hvordan var det? Relativt kjempekjedelig Når kom du? I morges Hvor lenge? To dager Reisefølge? Kontoret Hvordan var det? Sinnsykt bra Når kom du? 27. april Hvor dro du? Skarnes Randomtallene for dialogen er: 0 2 1 2 4 1 3 1 2 0 1 3 4 0 3 3 0 0. På de odde plassene er spørsmålsindeksene og på de mellomliggende plassene er indeksene for de tilhørende svarlistene. 531 Fra konstruerte dialoger til faktiske tekster I den obligatoriske oppgaven går vi enda lenger, idet vi også tar hensyn til sannsynlighetene for de ulike mulige svarene. Med en faktisk, ikke-konstruert, dialog, ville vi ha latt programmet lese gjennom dialogen og telle opp antall forekomster av de ulike svarene på hvert av de ulike spørsmålene. Da ville bare svar som forekom minst én gang etter et gitt spørsmål, komme med, i det store og hele med omtrent samme frekvens som i input-dialogen En slik opptelling krever en tabell med spørsmålene som oppslagsord og disses svarlister med par av svarnumre og forekomster som data. I oppgaven skal vi jobbe med ulike typer tekster, der vi ikke ser på par (spørsmål og svar), men sekvenser av single elementer, enten tegn eller ord, og teller etterfølgerne til de ulike tegnene eller ordene i teksten. 532 n-grammer Fra http://en.wikipedia.org/wiki/N-gram An n-gram model models sequences, notably natural languages, using the statistical properties of n-grams. This idea can be traced to an experiment by Claude Shannon's work in information theory. His question was, given a sequence of letters (for example, the sequence "for ex"), what is the likelihood of the next letter? From training data, one can derive a probability distribution for the next letter given a history of size n: a = 0.4, b = 0.00001, c = 0, ....; where the probabilities of all possible "next-letters" sum to 1.0. [Typisk består treningen i tellingen av forekomstene av de ulike n-grammene—slik vi også gjør det i oblig 4.] More concisely, an n-gram model predicts xi based on xi–1, xi–2, ..., xi–n. In Probability terms, this is nothing but P(xi | xi–1, xi–2, ..., xi–n). n-gram models are widely used in statistical natural language processing. In speech recognition, phonemes and sequences of phonemes are modeled using a n-gram distribution. 533 For parsing, words are modeled such that each n-gram is composed of n words. For language recognition, sequences of letters are modeled for different languages. n-grams can also be used for efficient approximate matching. [Bl.a. i opphavsmannsstudier: Hvor like er tekstene X og Y? Er Y et plagiat av X?] n-grams find use in several areas of computer science, computational linguistics, and applied mathematics. They have been used to: - design kernels that allow machine learning algorithms such as support vector machines to learn from string data - find likely candidates for the correct spelling of a misspelled word - improve compression in compression algorithms where a small area of data requires n-grams of greater length - assess the probability of a given word sequence appearing in text of a language of interest in pattern recognition systems, speech recognition, OCR (optical character recognition), Intelligent Character Recognition (ICR), machine translation and similar applications 534 - improve retrieval in information retrieval systems when it is hoped to find similar "documents" […] given a single query document and a database of reference documents - improve retrieval performance in genetic sequence analysis as in the BLAST family of programs - identify the language a text is in or the species a small sequence of DNA was taken from - predict letters or words at random in order to create text, as in the dissociated press algorithm. [uthevingene er gjort av foreleseren] Google uses n-gram models for a variety of R&D projects, such as statistical machine translation, speech recognition, checking spelling, entity recognition, and data mining. In September 2006 Google announced that they made their n-grams public at the Linguistic Data Consortium (LDC). 535 Et n-gram er en sekvens av n-elementer, typisk av tegn i en tekst. For n = 2, 3 og 4 har vi hhv. bigrammer, trigrammer og kvadragrammer. F.eks. inneholder teksten "+++testtekst+++" (+ representerer mellomrom.) bigrammene "++" "++" "+t" "te" "es" "st" "tt" "te" "ek" "ks" "st" "t+" "++" "++", trigrammene "+++" "++t" "+te" "tes" "est" "stt" "tte" "tek" "eks" "kst" "st+" "t++" "+++", kvadragrammene "+++t" "++te" "+tes" "test" "estt" "stte" "ttek" "teks" "ekst" "kst+" "st++" "t+++". Gitt en tekst T, kan vi ved hjelp av bi-gram telle forekomstene av de ulike etterfølgerne til de ulike tegnene i T, ved hjelp av tri-gram telle forekomstene av de ulike etterfølgerne til de ulike bi-grammene i T, og ved hjelp av kvadra-gram telle forekomstene av de ulike etterfølgerne til de ulike tri-grammene i T. F.eks. ser vi at tegnet 't' følges av 'e' to ganger t e s k + og 't' og mellomrom (+) én gang hver. Vi kan representere bigrammene i en tabell slik 536 t |1 |0 |2 |0 |1 e |2 |0 |0 |0 |0 s |0 |1 |0 |1 |1 k |0 |1 |0 |0 |0 + |1 |0 |0 |0 |4 | | | | | random numbers Fra http://en.wikipedia.org/wiki/Random_generator Random number generators are very useful in developing Monte Carlo method simulations as debugging is facilitated by the ability to run the same sequence of random numbers again by starting from the same random seed [the initial random number—X0]. They are also used in cryptography so long as the seed is secret. Sender and receiver can generate the same set of numbers automatically to use as keys. The generation of pseudo-random numbers is an important and common task in computer programming. While cryptography and certain numerical algorithms require a very high degree of apparent randomness, many other operations only need a modest amount of unpredictability. Some simple examples might be presenting a user with a "Random Quote of the Day", or determining which way a computer-controlled adversary might move in a computer game. 537 Alle fornuftige randomgeneratorer er direkte eller indirekte modulus-basert. Dvs. vi definerer f ved bl.a. en m, slik at 0 xk < m og 0 f(xk) < m, for alle k 0. Et enkelt eksempel på en ikke-random, modulus-basert sekvensen er døgnets timer fra 0 til 23. Her er xk = (xk–1 + 1) mod 24. Med konstantene a > 1 og c > 0 i tillegg til m, gir følgende en såkalt linear congruential sequence—en LCS. f(xn) = (axn – 1 + c) mod m. Eks (define (make-LCR m a c x) (lambda () (set! x (modulo (+ (* x a) c) m)) x)) (define f (make-LCR 8 3 1 5)) (list (f) (f) (f) (f) (f) (f) (f) (f) (f) (f)) ==> (0 1 4 5 0 1 4 5 0 1) ; periodelengden = 4 (se under) 538 Siden sekvensen kan inneholde maks m forskjellige tall og xn+1 er entydig bestemt av xn, må det finnes en p slik at xn = xn+p, n 0, hvilket betyr at sekvensen repeteres for hvert p-te tall. Vi sier at x0 … xp–1 er sekvensens periode og at p er dens periodelengde. Vi kunne i prinsippet ha representert en slik randomgenerator ved en endelig syklisk liste, men i praksis ville dette ha krevd for mye plass. (Faktisk finnes det perioder som ville ha fylt mange universer, hvis de ble realisert i fysiske lister. Mens universet grovt regnet inneholder 1080 atomer (se http://en.wikipedia.org/wiki/Observable_universe) inneholder de lengste periodene til en MWC-generator (se under) 10800000. tall. 539 Multiply-with-carry (MWC) MWC er bare litt mer komplisert enn LCS, men kan, som nevnt, ha nærmest ufattelig periodelengder. Som LCR kommer MWC i ulike varianter, med ulike periodelengder. I sin enkleste form skiller MWC seg fra LCR kun ved at verdien c i (axn – 1 + c) ikke er en konstant, men selv en funksjonen av foregående x og c, slik: xn = (axn – 1 + cn – 1) mod m og cn = (axn – 1 + cn – 1) / m. Dette gir minst en dobling av periodelengden i forhold til LCR. Eks (define (make-MWC m a c x) (lambda () (let ((prev-x x)) (set! x (modulo (+ (* a x) c) m)) (set! c (quotient (+ (* a prev-x) c) m)) x))) (define f (make-MWC 8 3 1 5)) (list (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f) (f)) ==> (0 2 6 2 0 1 3 1 4 4 5 0 2 6 2 0 1 3 1 4) 540 ; periodelengden = 11 (I en lag-r MWC er det lagt inn en forsinkelse r, slik at xn = (axn – r + cn) mod m og cn = (axn – r + cn) / m. En enkel MWC er dermed en lag-1 MWC.) Zn+1 = 36969(Zn & 65535) + (Zn >> 16), Wn+1 = 18000(Wn & 65535) + (Wn >> 16), Xn+1 = Zn+1 << 16 + Wn+1, I oppgaveteksten skal vi modifisere LCS slik at vi får en såkalt forsinket—lagged—fibonaccisekvens. fib(0) = 0, fib(1) = 0, fib(n) = fib(n – 1) + fib(n – 2) for n 2, når lagged-fib(n) = lagged-fib(n – j) + lagged-fib(n – k) Her må vi ha j 1 og k > j, og funksjonsverdiene opp til j må allerede foreligge . 541 The Mersenne Twister (MT) bruker, i stedet for ett eller to tall, en hel vektor V med tilhørende løpende vektorindeks i som tilstandsvariabel, slik at xk er en modifisert utgave av V[i]. V fylles på nytt hver gang i når slutten av V ved at dens tall modifiseres og flyttes til nye posisjoner. Modifiseringen av tallene består i diverse bit-manipuleringer. Det som gjøres til gjenstand for modulus-operasjoner i streng forstand er den løpende vektorindeksen, men modifiseringen av tallene i V kan også sies å være en slags modulusoperasjoner i den forstand at alle tallene i V til enhver tid ligger innenfor et gitt intervall. Periodelengden til en 32-bitsversjon av the Mersenne Twister er 219937 − 1, et tall med over seks tusen sifre imponerende, men ikke i nærheten av de 10800000 22000000 som en MWC kan komme opp i. 542 543 544 Et utdrag av oppgaveteksten—lettere modifisert Oppgaven går ut på å 1. lage tre tabeller, en todimensjonal, en tredimensjonal og en firedimensjonal, for opptelling av alle sekvenser av to, tre eller fire ord eller tegn, såkalte n-gram8, i en gitt tekst (for hhv. 2-gram, 3-gram og 4-gram, også kalt bigrams, trigrams og quadragrams, bruker vi hhv. den todimensjonal, den tredimensjonale og den firedimensjonale tabellen), 2. lage en strøm av randomtall, og 3. for en gitt n, bruke randomstrømmen og den tilsvarende den tabellen til å produsere en tekst der fordelingen av n-grammer for den valgte n, er den samme som i input, mens rekkefølgen mellom elementene (tegnene eller ordene) ellers er vilkårlig, dvs. basert på randomstrømmen. 8 Ett n-gram, n-grammet, flere n-gram, n-grammene 545 Eksempel 1. Teksten T = abba abra kadabra rabarbra akka bakka barakka bar, som vi koder til +abba+abra+kadabra+rabarbra+akka+bakka+barakka+bar+, når + står for ordskille, gir bigramsekvensen +a, ab, bb, ba, a+, +a, ab, br, ra, a+, +k, ka, ad, da, ab, br, ra, a+, +r, ra, ab, ba, ar, rb, br, ra, a+, +a, ak, kk, ka, a+, +b, ba, ak, kk, ka, a+, +b, ba, ar, ra, ak, kk, ka, a+, +b, ba, ar, r+. som igjen gir bigramstabellen + a b d k r + 0 7 0 0 0 1 a 3 0 5 1 4 5 b 3 4 1 0 0 1 d 0 1 0 0 0 0 k 1 3 0 0 3 0 r sum 1 8 3 18 3 9 0 1 0 7 0 7 546 La første tegn i output være a, og la r = 14 være første randomtall. Da henter vi rad a = 7 0 4 1 3 3 fra tabellen og teller ned fra 14 til 14 – 7 = 7 i kolonne +, til 7 – 4 = 3 i kolonne b, idet vi hopper over a, til 3 – 1 = 2 i kolonne d, og så stopper vi i kolonne k fordi 2 < 3, eller altså, fordi verdien i kolonne k er større enn det vi har igjen, + a b d k r |-------||----|-|---|---| r ------- ---- - -- Vi tar frem tabellen igjen + a b d k r og ser at sekvensen gir pent skrevet: a + 0 7 0 0 0 1 a 3 0 5 1 4 5 b 3 4 1 0 0 1 d 0 1 0 0 0 0 k 1 3 0 0 3 0 r 1 3 3 0 0 0 sum 8 18 9 1 7 7 14 3 8 6 1 5 3 2 11 0 16 6 2 5 5 7 3 13 2 17 k a b r a + b a a b a + b r a a d akabra badarba brakar 547 r k r Eksempel 2. Vi teller quadragrammene, trigrammene og bigrammene av tegnene i Jimi Hendix' Little Red House, There's a red house over yonder. That's where my baby stays. ain't seen my lovin' woman in ninety nine and one half day. Wait a minute, something's wrong, my key won't unlock the door. I got a bad, bad feeling, that my baby don't live here no more. og får tre tilsvarende tekster. 4-grams There no more / Wait a red house over / I got a minute, baby door / There no more no more no more. 3-grams Ther. / Wait aine. I got somety baby lock thin't seelin ine half day whe hat ait lock thin't linute houseeliver yond on't livere, minute a mine hat's ainute here no my stays whe a 2-grams r lit / Thalore bys woveereroomy he.. otan s re a sthay baby / Thomy douthe by dey aysorond madedathabanune ney d ba,, bade d my atheeda. woomay / her y ming'ty wr, ay. min whot oroving baby my ., soune t't my 548 Oppgave 1: N-gramstabellene Bigramstabellen er den samme som lærebokas todimenasjonale tabell, bortsett fra at nye rader og entries ikke legges foran men til de foreliggende. slik at første n-gram i teksten også ligger først i tabellen. F.eks. i en trigramstabell er da s1 nøkkelen til første bigram s2-s3 som følger etter s1, og s2 nøkkelen til første tegn s3 som følger etter s1-s2 i teksten Trigramstabellen er en liste med sider i form av bigramstabeller assosiert med sidenøkler. Quadragramstabellen er en liste med volum i form av trigramstabeller assosiert med volumnøkler. Også i disse tabellene legges nye elementer (hhv. sider og volum) til de eksisterende listene. 549 Under tellingen av bigrammene • løper vi gjennom inputsekvensen signal for signal, • idet vi holder rede på forrige og løpende signal, si–1 og si, og • enten legger inn en ny entry [si–1, si] med verdi 1 eller øker eksisterende entry med 1. Under tellingen av trigrammene holder vi også rede på si–2 • og legger inn eller oppdaterer [si–2, si–1, si]. Under tellingen av quadragrammene holder vi også rede på si–3 • og legger inn eller oppdaterer [si–3, si–2, si–1, si]. 550 Oppgave 2: Tekstgeneratoren En tekstgenerator har en tabell og en randomstrøm. Vi har én generator for hver av de tre gram-lengdene. I allmenntilfellet henter generatoren frem en rad, ved å bruke de foregående utsendte signalene som nøkler, det forrige signalet for bigrammer, de to foregående for trigrammer og de tre foregående signalene for quadragrammer. F.eks. bruker trigramsgeneratoren signalene si–2, si–1, til å hente tabellrad si–1 fra tabellside si–2 og trekke neste signal til utsendelse, si, fra raden. 551 Trekningen gjøres av en egen prosedyre, som de tre generatorene har felles, ved at løpende randomtall skaleres ned til summen av vektene (frekvensene) i løpende rad, hvorpå prosedyren løper gjennom raden frem til det tegnet som som randomtallet angir (se også Litt mer detaljert om vektingen under). For å komme igang, brukes det første grammet i den aktuelle teksten. Dette brukes også ved eventuelle brudd i signalsekvensen, forårsaket av at det ikke finnes noe entry for de foregående signalene. Slike brudd har forekommet ved kjøring av tidligere utgaver av programmet. Kanskje skyldtes dette en bug, men jeg har ikke studert dette så nøye at jeg vil utelukke muligheten for slik brudd også med nåværende versjon, og uansett skal denne muligheten tas hensyn til i programmet 552 Oppgave 3: Random-generatoren Implementér en strøm L som genererer en forsinket fibonaccisekvens i henhold til følgende definisjon: Ln = Bn hvis 0 n < k (Bk-j + B0) mod m hvis n = k (Ln-j + Ln-k) mod m hvis n > k, (1) j < k, når første del av L, B, er en lineær kongruent strøm (se under) med de k tallene som ligger til grunn for beregningen av de etterfølgende tallene i L. Selv om L0 = B0 er første tall i sekvensen, er Lk første tall som er beregnet etter formelen(Ln-j + Ln-k) mod m, og for å få et konsistent output sender vi Lk ut som første tall til bruker. Utfordringen består i å lage overgangen mellom B og de etterfølgende elementene i L. 553 Forklaring og motivasjon I en fotnote i SICP, avsnitt 3.1.2, side 226, vises det til Knuth (1981), ifølge hvem de fleste av dagens [les åttitallets] random-generatorer er basert på følgende strategi, beskrevet av Lehmer i 1949: Sekvensen sies å være kongruent, Vi velger følgende fire tall: fordi hvert par av etterfølgende tall står i et visst forholdet til hverandre m modulus 0 < m. a multiplikator 0 a < m. c inkriminator 0 c < m. X0 initialverdi 0 X0 < m. mht.delelighet. Vi sier at a er kongruent med b mht. modulus m, hvis delingen av hhv. a og b med m gir samme rest, hvilket er ensbetydende med at (a – b) mod m er et hel- Den ønskede strømmen av randomverdier får vi da ved formelen Xn+1 = (aXn + c) mod m, n 0. Dette kalles en linær kongruent sekvens—LCS. 554 tall, og vi skriver a b (modulo m). (2) For Xn gjelder dermed at Xn–1 (aXn + c) (modulo m). En variasjon av LCS, som er den vi skal implementere, er en forsinket fibonaccisekvens—LFS (lagged fibonacci sequence), der den n 'te termen Xn er en funksjon av to foregående termer i avstand j og k fra Xn slik at Xn = (Xn–j + Xn–k) mod m. Fibonaccisekvensen er definert slik: n Fn = Fn–1 + Fn–2 (3) hvis 0 n < 2 (4) hvis n 2 Vi kan definere varianter av denne ved å variere avstanden mellom leddene i summen—f.eks. fra 1 til 2: n Gn = hvis 0 n < 4 (5) Gn–2 + Gn–4 hvis n 4 n hvis 0 n < k. Dette kan vi generalisere slik: Hn = Hn–j + Hn–k hvis n k, 555 (6) j < k. Her er en grafisk representasjon av H. Det er en viktig forskjell mellom fiboncci-generaliseringen H og strømmen L. I H er alle leddene opp til k lik n. I L er leddene opp til k en LCS-strøm B. Ln Dette betyr at vi = Bn hvis 0 n < k (Bk-j + B0) mod m hvis n = k (Ln-j + Ln-k) mod m hvis n > k, (1) j < k. først må lage LCS-strømmen B med k elementer, regnet fra 0 til k – 1, for så å skjøte den etterfølgende forsinkede fibonaccistrømmen til B med Lk = (Bk-j + B0) mod m som første ledd i den påskjøtte strømmen. Den store utfordringen her ligger i sammenskøtingen av strømmene. 556 Flere eksempler Vi følger prosessen trinn for trinn. 1. Teksten T = '(dette er en tettere test-tekst enn den trette teksten der \.) konverteres fra en serie med ord til en serie med tegn der + angir ordskille. T' = (+ d e t t e + e r + e n + t e t t e r e + t e s t – t e k s t + e n n + d e n + t r e t t e + t e k s t e n + d e r + .) (I T er punktum er skilt fra siste ord av hensyn til frekvenstellingen og "escaped" med \ fordi det er et reservert tegn i Scheme, men i resten av herværende presentasjon er escape-tegnet sløyfet.) Antall bigram = antall tegn i teksten. +d +t tr de te re et es et tt st tt te tte e+ -t e+ +e te +t er ek te r+ ks ek +e st ks en t+ st n+ +e te 557 +t en en te nn n+ et n+ +d tt +d de te de er er en r. re n+ e+ +t 2. Bigrammene leggges i en todimensjonal tabell med førstetegnene som rad-nøkler, andretegnene som entry-nøkler (kolonnenøkler) og antall forekomster som entries. - + d - | | | + | | | d | | e | | k | | n | | r | | s | | t | . | 1 | | e | n r s t | | | 1 | | 1 4 | | 10 | | 3 | | 16 | | | | | | | 3 | | | | | | | | | | | | 4 | | | | 1 | | | | | | | | | 1 8 sum vekt | 3 2 . | | 3 3 k 2 | 4 | 1 | | | 2 | | | 2 | | | | | 5 | | | | | | 4 | | | | | 3 | | 3 | | | | | 3 | | 14 | | | | | | | 0 1 1 | 3 558 3 1 I programmet representerer vi tabellen vha. assossiasjonslister. (*bigram-table* (- (t . 1)) (+ (d . 3) (e (d (e . 3)) (e (+ . 3) (k (k (s . 2)) (n (+ . 4) (n (r (+ . 1) (e (s (t . 3)) (t (- . 1) (+ . 3) (t . 4)) . 2) (n . 4) (r . 3) (s . 1) (r . 1) (t . 3))) (t . 3)) . 1)) . 2) (# . 1)) . 1) (e . 8) Randomtallene ligger i et intervall hvis øvre grense = m (se formlene på side 505-8) f.eks. 232 – 1 = 4294967295, og for hver aktuelle linje skalerer vi det aktuelle tallet ned i forhold til linjens totale vekt w, slik: ' = w / m. 559 3. Med linjen (+ (d . 3) (e . 3) (t . 4)) som eksempel, trekkes neste tegn slik: 0 ' < 3 gir d, 3 ' < 6 gir e, 6 ' < 10 gir t. w = 10, så med = 2252832020 får vi ' = 10 2252832020 / 4294967295 = 5, hvilket gir e som neste tegn. 4. Til slutt konverteres teksten fra en serie med tegn til en serie med ord. F.eks. de teter ten den en de ttetete når vi følger tabellen på neste side. 560 Her følger 30 suksessive trekninger med utgangspunkt i ordskille: linje (+ (d (d (e (e (t (+ (d (t (t (e (t (t (t (e (t (r (+ (+ (d (t (t (e (t (n (+ (+ (d (d (e (e (t (n (+ (+ (d (e (t (n (+ (+ (d (d (e (e (t (+ (d (t (t (t (t (e (t (t (t (e (t (t (t (e (t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3) (e 3)) 3) (+ 3) (e 3) (e 3) (+ 3) (e 3) (+ 1) (e 3) (e 3) (e 3) (+ 4) (n 3) (e 3)) 3) (+ 4) (n 3) (e 3) (+ 4) (n 3) (e 3)) 3) (+ 3) (e 3) (e 3) (e 3) (+ 3) (e 3) (+ 3) (e 3) (+ . 3) (t . 4)) . . . . . . . . . . . . 3) (r 3) (t 8) (3) (r 8) (3) (r 2) (. 3) (t 8) (3) (r 1)) 3) (t . . . . . . . . . . . . . . . . 3) (r 1)) 3) (t 3) (r 1)) 3) (t . 3) (n . 4) (s . 1) (k . . . . . . . . . . 3) 3) 8) 8) 3) 8) 3) 8) 3) . . . . . . . . . (r (t (((r ((r ((r 3) (n 4)) 1) (+ 3) (n 1) (+ 3) (n 1)) 4)) 1) (+ 3) (n . 4) (s . 1) (k . . . . . 1) 4) 1) 4) (r (s (r (s . . . . 1)) 1) (k . 1)) 1) (k . . 1) (r . 1)) . 4) (s . 1) (k . . 4)) . 4)) . 3) (n . 4) (s . 1) (k . . 4)) 3) (n 4)) 1) (+ 1) (+ 3) (n 1) (+ 3) (n 1) (+ 3) (n . 4) (s . 1) (k . . . . . . . . 1) 1) 4) 1) 4) 1) 4) (r (r (s (r (s (r (s . . . . . . . 1)) 1)) 1) (k . 1)) 1) (k . 1)) 1) (k . sum vekt 10 3 2)) 16 10 14 2)) 16 14 2)) 16 4 10 14 2)) 16 5 10 3 2)) 16 5 10 2)) 16 5 10 3 2)) 16 10 14 14 2)) 16 14 2)) 16 14 2)) 561 rand-tall skalert 1057757735 2 864618140 0 1762719995 6 3645522350 8 2585673275 8 931642115 3 2953236530 9 2404298690 8 1995771470 1 3531489635 8 1593245840 5 2856715850 10 3678867440 4 375885260 0 3901641170 2 3617451425 13 3244039250 3 2252832020 5 3211054715 11 1932533330 2 1544992520 3 3611985365 2 1570282025 5 3450933695 8 604260185 1 2498243915 8 385203485 1 1599258830 5 837823700 3 2400132890 7 trukket d e + t e t e r + t e n + d e n + e n + d e + t t e t e t e Litt mer detaljert om vektingen Eks: Tegnsekvensen t e forekommer 23 ganger, og etter denne kommer e, n, r og + hhv. 1, 7, 12 og 3 ganger. Dette gir sannsynlighetene P(e | t e) = 1/23, P(n | t e) = 7/13, P(r | t e) = 12/23 og P(+ | t e) = 3/23. P(z | x y) = sannsynligheten for at z vil forekomme etter x y. Hvis øvre grense for randomtallene m = 4294967295, løpende randomtall = 1952832023, og vekten av rad [t, e] w = 23, så er nedskaleringen av , ' = w / m = 10. Legger vi forekomstene av hhv. e, n, r og + etter hverandre langs en linje, ser vi at r blir trukket ut. e n r + || | | | ' 20 23 0 1 8 Nå kunne vi like gjerne ha valgt rekkefølgen +, e, n, r, i hvilket fall n ville ha blitt trukket ut. + e n r | || | | ' 11 23 0 3 4 Men i det lange løp vil allikevel r bli trukket ut 12/7 1.7 ganger oftere en n etter t e. 562 En trigramstabell for tegnene i Trette netter. Trette netter—en tekst i 5-tegnsalfabetet DENRT Dee Derr, Etne, tretten tretten Trette netter, tretten erter, tre terner et1 tre ender Der nede erter tre nerdete terner tre erteetende ender Dette tenner de tre endene Endene er redde de tre ternene er etter de tretten ertene Tre netter etter eter de tre endene de tre ternene en etter en asbr 2006 1 Siden og ikke kan uttrykkes i DENRT, har jeg brukt det latinske et. 563 c1 c2 c3 page row d e n d d | | 1 | e | | | 3 n | | | r | | | t | | | + | | | e d | 1 | 1 | e | | | n | 6 | 6 | 1 r | 1 | | 4 t | | 3 | 1 + | 3 | 11 | 3 n d | | 6 | e | 1 | | 2 n | | 1 | r | | | t | | | + | | 3 | r d | | 1 | e | 1 | | n | | 4 | r | | 1 | t | | 4 | + | 5 | 5 | 1 t d | | | e | | 1 | 7 n | | 1 | r | | 14 | t | | 11 | + | | | + d | | 8 | e | | | 7 n | | 4 | r | | 1 | t | | 5 | + | | | r t + | | | | | 4 | 2 | 9 | | | | | | | | | | | | | | | | | | | 1 | | | | | | | | | 5 | | 1 | 4 | 16 | | | 11 | 1 | | | 12 | | | | | | | 4 | 2 | 7 | | | | | | | | | | | | | | | | | | | | | | | 5 | 10 | | | | | | | | | | | | | | 1 | 4 | | | | | | | 12 | | 3 | | | | | | | | | | | | | | | 1 | | | | | | | 6 | 6 | | | | | | | | | | | 14 | | | | | | | En bigramstabell for ordene i Trette netter. d e de | der | derre | dette | en | endene | ender | er | ertene | erteetende| erter | et | eter | etne | etter | nede | nerdete | netter | redde | tenner | ternene | terner | tre | trette | tretten | . | d e r d e r r e d e t t e e n e n d e n e e n d e r e r e r t e n e e r t e e t e n d e e r t e r e t e t e r e t n e e t t e r n e d e n e r d e t e n e t t e r r e d d e t e n n e r t e r n e n e t e r n e r t r e | | 1| | | | | | | | | | | | | | | | | | | | 4| | | | | | | | | | | | | | | | 1| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 1| | | | | | | | | | | | | | | | | | 1| | | | | | | | | 1| | | | | | | 1| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 1| | | | 1| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 1| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 2| | | | | | | | | | | | | | | | | | | | | | | 1| 1| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 1| | | | 1| | | | | | | | 1| | | | | | | | | | | | | | | | | | | | | 1| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 1| | | | | | | | | | | | | | | | | 1| | | | | | | | | 1| | | | | | | | | | | | | | | | | | | | | | | 1| | | | | | | | | | | | | | | | | | | | | | | | | | | 1| 1| | 1| | | | | | | | | | | | | | | | | | | | | | | | | | | 1| | | | | | | | | | | 1| | | | | | 2| 1| | | 1| | | | | | | 1| 1| | | 2| 1| | | | | | | | | | | | | | | | | | | 1| | | | | | | | | | | | | | 1| | 1| | | | | | | | | | | | | | 1| | 1| | 1| | | | | | | | 1| | | | | | | | | 1| 564 t r e t t e | | | | | | | | | | | | | | | | | | | | | | | | | | t r e t t e n . 1| | | | | | | | | | | | | 1| | | | 1| | | | | | | 1| 1| | | 1| | | 1| 2| | 1| | | | | | | | | | | | | | | | 1| | Kjøreeksempel 1 :"Little Red House" % There's a red house over yonder. That's where my baby stays. % ain't seen my lovin' woman in ninety nine and one half day. % Wait a minute, something's wrong, my key won't unlock the door. % I got a bad, bad feeling, that my baby don't live here no more. Jimi Hendrix units = words, grams = 4 9 ain't seen my lovin' woman in ninety nine and one half day. Wait a minute, something's wrong, my key won't unlock the door. I got a bad, bad feeling, that my baby don't live here no more. units = words, grams = 3 9 There's a red house over yonder. That's where my baby stays. There's a red house over yonder. That's where my baby don't live here no more. Wait a minute, something's wrong, my key won't unlock the door. I got a bad, bad feeling, that my baby don't live here no more. ain't seen my lovin' woman in ninety nine and one half day. Med ord-quadragrammer er den randomiserte teksten identisk med originalen (uten repetisjoner). 565 units = words, grams = 2 There's a red house over yonder. Wait a bad feeling, that my key won't unlock the door. Wait a red house over yonder. That's where my lovin' woman in ninety nine and one half day. units = letters, grams = 4 There no more. Wait a red house over. I got a minute, baby door. There no more no more no more. units = letters, grams = 3 Ther. Wait aine. I got somety baby lock thin't seelin ine half day whe hat ait lock thin't linute houseeliver yond on't livere, minute a mine hat's ainute here no my stays whe a units = letters, grams = 2 . r lit Thalore bys woveereroomy he.. otan s re a sthay baby Thomy douthe by dey aysorond madedathabanune ney d ba,, bade d my atheeda. woomay her y ming'ty wr, ay. min whot oroving baby my ., soune t't my 566 Kjøreeksempel 2 :Randomtekst basert på quadragrams av hhv. tegn og ord i Trette Netter units = letters, case = unsense, grams = 4, text = trette-netter, language = DENRT de ender tre tre erteetenderre netter tre ende tre. etterner tretten erter ette netten erter tre ter. etten. etten. tre. tre tre endene. etten. tre. tre. tre erter. tre ender etter tre er tre enderre tre erter etter units = words, case = unsense, grams = 4, text = trette-netter, language = DENRT Dette tenner de tre endene. Endene er redde de tre ternene er etter de tretten ertene. Tre netter etter eter de tre endene. Endene er redde de tre ternene er etter de tretten ertene. Tre netter etter 567 568 Forelesning 14 Telling av rasjonale tall Vi kan anskueliggjøre som en matrise der radene har formen 1/n, 2/n, 3/n, … og kolonnene har formen n/1, n/2, n/3, …. Men hvordan teller vi tallene i matrisen, når radene og kolonnene hver for seg er uendelige? 1/1 2/1 3/1 1/2 2/2 1/3 2/3 1/4 1/6 1/7 7/1 6/1 6/2 5/3 4/4 3/5 2/6 5/2 4/3 3/4 2/5 5/1 4/2 3/3 2/4 1/5 3/2 4/1 8/1 ... 7/2 6/3 5/4 4/5 3/6 2/7 1/8 : Dette gir alle tallene i , men det gir også en masse tall vi ikke ønsker å ha med, dvs. vil vil bare ha med de tall x/y der x og y er relativt prime (hvilket vil si at gcd(x, y) = 1). 569 Vi prøver å lage en strøm av telle-sekvensen. 1 1 1 | | | | | | | | | 1 2 1 2 | | | | | | | | | | 1 1 2 1 2 1 3 1 | | | | | | | 3 2 2 2 2 3 1 3 1 4 1 4 | | | | | | | | | | | | 1 3 2 3 2 2 3 2 3 1 4 1 4 1 5 1 | | | | | 5 2 4 2 4 3 3 3 3 4 2 4 2 5 1 5 1 6 1 6 | | | | | | | | | | | | | | 1 5 2 5 2 4 3 4 3 3 4 3 4 2 5 2 5 1 6 1 6 1 7 1 | | | 7 2 6 2 6 3 5 3 5 La mtk og mnk være løpende lokale maksima for hhv. tellerne og nevnerne., slik at mt1 = 2 og mn1 = 1. Vi ser at mtk = mnk + 1, mtk ligger mnk plasser etter mnk , og mnk+1 ligger mtk plasser etter mtk. Dette forklarer hvorfor de to kurvene holder følge. 570 4 4 4 4 5 3 5 3 6 2 6 2 7 1 7 1 8 1 8 | | | | | | | | | | | | | | | | 1 7 2 7 6 3 6 5 4 5 4 5 4 3 6 3 2 7 2 1 8 1 8 2 3 4 5 6 7 1 9 1 | 9 2 8 2 3 7 ... 3 8 7 Følgende prosedyre lager én av rekkene av tellere og nevnere (define (make-rat-element-stream delta j k) (cond ((= j 0 ; delta er enten 1 eller -1 ) (make-rat-element-stream ; Forrige element = 1 1 1 (+ k 2))) ((= j k) ; Dette vil være neste maksimum ; Forrige element = k (make-rat-element-stream -1 j k)) ; Gjør klar til nedtelling fra løpende maksimum (else ; Fortsett ned/oppstigningen mot k (cons-stream j (make-rat-element-stream delta (+ j delta) k))))) Forskjellen mellom rekkene er knyttet til pariteten til j, når denne når sitt lokale maksimum, i andre cond clause. Er det lokale maksimum et partall, er det et odde antall trinn opp til (og ned fra) dette,og omvendt. Er j og k initielt like, vil de lokale maxima være partall, og er j og k initielt ulike vil de lokale maxima være oddetall. Denne gir brøkene: (stream-map list (make-rat-element-stream 1 1 1) (make-rat-element-stream 1 1 0)) 571 Denne gir også brøkene—direkte: (define (make-rat-stream delta j k) (cond ((= j 0 ) (make-rat-stream 1 1 (+ k 1))) ((= j (+ k 1)) (make-rat-stream -1 j j)) (else (cons-stream (list j (+ (- k j) 1))) (make-rat-stream delta (+ j delta) k))))) Vi ser at denne er nesten helt lik make-rat-element-stream, og den sparer oss for mappingen. For å få ut de rasjonelle tallene alene, kan vi bruke stream-filter der vi tester om teller og nevner har noen felles faktor (stream-filter (lambda (rat) (= (gcd (car rat) (cadr rat)) 1)) (make-rat-stream 1 1 1)) 572 Fra eksamen 2005 4 c) Prosedyren feedback tar en initialverdi a og to unære prosedyrer f og g som argumenter og returnerer den strømmen som dannes ved at de to prosedyrene mater hverandre gjensidig. Prosedyrene har følgende lokale objekter: - Prosedyren f-strøm tar en strøm s som argument og returnerer strømobjektet der a er første og løftet om avbildningen av f på s er andre element. - Strømobjektet g-strøm er definert ved et kall på f-strøm med avbildningen av g på en strøm x som argument. Hvilken strøm x må være, er en del av oppgaven. For at dette skal virke, kan ikke argumentet til f-strøm evalueres før definisjonen av både f-strøm og g-strøm er evaluert. Løsning (define (feedback a f g) (define (f-strøm delayed) (cons-stream a (stream-map f (force delayed)))) (define g-strøm (f-strøm (delay (stream-map g g-strøm)))) g-strøm) 573 4 d) Prosedyren prosedyrenøste tar en unær prosedyre f som argument og returnerer strømmen av suksessivt tykkere nøster av f — f.eks. slik at anvendelsen av tredje element på x = f ( f ( f (x) ) ). Eksempler: ((strømelement 1 (prosedyrenøste legg-til-1)) 1) ==> 2 ((strømelement 3 (prosedyrenøste legg-til-1)) 1) ==> 4 ((strømelement 4 (prosedyrenøste doble)) 1) ==> 16 ((strømelement 6 (prosedyrenøste doble)) 1) ==> 64 når (strømelement k s) ==> element nummer k i strømmen s, når vi teller fra 1. (legg-til-1 x) ==> x + 1, (doble x) ==> x × 2, Løsning (define (prosedyrestrøm f) (define proc-s (cons-stream (lambda (x) (f x)) ; dette er første prosedyre i strømmen (stream-map ; f er et prosedyrenøste i strømmen (lambda (g) (lambda (x) (f (g x)))) ; map f til anvendelsen av p på f proc-s))) proc-s) 574 4 e) Prosedyren feedback kan brukes til å utvikle nøstede brøker, som f.eks. brøken B under, og det samme kan vi oppnå vha. prosedyrenøste, ved passende valg av prosedyreargument. B = 1 ———————— 1 1 + —————— 1 1 + ———— 1+ = 1 ⁄ (1 + 1 ⁄ (1 + 1 ⁄ (1 + … ● Vis hvordan vi kan utvikle B frem til ledd nummer 40, først ved hjelp av feedback og så ved hjelp av prosedyrenøste. (Merk at Scheme-primitiven for divisjon kan brukes som en unær prosedyre, slik at f.eks. (/ 5) ==> 1/5.) Løsning (stream-ref (feedback 1.0 / (lambda (x) (+ x 1))) 100) ((stream-ref (prosedyrestrøm (lambda (x) (/ (+ x 1)))) 100) 1.0) (Dette konvergerer mot – 1, når er det gylne snitt .) 575 576 Forelesning 15 Gjennomgåelse av eksamensoppgaven i HUMIT2710 fra våren 2004 Oppgave 1 For å komme nærmere kvadratroten til et tall x fra en foreløpig tilnærming y, kan vi bruke formelen (y + x/y)/2. Dette gir grunnlag for følgende Scheme–program. (define (kvadratrot x) (kvadratrot-tilnærming 1.0 x)) (define (kvadratrot-tilnærming y x) (if (nær-nok-kvadratrot? y x) y (kvadratrot-tilnærming (nærmere-kvadratrot y x) x))) (define (nær-nok-kvadratrot? y x) (< (abs (- (kvadrat y) x)) 0.001)) (define (nærmere-kvadratrot y x) (/ ( + y (/ x y)) 2)) (define (kvadrat x) (* x x)) 577 (a) For å komme nærmere kuberoten (tredjeroten) til et tall x fra en foreløpig tilnærming y, kan vi bruke formelen (2y + x/y2)/3. Skriv prosedyrene (nær-nok-kuberot? y x), (nærmere-kuberot y x) og (kube x) tilsvarende (nær-nok-kvadratrot? y x), (nærmere-kvadratrot y x) og (kvadrat x). Svar (a) (define (naer-nok-kuberot? y x) (< (abs (- (kube y) x)) 0.001)) (define (naermere-kuberot y x) (/ (+ (* y 2) (/ x (kvadrat y))) 3)) (define (kube x) (* x x x)) 578 (b) Skriv om prosedyren kvadratrot-tilnærming til en generell prosedyre tilnærming som tar et toarguments-predikat nær-nok? og en to-arguments-prosedyre nærmere som argumenter i tillegg til argumentene x og y. Skriv så en prosedyre kuberot som kaller tilnærming med passende argumenter fra (a). Svar (b) (define (tilnaerming y x naer-nok? naermere) (if (naer-nok? y x) y (tilnaerming (naermere y x) x naer-nok? naermere))) (define (kuberot x) (tilnaerming 1.0 x naer-nok-kuberot? naermere-kuberot)) 579 (c) Skriv om prosedyren kuberot fra (b) slik at den kaller tilnærming med lambda-uttrykk i stedet for ferdig definerte prosedyrer. Svar (c) (define (kuberot x) (tilnaerming 1.0 x (lambda (y x) (< (abs (- (kube y) x)) 0.001)) (lambda (y x) (/ (+ (* y 2) (/ x (kvadrat y))) 3)))) 580 (d) Gitt følgende definisjon og kalleksempler, der prosedyren tilnærming er fra punkt (b): (define (listesøk x liste) (tilnærming liste x <??> <??>)) (listesøk 'c '(a b c d)) (c d) (listesøk 'e '(a b c d)) () Fyll ut de manglende delene med passende lambda-uttrykk. Svar Poenget her er at problemet: finn del-listen som begynner med x i listen liste, er det samme som problemet: finn kvadratroten av x med utgangspunkt i gjettingen y. Vi starter med den gjettingen at x ligger først i liste. (define (listesøk x liste) (tilnaerming liste x (lambda (liste x) ; Er vi nær nok? — Eller (or (null? liste) (eq? x (car liste)))) ; fant vi ingenting? (lambda (liste x) (cdr liste)))) 581 ; Prøv å komme nærmere. Oppgave 2 Vi skal se på addisjon, subtraksjon, multiplikasjon og divisjon av positive heltall. Som basis for disse operasjonene tar vi følgende prosedyrer for gitt: (define (zero? x) …) ; Returnerer #t eller #f avhengig av om x er mindre enn noe ; annet positivt heltall, eller ikke. (define (inc x) …) ; Returnerer det nærmeste heltallet til x som er større enn x. (define (dec x) …) ; Returnerer det nærmeste heltallet til x som er mindre enn x, ; eller x, om det ikke finnes noe slikt tall. Ved hjelp av disse skal vi implementere følgende prosedyrer: (define (add x y) …) ; Returnerer summen av x og y. (define (sub x y) …) ; Returnerer den positive differansen mellom x og y , eller det ; minste mulige heltallet, hvis en slik differanse ikke finnes. (define (mul x y) …) ; Returnerer produktet av x og y. (define (div x y) …) ; Returnerer den hele kvotienten mellom x og y. I det følgende betyr 'strengt rekursiv' og 'halerekursiv', hhv. 'som gir opphav til en rekursiv prosess ' og 'som gir opphav til en iterative prosess' 582 (a) Adderingen i add skal utføres ved at add kun kaller basisprosedyrene og seg selv. Skriv en strengt rekursiv versjon av prosedyren add, uten å bruke noen hjelpeprosedyre. rekursiv add Svar y sier oss hvor mange ganger utgangspunktet må inkrementeres (define (add x y) (if (zero? y) x ; Har ikke mer å legge til ; x er den samme som ved det initielle kallet (inc (add x (dec y))))) ; dekrementér y på vei inn i og ; inkrementér x på vei ut av rekursjonen 583 (b) Skriv en halerekursiv versjon av prosedyren add, uten å bruke noen hjelpeprosedyre. iterativ add Svar y sier oss hvor mange ganger utgangspunktet må inkrementeres (define (add x y) (if (zero? y) x ; har ikke mer å legge til ; x er ferdig inkrementert (add (inc x) (dec y)))) ; Dekrementér y og inkrementér x ; i hver iterasjonen 584 (c) Subtraheringen i sub skal utføres på tilsvarende måte som adderingen i add, hvilket bl.a. utelukker bruken av noen sammenligningsoperator. Skriv en halerekursiv versjon av prosedyren sub, uten å bruke noen hjelpeprosedyre. rekursiv sub Svar y sier oss hvor mange ganger utgangspunktet må dekrementeres (define (sub x y) (if (or (zero? y) ; Har ikke mer å trekke fra (zero? x)) ; Kan ikke gå under null (vi har ikke negative tall) x (sub (dec x) (dec y)))) ; Dekrementér begge i hver iterasjon 585 (d) Ved beregningen av produktet i mul kan det, i tillegg til basisprosedyrene, være greit å benytte prosedyren add og sub hhv. i allmentilfellet og i basistilfellet ved rekursjonen. (Ettertanke: Ingen grunn til å bruke sub. Basistilfellet er (zero? y), og vi kan da returnere y.) Skriv en strengt rekursiv versjon av prosedyren mul, uten å bruke noen hjelpeprosedyre.. Svar (define (mul x y) (if (zero? y) y ; skal ikke legge til flere ganger ; må retunere variabelen y siden 0 ikke er definert (add (mul x (dec y)) x))) ; legg enda en x til resultatet, ; på vei ut av rekursjonen 586 (e) En mulig strategi for å beregne heltallskvotienten i div, er å telle hvor mange ganger divisor y kan dekrementeres, samtidig som dividenden x dekrementeres så langt ned som mulig. Med en slik strategi kan det være greit å bruke en hjelpeprosedyre. Skriv prosedyren div. Svar Vil vi sikre oss mot null-divisjon, kan vi la divideringen utføres i en lokal prosedyre. (define (div x y) (define (div x) <se under>) ; Vi kan la indre div skygger for yttre, siden yttre bare skal kalles utenfra. (if (zero? y) (error "div: division by zero!" x y) (div x))) Resultatet, som telles opp fra 0, inkrementeres hver gang vi trekker x fra y. (define (div x y) (if (zero? (sub (inc x) y)) (sub x x) ; sub returnerer minimum zero. Se under angående (inc x). ; tell opp fra 0 på vei ut av rekursjonen. Vi bruker (sub x x) for å få 0. (inc (lokal-div (sub x y) y)))) ; trekk y fra x én gang til og tell hele deler på vei ut. 587 Det kan virke nærliggende å la testen være (zero? sub x y), men det ville ha gitt galt resultat, når divisjonen gikk opp. Inkrementeringen av x i (zero? (sub (inc x) y)) sikrer rett resultat i alle tilfeller. (div 11 4) x runde 1 2 3 11 7 3 (div 13 4) x rundex 1 2 3 4 0 (sub (inc x) 12 8 4 2 4 4 4 (sub (inc x) 13 9 5 1 1 y) y) 14 10 6 2 3 4 4 4 4 4 5 = 8 4 0 = 10 6 2 0 6 (div 12 4) runde x retur 2 1 0 1 2 3 4 12 8 4 0 13 9 5 1 y) = 4 4 4 4 9 5 1 0 retur 3 2 1 0 Denne divisjonen går opp. I runde 3 ville (sub x y) ha gitt 0, og vi ville ha stoppet én runde for tidlig. retur 3 2 1 0 7 (sub (inc x) 8 9 10 11 12 x x 13 x 588 14 15 16 Oppgave 3 Vi skal representere mengder som uordnede lister med bare unike elementer. Til representasjonen regner vi følgende primitiver for par og lister (når en liste er et par der andre del er er et par eller den tomme listen). - Konstruktoren cons tar to argumenter og konstruerer et par av disse. - Selektoren car tar et par som argument og returnerer dettes første element. - Selektoren cdr tar et par som argument og returnerer dettes andre element. - Predikatet null? tar en liste som argument og returnerer #t hvis denne er den tomme listen. Den tomme listen angir vi slik: '(). I tillegg tar vi predikatet (medlem? obj liste) for gitt. Dette sjekker eventuelle atomære objekter og lister i liste og returnerer #t eller #f, avhengig av om obj ble funnet eller ikke. (Ettertanke: Her kunne det ha kanskje ha vært advart mot sammenblanding med standardprosedyren member, som i motsetning til medlem? er et semipredikat, men siden medlem? ikke skal implementeres, er det vel greit slik det står) (a) Skriv prosedyren (legg-til-mengde element mengde) som legger element til mengde og returnerer resultatmengden. 589 Kalleksempler: (legg-til-mengde 3 '(1 5)) (3 1 5) (legg-til-mengde 3 '(3 1 5)) (3 1 5) Svar (define (legg-til-mengde element mengde) (if (medlem? element mengde) mengde (cons element mengde))) Siden det nye elementet cons'es på mengden hadde navnet legg-første-i-mengde kanskje ha vært mer dekkende, særlig, skal vi se, med henblikk på på oppgave (e), så vi definerer følgende synonym: (define legg-først-i-mengde legg-til-mengde) 590 (b) Skriv predikatet (mengde? liste), som sjekker om liste er en mengde, dvs. om den inneholder bare unike verdier. Kalleksempler: (mengde? '(3 1 5)) #t (mengde? '(1 3 1 5)) #f Svar (b) (mengde? liste) Alternativ 1 Alternativ 2 (samme logikk som i 1, siden cond, and og or alle er definert ved if) (define (mengde? M) (define (mengde? M) (cond ((null? M)) (or (null? ((medlem? (car M) (cdr M)) #f) M) (and (not (medlem? (car M) (cdr M))) (else (mengde? (cdr M))))) (mengde? (cdr M))))) I alternativ 1 utnytter vi at en cond-clause tar ett eller flere uttrykk og returnerer resultatet av evalueringen av det siste. I begge alternativer er vi fornøyd hvis vi har kommet gjennom hele listen set, uten å finne noe element som har et identisk element etter seg i listen. 591 (c) Skriv predikatet (delmengde? A B) som sjekker om A er en delmengde av B, dvs. om alle elementene i A finnes i B. Kalleksempler: (delmengde? '(1 5) '(3 1 5)) #t (delmengde? '(3 1 5) '(3 1 5)) #t (delmengde? '(3 5) '(7 1 5)) #f Svar (c) (delmengde? A B) Alternativ 1 Alternativ 2 (samme logikk som i 1, siden cond, and og or alle er definert ved if) (define (delmengde? A B) (define (delmengde? A B) (cond ((null? A)) (or (null? ((not (medlem? (car A) B)) #f) A) (and (medlem? (car A) B) (else (delmengde? (cdr A) B)))) (delmengde? (cdr A) B)))) 592 (d) Skriv prosedyren (snitt A B) som returnerer snittet av mengdene A og B. Kalleksempler: () (snitt '(1 5) '(3 7)) (snitt '(3 7 5) '(7 1 5)) (7 5) (snitt '(5 3 1) '(3 1 5)) (5 3 1) Svar (d) (snitt A B) (rekursiv variant) (define (snitt A B) (cond ((or (null? A) (null? B)) '()) ((medlem? (car A) B) (cons (car A) (snitt (cdr A) B))) (else (snitt (cdr A) B)))) 593 (e) Potensmengden til en mengde er mengden av alle dens delmengder. F.eks. har mengden {1, 2, 3, 4} følgende potensmengde, bestånde av 24 = 16 mengder: {{}, {1}, {2}, {3}, {4}, {1, 2}, {1, 3}, {1, 4}, {2, 3}, {2, 4}, {3, 4}, {1, 2, 3}, {1, 2, 4}, {1, 3, 4}, {2, 3, 4}, {1, 2, 3, 4}} Vi merker oss at den tomme mengden er med her. I algoritmen for å generer potensmengden til en mengde kan det imidlertid være forenklende å utelate den tomme mengden fra resultet. (Ettertanke: Dette tipset er nokså tøvete og bare relevant i noen dårlige løsninger). Skriv prosedyren (potensmengde M) som genererer potensmengden til M , med eller uten den tomme mengden. Hint: En mulig strategi er rekursivt å legge første element i løpende mengde til hver mengde i potensmengden til resten av løpende mengde, og så legge resulatet av alt dette sammen med potensmengden til resten av løpende mengde. Her kan det være greit med en hjelpeprosedyre, og i den vil du kunne dra nytte av punkt (a). 594 Svar Eks: {1, 2, 3, 4} potensmengden av {2, 3, 4} har de 8 mengdene {{}, {2}, {3}, {4}, {2, 3}, {2, 4}, {3, 4}, {2, 3, 4}} Hvis vi legger 1 først i hver av disse, får vi resten av potensmengden til {1, 2, 3, 4} {{1}, {1, 2}, {1, 3}, {1, 4}, {1, 2, 3}, {1, 2, 4}, {1, 3, 4}, {1, 2, 3, 4}} NB! I den første løsningen følger vi ikke hintet om å bruke en hjelpeprosedyre, men bruker i stedet map, som tar seg av det vi er ute etter. (I det følgende har jeg brukt engelsk, for korthets skyld.) (define (powerset set) (if (null? set) '(()) (let ((first-remaining (car set)) (powerset-of-rest (powerset (cdr set)))) (append powerset-of-rest (map (lambda (set) (cons first-remaining set)) powerset-of-rest))))) 595 En hjelpeprosedyre må gi en ad hoc mapping. Generisk mapping (define (prepend-every-set element sets) (define (map proc set) (if (null? sets) (if (null? set) '() '() (cons (cons (cons element (car sets)) (proc (car set) (prepend-every-set element (cdr sets))))) (map proc (cdr set))))) Dette gir følgende løsning (define (powerset set) (if (null? set) ; returner mengden med null-mengden '(()) (let ((powerset-of-rest (powerset (cdr set)))) (append powerset-of-rest (prepend-every-set (car set) powerset-of-rest))))) 596 Oppgave 4 Vi skal se på strømmer. Til representasjonene av strømmer hører følgende: - Spesialformen cons–stream tar to argumenter og konstruerer et strømobjekt av disse i form av et par der første del er første argument ferdig evaluert og andre del er et løfte om evalueringen av andre argument. - Selektoren stream–car tar et strømobjekt som argument og returnerer dets første del. - Selektoren stream–cdr tar et strømobjekt som argument og fremtvinger evalueringen av dets andre del. - Predikatet stream–null? tar et strømobjekt eller den tomme strømmen som argument og returnerer #t eller #f avhengig av om dette er den tomme strømmen eller ikke. - Den tomme strømmen stream–nil ('()). I tillegg tar vi følgende prosedyrer for gitt: - Prosedyren (stream–filter p s) som returnerer strømmen med de elementer i strømmen s som tilfredstiller predikatet p. - Prosedyren (finitt-strøm->liste s) som konverterer den endelige strømmen s til en liste. 597 (a) Definer prosedyren (heltall-fra n) som returnerer den uendelige strømmen av heltall fra og med n. Svar (define (heltall-fra n) (cons-stream n (heltall-fra (+ n 1)))) (b) Definer prosedyren (uten–felles–faktorer s) der s er en strøm med heltall. Prosedyren skal returnerer strømmen der første element er første element i s, og der ingen av de etterfølgende elementene er delelig med første element. Prosedyren skal dessuten være rekursiv slik at ingen av elementene i resultatstrømmen har felles faktorer. Du kan ta for gitt predikatet (delelig? x y) som returnerer #t eller #f avhengig av om heltallet x er delelig med heltallet y, eller ikke. 598 Svar (b) (define (uten-felles-faktorer tallstrøm) (cons-stream (stream-car tallstrøm) (uten-felles-faktorer (stream-filter (lambda (x) (not (delelig? x (stream-car tallstrøm)))) (stream-cdr tallstrøm))))) 599 (c) Primtallsfaktorene til et heltall er de primtallene tallet er et produkt av. F.eks. har tallet 84 primtallsfaktorene 2, 2, 3 og 7. Faktoriseringen kan gjøres ved hjelp av primtallsstrømmen, og arbeidsmengden vil da bl.a. være bestemt av hvor mange primtall vi må sjekke før alle faktorene er identifisert. (At arbeidsmengden også er bestemt av hvor mange ganger hver enkelt primtallsfaktor forekommer i det aktuelle tallet, ser vi bort fra.) Har faktoriseringsalgoritmen lineær vekst må vi for å faktorisere f.eks. tallet 35 sjekke de fire primtallen 2, 3, 5 og 7 (og vi må sjekke de samme fire tallene for å faktorisere bl.a. 7, 14, 21, 70 og 7000000000). Definer prosedyren (faktorer n) som returnerer den endelige strømmen av primtallsfaktorene i n. Prosedyren skal beregne de aktuelle faktorene vha. primtallsstrømmen, og den skal ha en lineært voksende arbeidsmengde. Du kan definere primtallsstrømmen ved å kombinere (a) og (b) og bruke en lokal hjelpeprosedyre for å mate den inn. Kalleksempler: (finitt-strøm->liste (faktorer 42)) (2 3 7) (finitt-strøm->liste (faktorer 360)) (2 2 2 3 3 5) 600 Svar (define (faktorer n) (define (iter n primtall) (cond ((= n 1) stream-nil) ((delelig? n (car primtall)) ; Det kan være flere av denne (cons-stream (car primtall) (iter (/ n (car primtall)) primtall))) ; så vi holder på løpende ; primtall inntil videre. (else (iter n (stream-cdr primtall))))) ; Ingen primtallsfaktor (iter n (uten-felles-faktorer (heltall-fra 2)))) (d) Når vi beregner et talls faktorer vha. primtallsstrømmen er det mulig å redusere arbeidsmengden slik at vi for å faktoriserer f.eks. tallet 138 = 2 3 23 kan nøye oss med å sjekke primtallene 2, 3 og 5. 601 Definer en versjon av faktorer som har mindre enn linær vekst. Svar Følgende er en tillemping av én av lærebokas algoritmer for generering av primtallsstrømmen (s 330). (define (faktorer n) (define (kvadrat n) (* n n)) (define (iter n primtall) (cond ((> (kvadrat (car primtall)) n) (cons-stream n stream-nil)) ((delelig? n (car primtall)) (cons-stream (car primtall) (iter (/ n (car primtall)) primtall))) (else (iter n (stream-cdr primtall))))) (iter n (uten-felles-faktorer (heltall-fra 2)))) F.eks. 138 / (23) = 23 og 23 < 52. 602 (e) Gjør kort rede for prinsippet for vekstreduksjonen fra (c) til (d). Ettertanke: Dette er ikke en god oppgave. Alle oppgaver bør dreie seg om koding. Svar Vekstreduksjonen er tilnærmet radikal, dvs. om vi, for enkelhets skyld, antar at det finnes en x slik at for alle heltallsintervaller av lengde y er antall primtall = lik y/x, så er veksten redusert fra n/x til (n)/x. Vekstreduksjonen oppnås ved at faktoriseringen avsluttes når løpende primtall pk > n, hvilket vil si det samme som at pk > n / pk. Dermed kan det ikke finnes noe primtall større enn pk som deler n, og n må selv være et primtall. 603 (f) Divisorene til et heltall er de tall som deler tallet. Regner vi med 1 og tallet selv, har f.eks. tallet 30 de åtte divisorene 1, 2, 3, 5, 6, 10, 15 og 30. Tallet 30 har primtallsfaktorene 2, 3 og 5, og vi merker oss at hver divisor, bortsett fra 1, enten er en av primtallsfaktorene eller et produkt av to eller flere av disse. Definer prosedyren (divisorer n) som returnerer listen med divisorene til n. Hint: Du vil her, i tillegg til prosedyren faktorer og finitt-strøm->liste, kunne ha nytte av prosedyren potensmengde i 3(e) (du må gjerne bruke prosedyren, selv om du ikke løste oppgaven). 604 Svar (define (divisorer n) (map (lambda (subset) (apply * subset)) (potensmengde (finitt-stream->list (faktorer n))))) Merk at mengden av primtallsfaktorer er en multimengde der vi tillater flere elementer med same verdi. For eksempel har 12 faktoren (2 2 3) som med unike verdier gir potensmengden (() (2) (3) (2 3)) men som med multiset gir (() (3) (2) (2 3) (2) (2 3) (2 2) (2 2 3)). Dette gir divisorene (1 2 2 3 4 6 6 12). Her må vi ha med både (2 2) og (2 2 3) for å få med 4 og 12. På den andre siden, skal divisormengden ikke være et multimengde, så det vi skulle ha fått er (1 2 3 4 6 12). Dette forventes det ikke at man skal ta hensyn til, men gjør man det, gir det et ekstra positivt inntrykk. En prosedyre for å fjerne duplikater fra et multiset kunne ha sett slik ut: (define (remove-duplikates set) (cond ((null? set) '()) ((member (car set) (cdr set)) (remove-duplikates (cdr set))) (else (cons (car set) (remove-duplikates (cdr set)))))) 605 606 607 608