0INF2810 — Funksjonell programmering — 1. forelesning. 18.01.2012

advertisement
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),
n1
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)
anam = an+m
(2)
a1 = a
(7)
an/am = an–m
(3)
a2 = aa,
(8)
(an)m = anm
(4)
a–p = 1/ap
(9)
xn = enlog x
(5)
ap/q = q√(ap) = (q√a)p
(10) x = b
a3 = aaa, …
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 = bbn-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 4321 = 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å nlog2 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 = ——— + ——— + ——— + 
13
57
911
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 - 25 = 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 tallbrø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 = 22357.
378 = 23337.
Felles:
42 = 237.
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 : AB 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
h0
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 x0 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)'
= nxn-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
(xxx)' = ((xx)x)' = (xx)'x + (xx)x' = (x'x + xx')x + xx1
= (1x + x1)x + xx1 = xx + xx + xx = 3xx.
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(uv)
——
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(xy)
——  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=? (symbolstring a) (symbolstring 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 xer 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 xer 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 noderf.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 = (22 + 34)/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å
(181 + 133 + 614)/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øyrevenstrehøyrevenstre 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øyrehøyrevenstrevenstre
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=xy
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 = 57/71 + 31/71 = (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 forgjengerealtså 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
13
57
911 1315
(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

13
+
1
1
   +
1
3
2

57
+
1
1
   +
5
7
2

911
2

1315
+ 
(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 / (23) = 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
Download