27.07.2013 Views

FP-2: Supplerende noter i funktionsprogrammering - Københavns ...

FP-2: Supplerende noter i funktionsprogrammering - Københavns ...

FP-2: Supplerende noter i funktionsprogrammering - Københavns ...

SHOW MORE
SHOW LESS

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

<strong>FP</strong>-2: <strong>Supplerende</strong> <strong>noter</strong> i<br />

<strong>funktionsprogrammering</strong><br />

Nils Andersen<br />

juli 2005<br />

Datalogisk Institut<br />

<strong>Københavns</strong> Universitet<br />

2005 ∗HCØ Tryk∗


Redaktion: c○ Nils Andersen 2005<br />

Tryk: HCØ Tryk<br />

Oplag: 120 eksemplarer<br />

ISSN 0108-3708<br />

Forord<br />

Disse <strong>noter</strong> er skrevet med henblik p˚a den indledende programmeringsundervisning i <strong>funktionsprogrammering</strong><br />

p˚a Datalogisk Institut ved <strong>Københavns</strong> Universitet (DIKU).<br />

Siden den første version fra 1996 er <strong>noter</strong>ne løbende blevet omarbejdet, og ved Det<br />

Naturvidenskabelige Fakultets overgang i efter˚aret 2004 fra semester- til kvartalsstruktur<br />

m˚atte en del af stoffet fjernes.<br />

Efter fremkomsten af lærebogen <strong>FP</strong>-1 [5], (Hansen og Rischel: Introduction to Programming<br />

using SML) blev <strong>noter</strong>ne omskrevet, s˚a de først og fremmest behandlede emner, der<br />

enten kun berøres overfladisk eller helt forbig˚as i lærebogen.<br />

Noternes første seks kapitler udgør en selvstændig fremstilling, men senest inden kapitel 7<br />

(Tegn, tekster, udskrivning) er det nødvendigt at sætte sig ind i typen af lister [5, Chapt. 5].<br />

Tak til kollegaer og studerende, som gennem ˚arene har bidraget med kritik og kommentarer,<br />

og en særlig tak til Jakob Grue Simonsen for hans pertentlige gennemlæsning. Yderligere<br />

kommentarer, forslag, oplysninger om trykfejl og lignende modtages gerne. De kan sendes<br />

til nils@diku.dk<br />

2


Indhold<br />

1 Programstyret databehandling 9<br />

1.1 Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9<br />

1.2 Databehandling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10<br />

1.3 Analoge og digitale data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11<br />

1.4 Automatisering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />

1.4.1 Universalitet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />

1.5 Programmering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />

1.5.1 Det imperative princip . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />

1.5.2 Det applikative princip . . . . . . . . . . . . . . . . . . . . . . . . . . 13<br />

1.5.3 Højere programmeringssprog . . . . . . . . . . . . . . . . . . . . . . . 14<br />

1.5.4 Valg af programmeringssprog . . . . . . . . . . . . . . . . . . . . . . 15<br />

1.6 Ind- og udlæsning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />

1.7 Alfabeter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />

1.7.1 ISO’s 7 bit-kode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />

1.7.2 ISO’s 8 bit-kode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19<br />

1.7.3 Unicode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21<br />

1.8 Funktionsbegrebet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22<br />

1.8.1 Den matematiske opfattelse . . . . . . . . . . . . . . . . . . . . . . . 22<br />

1.8.2 Den datalogiske opfattelse . . . . . . . . . . . . . . . . . . . . . . . . 22<br />

1.9 Litteratur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23<br />

2 Dialog med Standard ML 25<br />

2.1 Dialogen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25<br />

2.1.1 Foranstillede (monadiske) operatorer . . . . . . . . . . . . . . . . . . 26<br />

2.1.2 Sandhedsværdi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27<br />

2.1.3 Værdierklæring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28<br />

2.1.4 Afslutning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28<br />

2.2 Gruppering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29<br />

2.2.1 Associering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29<br />

2.2.2 Prioritet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29<br />

2.3 Funktioner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30<br />

2.3.1 Funktionserklæring . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31<br />

2.3.2 Funktionskald, parenteser og prioritet . . . . . . . . . . . . . . . . . . 32<br />

2.3.3 Argumentsæt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33<br />

2.4 Sprogsystemets faser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33<br />

3


2.4.1 Navne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34<br />

2.5 Fem simple typer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35<br />

2.5.1 To typer tal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35<br />

2.5.2 Sandhedsværdier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37<br />

2.5.3 Tegn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37<br />

2.5.4 Tekster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />

2.6 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />

2.7 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />

3 Programmeringssproget Standard ML 41<br />

3.1 Funktionsdefinition med valg mellem flere parametermønstre . . . . . . . . . 41<br />

3.2 Evaluering af funktionskald . . . . . . . . . . . . . . . . . . . . . . . . . . . 42<br />

3.3 Virkefelt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42<br />

3.3.1 Redefinition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43<br />

3.3.2 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45<br />

3.4 Navnerum; biblioteksmoduler . . . . . . . . . . . . . . . . . . . . . . . . . . 46<br />

3.5 Typen af heltal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46<br />

3.5.1 Heltalsdivision . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47<br />

3.5.2 Euklids algoritme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48<br />

3.6 Standardundtagelser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48<br />

3.6.1 Ikke-udtømmende parametermønstre . . . . . . . . . . . . . . . . . . 49<br />

3.6.2 Overløb . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49<br />

3.7 Typen af sandhedsværdier . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50<br />

3.7.1 Striks eller efterladende strategi? . . . . . . . . . . . . . . . . . . . . 50<br />

3.7.2 Betingelse som skildvagt . . . . . . . . . . . . . . . . . . . . . . . . . 51<br />

3.7.3 Konjunktion og disjunktion . . . . . . . . . . . . . . . . . . . . . . . 51<br />

3.7.4 Koncis omgang med sandhedsværdier . . . . . . . . . . . . . . . . . . 52<br />

3.8 De øvrige simple typer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52<br />

3.8.1 Typen af brudne tal . . . . . . . . . . . . . . . . . . . . . . . . . . . 52<br />

3.8.2 Typerne af tegn og tekster . . . . . . . . . . . . . . . . . . . . . . . . 53<br />

3.9 Overlæssede symboler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53<br />

3.9.1 Typebegrænsning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54<br />

3.10 Definitionen af Standard ML . . . . . . . . . . . . . . . . . . . . . . . . . . . 54<br />

3.10.1 Skandering i grundsymboler . . . . . . . . . . . . . . . . . . . . . . . 55<br />

3.10.2 Gruppering i syntaktiske helheder . . . . . . . . . . . . . . . . . . . . 57<br />

3.11 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57<br />

3.12 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58<br />

4 Produkttype. Terminering. Blokke 61<br />

4.1 Strukturerede typer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61<br />

4.2 Sæt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61<br />

4.2.1 Enhedstypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62<br />

4.3 Komponentvalg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63<br />

4.3.1 Projektionsfunktionerne . . . . . . . . . . . . . . . . . . . . . . . . . 64<br />

4.3.2 Joker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65<br />

4.4 Blok . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65<br />

4


4.4.1 Analytisk brug af val . . . . . . . . . . . . . . . . . . . . . . . . . . 67<br />

4.4.2 Udtryk og erklæringer; let og local . . . . . . . . . . . . . . . . . . 67<br />

4.5 Polymorfi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68<br />

4.5.1 Værdipolymorfi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68<br />

4.6 Kast af standardundtagelse . . . . . . . . . . . . . . . . . . . . . . . . . . . 69<br />

4.7 Robusthed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70<br />

4.8 Uendelig løkke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71<br />

4.9 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73<br />

4.10 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73<br />

5 Konstruktion af funktioner 75<br />

5.1 Problemløsning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75<br />

5.1.1 Et eksempel: Hexadecimale tegnkoder . . . . . . . . . . . . . . . . . . 75<br />

5.2 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77<br />

5.2.1 Fakultetsfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77<br />

5.2.2 Virkning af funktionskald . . . . . . . . . . . . . . . . . . . . . . . . 78<br />

5.2.3 Rekursiv problemløsning . . . . . . . . . . . . . . . . . . . . . . . . . 78<br />

5.2.4 Ræsonneren om funktionskald . . . . . . . . . . . . . . . . . . . . . . 79<br />

5.3 Indsættelse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79<br />

5.4 Programnedskrivning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80<br />

5.4.1 Navnevalg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80<br />

5.4.2 Opstilling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80<br />

5.4.3 Kommentarer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81<br />

5.4.4 Systematik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81<br />

5.5 Afprøvning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81<br />

5.5.1 Bevis eller afprøvning? . . . . . . . . . . . . . . . . . . . . . . . . . . 82<br />

5.5.2 Afprøvningsdata i programteksten . . . . . . . . . . . . . . . . . . . . 82<br />

5.5.3 Udformning af testdata . . . . . . . . . . . . . . . . . . . . . . . . . . 83<br />

5.6 Programkonstruktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85<br />

5.6.1 Hanois t˚arn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85<br />

5.6.2 Egenskaber og definitioner . . . . . . . . . . . . . . . . . . . . . . . . 87<br />

5.7 Kaldtræer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87<br />

5.8 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89<br />

6 Højereordensfunktioner 91<br />

6.1 Funktion som funktionsværdi . . . . . . . . . . . . . . . . . . . . . . . . . . 91<br />

6.1.1 Parentesregler ved funktionskald og funktionstype . . . . . . . . . . . 92<br />

6.1.2 Sekventialisering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92<br />

6.1.3 Funktion af tupel eller sekventialiseret funktion? . . . . . . . . . . . . 93<br />

6.1.4 Operator som funktion . . . . . . . . . . . . . . . . . . . . . . . . . . 94<br />

6.2 Funktion som funktionsargument . . . . . . . . . . . . . . . . . . . . . . . . 94<br />

6.3 Anonym funktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96<br />

6.3.1 Funktionserklæring med val . . . . . . . . . . . . . . . . . . . . . . . 97<br />

6.4 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97<br />

6.5 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98<br />

5


7 Tegn, tekster, udskrivning 99<br />

7.1 Typen string af tekster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99<br />

7.2 Typen char af tegn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100<br />

7.3 Typeerklæring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101<br />

7.4 Eksempler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102<br />

7.4.1 Majuskler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102<br />

7.4.2 Decimale talord . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103<br />

7.4.3 Opstilling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104<br />

7.5 Listefunktionalen map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106<br />

7.6 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108<br />

7.7 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108<br />

8 Naïv sortering 111<br />

8.1 Naboombytning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111<br />

8.2 Boblesortering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113<br />

8.3 Indsættelsessortering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113<br />

8.4 Udtagelsessortering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113<br />

8.5 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114<br />

8.6 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114<br />

9 Et større eksempel: kalender 115<br />

9.1 Rektangulære billeder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117<br />

9.2 Opdeling af en liste i dellister . . . . . . . . . . . . . . . . . . . . . . . . . . 119<br />

9.3 En enkelt m˚aneds billede . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120<br />

9.4 Ugedag . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121<br />

9.5 M˚anedsdata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121<br />

9.5.1 Skud˚ar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122<br />

9.5.2 De enkelte m˚aneder . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122<br />

9.5.3 Opsamling af m˚anedsdata . . . . . . . . . . . . . . . . . . . . . . . . 124<br />

9.6 Kalenderfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125<br />

9.7 Hele programmet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125<br />

9.8 Udnyttelse af listefunktionalen map . . . . . . . . . . . . . . . . . . . . . . . 129<br />

9.9 Hele programmet p˚a kompakt form . . . . . . . . . . . . . . . . . . . . . . . 130<br />

9.10 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132<br />

10 Kombinatorisk søgning 135<br />

10.1 Det gr˚adige princip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135<br />

10.2 Alle løsninger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137<br />

10.3 Kaste og gribe undtagelser . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138<br />

10.4 Blindgydesøgning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139<br />

10.5 Otte-dronning-problemet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140<br />

10.6 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143<br />

10.7 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144<br />

6


11 Køretidseffektivitet 145<br />

11.1 Eksempel: potensopløftning . . . . . . . . . . . . . . . . . . . . . . . . . . . 145<br />

11.1.1 En forbedring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146<br />

11.2 M˚aling af ressourceforbrug . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146<br />

11.3 Beregning af udførelsestid . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147<br />

11.4 Funktioners vækst . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148<br />

11.4.1 Store O-notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149<br />

11.4.2 Store Ω- og store Θ-notation . . . . . . . . . . . . . . . . . . . . . . . 150<br />

11.5 Effektivitet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150<br />

11.6 Listespejling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152<br />

11.6.1 Analyse af listesammenstillen (operatoren @) . . . . . . . . . . . . . . 152<br />

11.6.2 Analyse af funktionen spejl . . . . . . . . . . . . . . . . . . . . . . . 153<br />

11.7 Programtransformation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155<br />

11.8 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157<br />

11.9 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157<br />

12 Effektiv sortering 159<br />

12.1 Ineffektiv sortering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159<br />

12.2 En nedre grænse for sorteringstid . . . . . . . . . . . . . . . . . . . . . . . . 159<br />

12.3 Forbedring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160<br />

12.4 Flettesortering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160<br />

12.4.1 Analyse af flettesortering . . . . . . . . . . . . . . . . . . . . . . . . . 161<br />

12.5 Kviksortering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162<br />

12.5.1 Analyse af kviksortering . . . . . . . . . . . . . . . . . . . . . . . . . 162<br />

12.6 Effektiv sortering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163<br />

12.7 Tilfældige tal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163<br />

12.7.1 Pseudo-tilfældige tal . . . . . . . . . . . . . . . . . . . . . . . . . . . 163<br />

12.8 Sammenfatning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165<br />

12.9 Opgaver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165<br />

A Styretegn 169<br />

B Engelsk-dansk ordliste 173<br />

B.1 Ordliste for de enkelte afsnit i lærebogen . . . . . . . . . . . . . . . . . . . . 174<br />

B.1.1 Chapter 1: Getting started . . . . . . . . . . . . . . . . . . . . . . . . 174<br />

B.1.2 Chapter 2: Basic values and operators . . . . . . . . . . . . . . . . . 175<br />

B.1.3 Chapter 3: Tuples and records . . . . . . . . . . . . . . . . . . . . . . 176<br />

B.1.4 Chapter 4: Problem solving I . . . . . . . . . . . . . . . . . . . . . . 177<br />

B.1.5 Chapter 5: Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177<br />

B.1.6 Chapter 6: Problem solving II . . . . . . . . . . . . . . . . . . . . . . 177<br />

B.1.7 Chapter 7: Tagged values and partial functions . . . . . . . . . . . . . 177<br />

B.1.8 Chapter 8: Finite trees . . . . . . . . . . . . . . . . . . . . . . . . . . 178<br />

B.2 Engelsk-dansk ordliste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179<br />

C Indeks 183<br />

7


Kapitel 1<br />

Programstyret databehandling<br />

I betragtning af, at en computer tilsyneladende hverken producerer eller forarbejder eller<br />

transporterer noget fysisk h˚andgribeligt materiale, kan man undre sig over disse apparaters<br />

store betydning og udbredelse.<br />

Dette kapitel indeholder nogle mere principielle overvejelser over en computers funktion.<br />

1.1 Data<br />

Det er centralt, at computere ikke arbejder med fænomenerne selv, men med repræsentationer<br />

af dem eller, som man ogs˚a siger, med data. Ordbogsdefinitionen 1 af data er:<br />

• En formaliseret repræsentation af kendsgerninger eller forestillinger p˚a en s˚adan form,<br />

at den kan kommunikeres eller omformes ved en eller anden proces.<br />

Bemærkning: En repræsentation kan tage sigte p˚a menneskelig tolkning (fx trykt tekst)<br />

eller p˚a tolkning ved hjælp af apparatur (fx hulkort eller elektriske signaler).<br />

✛ ✘<br />

✩<br />

✛<br />

✩<br />

✚<br />

✚ ✪<br />

✓<br />

omverden<br />

perception ✲<br />

fænomen ✛<br />

✔ forst˚aelse<br />

✒✖✑ ✕<br />

virkelighed<br />

tanke<br />

✬<br />

bevidsthedsindhold<br />

✫<br />

information<br />

✩<br />

repræsentation✲ ✛<br />

tolkning<br />

✪<br />

Figur 1.1: Forholdet imellem omverden, tanke og sprog.<br />

sprog<br />

symbol<br />

Forholdet imellem omverdenen og menneskets erkendelse af den, beskaffenheden af vores<br />

bevidsthed og tilsvarende problemer behandles og diskuteres i filosofisk og datalogisk litteratur.<br />

Spørgsm˚alene er vanskelige, og der er ingenlunde enighed om svarene, men man kan<br />

vælge at anskue sammenhængen p˚a følgende m˚ade (se figur 1.1): I det bombardement af<br />

sanseindtryk og udfordringer, den omgivende virkelighed p˚atrykker os, forsøger mennesker<br />

1 fra [1].<br />

9<br />

data


at orientere sig ved at danne ideer og begreber. Skal s˚adanne bevidsthedsfænomener huskes<br />

til senere, kommunikeres til andre eller p˚a anden m˚ade behandles uden for bevidstheden, m˚a<br />

de formes som talte eller skrevne ord eller andre symboler, der da (i henhold til ovenst˚aende<br />

definition) fungerer som data.<br />

Sagt p˚a en anden m˚ade er data den form, tanker og ideer fremtræder p˚a, n˚ar de befinder<br />

sig uden for et menneskes bevidsthed, og der er en konvention for, hvorledes de skal tillægges<br />

betydning.<br />

Overgange mellem de tre faser finder sted i begge retninger: Ord og begreber foranlediges<br />

som nævnt af virkelighedens ydre p˚avirkninger og dannes i en analyse, der givetvis i høj grad<br />

er sprogligt og kulturelt betinget; man kunne sige, at ideerne udgjorde en form for model<br />

af det studerede fænomen. Den anden vej siges vi at have forst˚aet et begreb, hvis vi er<br />

klare over, hvilke praktiske betydninger det dækker. Begreber kan s˚a repræsenteres i form<br />

af symboler, og omvendt kan symboler fortolkes som bestemte begreber.<br />

1.2 Databehandling<br />

12<br />

31<br />

5<br />

indgangsdata<br />

✲<br />

addition<br />

af decimale<br />

talord<br />

dataproces<br />

Figur 1.2: Databehandling.<br />

✲ 48<br />

udgangsdata<br />

I de mange situationer, hvor man benytter repræsentationer af ting i stedet for tingene<br />

selv, foretager man en databehandling. I denne brede betydning er fænomenet ikke nyt:<br />

opbevaring eller afsendelse af en lertavle med en meddelelse i kileskrift ligesom beregning p˚a<br />

en abacus (kugleramme) var i henhold til begrebets definition databehandling. N˚ar databehandling<br />

finder sted, foreligger der nogle indgangsdata eller inddata (eng.:input), hvoraf der<br />

ved en dataproces dannes tilhørende udgangsdata eller uddata (eng.:output), se figur 1.2.<br />

En del af ˚arsagen til, databehandling er s˚a nyttig, kan forklares ved at opfatte den som<br />

simulering: Der er en proces, man gerne vil vide noget om, men af en eller anden grund<br />

ønsker man ikke, den skal udspille sig i virkeligheden. Det kan være, den tager for lang tid<br />

(hvordan bliver vejret i morgen?), er for dyr (opfør et nyt kraftværk!), destruktiv (hvad er<br />

effekten af en bombesprængning?), forurenende, eller der kan være andre grunde til, at man<br />

ikke ønsker et reelt eksperiment. Ved at simulere processen i en databehandling kan man<br />

alligevel (i det omfang, modellen er troværdig) komme til kendskab om den. Denne opfattelse<br />

illustreres af figur 1.3 og udgør vores bedste bud p˚a et svar p˚a det indledende spørgsm˚al om,<br />

hvorfor computere egentlig er nyttige.<br />

Mange EDB-anvendelser kan beskrives ud fra figur 1.3, men i en moderne hverdag tager<br />

man ogs˚a en del hjælpemidler i brug, som kun langt ude harmonerer med opfattelsen af<br />

databehandling som simulering: supermarkedets stregkodelæser og dankortautomat, pladsreservationssystemer<br />

(for teatre, færge- og flyselskaber) og et elektronisk kontaktur er eksempler<br />

p˚a databehandlingssystemer.<br />

10


virkelighedens<br />

plan<br />

det bevidstheds- eller<br />

begrebsmæssige plan<br />

sprogets eller<br />

kalkulens plan<br />

✛ ✘<br />

✩proces,<br />

✛ der ✩ønskes<br />

✚ studeret<br />

✲<br />

✚ ✪<br />

✓<br />

✛ ✘<br />

✩<br />

✛<br />

✩<br />

✚<br />

indgangsfænomen✔<br />

✚ ✪<br />

✓<br />

udgangsfænomen✔<br />

✒ ✑ ✖ ✕<br />

modellering<br />

❄<br />

✬<br />

indgangsoplysninger<br />

✫<br />

repræsentation<br />

❄<br />

inddata<br />

✩<br />

✪<br />

✲<br />

dataproces<br />

✒✖✑ ✕<br />

✻<br />

indsigt<br />

✬<br />

resultater<br />

✫<br />

✻ tolkning<br />

uddata<br />

Figur 1.3: Databehandling opfattet som simulering.<br />

1.3 Analoge og digitale data<br />

✩<br />

✪<br />

Man skelner mellem analog og digital repræsentation af information. Hvis der ved repræsentationen<br />

benyttes en længde, vinkel, strøm, spænding eller anden kontinuert varierende fysisk<br />

størrelse, kaldes det analoge data. I en analogregnemaskine benyttes der analoge signaler i<br />

de centrale kredsløb; udviklingen inden for elektronik har imidlertid betydet, at man nu kan<br />

opn˚a langt større hastighed og præcision ved at benytte digital teknik. (Det er den samme<br />

udvikling, man i øjeblikket ser inden for telefon-, radio- og fjernsynstransmission.)<br />

Digitale data forudsætter et vedtagent alfabet af s˚akaldte tegn (eng.:character), og i en<br />

digital repræsentation, som ogs˚a kaldes et ord, er der til hver af en række positioner knyttet<br />

et tegn.<br />

Den skrivem˚ade for ikke-negative hele tal, som vi har fra araberne, er et eksempel p˚a<br />

digital repræsentation: Tegnene er de ti decimale cifre, og positionerne betegner antallet af<br />

enere, tiere, hundreder, osv., regnet fra højre (fordi arabisk læses fra højre mod venstre). Vi<br />

vil skelne mellem et tal, som er det matematiske begreb, og dets repræsentation som talord.<br />

Med et givet antal positioner udnytter det arabiske talsystem mulighederne optimalt: har<br />

man for eksempel tre cifre til r˚adighed, kan der dannes 10·10·10 forskellige talord, som bruges<br />

til at betegne de 1000 forskellige tal fra 0 (betegnet 000) til 999. Det underliggende princip<br />

kaldes “radixnotation” og er ikke afhængigt af, at alfabetet netop har ti forskellige tegn:<br />

Systemet kan bruges for et hvilket som helst alfabet, n˚ar der blot mindst er to forskellige<br />

tegn i det. (Man kan s˚agar tillade forskellige alfabeter for hver af de forskellige positioner.)<br />

Bruges kun de to cifre 0 og 1, f˚as det binære talsystem eller totalssystemet; et binært<br />

ciffer kaldes ogs˚a en bit. Skrevet binært begynder talrækken 0, 1, 10, 11, 100, 101, 110, 111,<br />

1000, 1001, . . .<br />

I disse <strong>noter</strong> skal vi kun interessere os for digitale data.<br />

11


1.4 Automatisering<br />

Data skal (det ligger i selve definitionen) kunne behandles af en proces. Udførelse af de fire<br />

elementære regningsarter p˚a decimale talord ciffer for ciffer under brug af “mente”, “l˚an”,<br />

“den lille tabel” og s˚a videre er eksempler p˚a s˚adanne processer.<br />

De nævnte metoder g˚ar helt slavisk frem, og i 1642 konstruerede Blaise Pascal (1623–<br />

1662) en mekanisk regnemaskine, som kunne addere og subtrahere sekscifrede decimale talord;<br />

moderne elektroniske lommeregnere kan behandle flere cifre og har mange flere funktioner.<br />

Selv om en automatisk regnemaskine kan aflaste megen triviel talbehandling, skal man<br />

dog stadig selv tage stilling til, hvilke operationer der skal udføres, og i hvilken rækkefølge<br />

det skal ske. Det afgørende skridt fra regnemaskine til computer g˚ar ud p˚a at automatisere<br />

hele dataprocessen og ikke blot de aritmetiske operationer. En computer indeholder<br />

en regneenhed som en af sine dele, men er yderligere bygget ud, s˚a den kan løse en hel<br />

beregningsopgave uden indgriben undervejs.<br />

1.4.1 Universalitet<br />

Rent teknisk er der ikke noget i vejen for at bygge automater, der løser specialiserede databehandlingsopgaver<br />

(som at udstede billetter, veksle penge, styre en vaskemaskine og<br />

lignende). Det viser sig imidlertid, at man ikke skal gøre en automats ordrerepertoire særligt<br />

omfattende, før den bliver universel i den forstand, at den kan udføre en hvilken som helst<br />

databehandling.<br />

For os vil det interessante netop være universelle computere, det vil sige databehandlingsapparater<br />

med den egenskab, at hvis man kunne specialbygge en indretning, der løste<br />

en bestemt ønsket databehandlingsopgave, s˚a kan man ogs˚a f˚a computeren til at løse denne<br />

opgave.<br />

1.5 Programmering<br />

Fysisk set ombygges computeren ikke mellem hver af de forskellige opgaver, den skal løse.<br />

Det, der sker, er, at man udskifter dens program, det vil sige de ordrer, som styrer dens<br />

virkem˚ade.<br />

At programmere (eller, som man sagde før i tiden: kode) en computer g˚ar ud p˚a at<br />

forsyne den med de ordrer, der skal til for at løse en forelagt opgave. Her st˚ar flere forskellige<br />

principper eller beregningsmodeller til r˚adighed; vi omtaler nedenfor kort to af dem: det<br />

imperative og det applikative princip.<br />

1.5.1 Det imperative princip<br />

N˚ar en beregningsopgave løses manuelt, har man ud over en regnemaskine brug for at kunne<br />

<strong>noter</strong>e mellemresultater ned, taste andre mellemresultater ind igen og eventuelt lade delresultater<br />

styre de videre beregninger. En af de m˚ader, hvorp˚a man kan udbygge en regneenhed<br />

til en computer, der er universel i den ovennævnte forstand, er derfor ved siden af regneenheden<br />

at have et lager til mellemresultater.<br />

12


Et vigtigt skridt i datamaskinernes udvikling blev taget, da Charles Babbage (1792–<br />

1871) planlagde en mekanisk regnemaskine, som ud over datalager og regneenhed samt indog<br />

udlæsningsenheder ogs˚a skulle have en styreenhed, hvori en sekvens af hulkort bestemte,<br />

hvilke operationer der skulle udføres. Denne maskine kunne alts˚a programmeres: med forskellige<br />

sekvenser af hulkort i styreenheden kunne den løse forskellige opgaver. Til assistance<br />

ved planlægning af styringen havde han komtesse Augusta Ada Lovelace (den engelske digter<br />

lord Byrons datter), som s˚aledes i en vis forstand blev verdens første programmør. Af<br />

praktiske grunde forblev maskinen dog p˚a tegnebrættet — med datidens teknik var den<br />

umulig at bygge.<br />

Imperativ programmering, der ogs˚a kan kaldes tilstandsorienteret programmering eller<br />

variabelbaseret programmering, g˚ar ud p˚a at supplere regnekapaciteten med et lager. Skudt<br />

ind mellem de almindelige regneordrer (til addition, subtraktion, multiplikation og s˚a videre)<br />

ligger der ordrer om at læse fra pladserne i lageret og at skrive til dem, s˚a lagerets indhold<br />

hele tiden ændrer sig.<br />

Fungerende datamaskiner har næsten alle denne opbygning, og skal man benytte s˚adan<br />

en maskine, skal den alts˚a udføre et program, der bestemmer, hvordan lagerets indhold skal<br />

bruges og ændres.<br />

Efter den imperative model foreg˚ar en beregning ved, at en maskine (med sin regneenhed<br />

og sit lager) præsenteres for to ingredienser: dels de aktuelle inddata, dels et program,<br />

hvorefter maskinen behandler inddata i overensstemmelse med programmet og som resultat<br />

leverer uddata:<br />

x<br />

inddata<br />

p<br />

program<br />

1.5.2 Det applikative princip<br />

✲<br />

✲<br />

datamaskine<br />

M<br />

(imperativ forst˚aelse)<br />

✲ y<br />

uddata<br />

Almindelige regneudtryk form˚ar ikke at beskrive alle beregninger, og derfor skulle der i den<br />

imperative model suppleres med et lager til “mellemresultater”.<br />

En anden opfattelse g˚ar ud p˚a, at en beregning veksler mellem at inddrage forskellige<br />

dele. Ved at identificere de forskellige dele med passende navne kan man sørge for, at de<br />

kommer i brug til det rigtige tidspunkt. Mentalt arbejder denne model derfor med bindinger<br />

mellem navne eller parametre og værdier. S˚a længe et b˚and best˚ar, ændres et navns værdi<br />

ikke (i modsætning til forholdene i den imperative model, hvor en plads kunne skifte værdi).<br />

I applikativ programmering, der ogs˚a kaldes funktionsorienteret programmering eller værdiorienteret<br />

programmering, opn˚as universaliteten ved at udvide klassen af udtryk med nye<br />

former og operationer, som gør det muligt at formulere en vilk˚arlig beregning som et udtryk.<br />

Da disse nye operationer skal erstatte brugen af et eksplicit lager, er det klart, mange<br />

af dem vil have karakter af nogle “omflytninger”, og den nødvendige udvidelse kaldes ogs˚a<br />

“kombinatorisk logik”.<br />

Det matematisk grundlag for det applikative synspunkt blev udviklet i 1930’erne af<br />

Alonzo Church (1903–95) i form af den s˚akaldte lambda-kalkule, men den første, der foreslog<br />

teorien benyttet i forbindelse med programmering, var John McCarthy, som omkring<br />

1960 udviklede en kalkule for rekursive funktioner af symbolske udtryk og konstruerede<br />

programmeringssproget Lisp.<br />

13


I applikativ programmering formuleres den opgave, man vil have løst, som et generaliseret<br />

regneudtryk, og man lader derefter det datamatiske system evaluere dette regneudtryk til<br />

en værdi. Sagt p˚a en anden m˚ade skal inddata kapsles ind i et mere omfattende regneudtryk;<br />

hvad der i det imperative paradigme var et program, svarer her til en kontekst: et generaliseret<br />

udtryk med et “hul” (hvori inddata kan placeres).<br />

f(x) ✲<br />

inddata indkapslet i et<br />

applikativt udtryk<br />

datamaskine<br />

A<br />

(applikativ forst˚aelse)<br />

1.5.3 Højere programmeringssprog<br />

✲ y<br />

uddata<br />

Udviklingen inden for elektronik (radiorør) gjorde det muligt at bygge de første fungerende<br />

datamaskiner i tiden omkring anden verdenskrigs slutning; disse maskiner havde dog stadig<br />

et separat styrende program. Forud for hver ny anvendelse af maskinen skulle programmet<br />

skiftes ud, hvilket kunne være en møjsommelig opgave. I 1940’erne fik en gruppe ingeniører,<br />

hvis mest fremtrædende medlem var John von Neumann (1903–1957), den afgørende ide at<br />

integrere data- og programlager, s˚aledes at de ordrer, som styrede dataprocessen, blev hentet<br />

fra lageret, hvilket ogs˚a betød, at maskinen undervejs i en beregning kunne modificere sine<br />

egne ordrer.<br />

Selv om det skyldes den moderne elektronik (transistorer, integrerede kredse), at datamaskiner<br />

er blevet hurtige og billige og dermed har f˚aet en s˚adan udbredelse, som tilfældet<br />

er, har elektronik ikke principiel betydning i forbindelse med datateknik: man kunne ogs˚a<br />

(og gør det i et vist omfang) bruge mekaniske, hydrauliske, pneumatiske (trykluft), magnetiske<br />

eller optiske (lys) lagrings- og transportmedier; det afgørende er, at vi har at gøre<br />

med en universel datamaskine, der fortolker sit program fra et lager, hvortil der<br />

automatisk kan skrives.<br />

Betegnelsen datamaskine eller datamat (eng.:computer) vil i det følgende blive brugt<br />

om en s˚adan universel datamaskine med lagret program; datamatik (eng.:computer science)<br />

drejer sig om egenskaber ved og brugen af s˚adanne maskiner.<br />

Reglerne for, hvorledes en datamat fortolker et lagerindhold som en ordre, er bestemt<br />

af maskinens ingeniørmæssige konstruktion og kaldes for dens maskinsprog. Da ordrer kan<br />

behandles som data, behøver man imidlertid ikke at programmere maskinen i dette sprog:<br />

et afviklingsprogram (der dog da selv m˚a være lagt ind i maskinsprog) kan oversætte eller<br />

fortolke programmer udformet i et s˚akaldt højere programmeringssprog til maskinsprog,<br />

og man siger da, at det højere programmeringssprog er implementeret p˚a den p˚agældende<br />

datamat.<br />

Hvis et afviklingsprogram for sproget L er lagt ind i en maskine M, ser det fuldstændig<br />

ud, som om man havde at gøre med en L-maskine.<br />

Fremkomsten af højere programmeringssprog har gjort det muligt at afvikle de samme<br />

programmer p˚a meget forskellige datamater (og at afvikle programmer i mange forskellige<br />

programmeringssprog p˚a samme datamat) — i princippet kan alle datamater det samme.<br />

Siden implementeringen i 1958 af det første højere programmeringssprog Fortran (afledt<br />

af “formula translator”) er der fremkommet et overordentlig stort antal af den slags<br />

sprog. To af dem har navn efter de ovennævnte pionerer Pascal og Ada; et meget udbredt<br />

sprog hedder C (videreudviklet til C++). Sammen med Algol, Cobol, PL/I, Basic og<br />

14


mange andre tilhører alle de nævnte sprog klassen af imperative eller tilstandsorienterede<br />

programmeringssprog. Fælles for disse sprog er et forholdsvis tro billede af datamatens lager<br />

som opbygget af variable, der skifter værdi under en beregning.<br />

I Objektorienteret programmering opfattes alle forhold og størrelse, programmet skal<br />

arbejde med, som objekter med visse egenskaber. Objektorienterede programmeringssprog<br />

er blandt andet Simula, Emerald, Java og C++.<br />

Det sprog, der præsenteres i de følgende kapitler, tilhører klassen af s˚akaldte applikative,<br />

værdiorienterede eller funktionsorienterede programmeringssprog, hvor program og inddata<br />

bygges sammen til et udtryk, og den ønskede databehandling opfattes som evaluering af<br />

dette udtryk til en værdi. De applikative sprog omfatter blandt andet Lisp, Scheme, ML,<br />

Hope, Miranda og Haskell.<br />

Logikprogrammering, g˚ar ud p˚a at beskrive de egenskaber, uddata skal have, p˚a en s˚adan<br />

m˚ade, at denne beskrivelse kan danne grundlag for beregning; <strong>funktionsprogrammering</strong> og logikprogrammering<br />

kan under et betegnes deklarativ programmering. Eksempler p˚a logikprogrammeringssprog<br />

er Prolog og SQL.<br />

Datamater med et applikativt sprog som maskinsprog har været konstrueret, men har<br />

ikke vundet stor udbredelse. I praksis afvikles ethvert højere programmeringssprog, hvad<br />

enten det er imperativt eller applikativt, p˚a traditionelle imperative maskinarkitekturer ved<br />

hjælp af et passende afviklingsprogram.<br />

1.5.4 Valg af programmeringssprog<br />

At løse en opgave ved hjælp af en datamat betyder at realisere en bestemt dataproces K og<br />

indebærer i praksis, at man vælger et bestemt højere programmeringssprog (imperativt eller<br />

applikativt), som man har adgang til et afviklingsprogram for, og skriver passende kode (et<br />

program pK eller en fK(. . .)).<br />

Der er en tendens til, at programmører opdeler sig i “skoler”, der (undertiden nærmest<br />

fanatisk) sværger til hver deres foretrukne programmeringssprog. Ved en mere nøgtern betragtning<br />

m˚a det erkendes, at de forskellige sprog har hver deres styrker og svagheder.<br />

Tilstandsorienterede sprog holder mest konkret styr p˚a, hvad der foreg˚ar i datamaten, og<br />

giver derigennem mulighed for konstruktion af meget effektive programmer. Effektiviteten<br />

opn˚as p˚a bekostning af, at der er flere detaljer, man selv skal tage stilling til, s˚a det bliver<br />

mere omstændeligt at n˚a frem til en løsning og vanskeligere at overbevise sig om, at den er<br />

korrekt.<br />

Programmeres efter det funktionsorienterede princip, kan det være vanskeligere at gennemskue,<br />

hvilke maskinaktiviteter der sættes i gang, og man kan uforvarende komme til at<br />

skrive ineffektive programmer. Nyere forskning inden for omr˚adet betyder dog, at afviklingstiderne<br />

i de senere ˚ar er reduceret betydeligt. Først og fremmest opvejes eventuelle ulemper<br />

ved funktionsorienterede sprog af, at der er mange detaljer, man slipper for at tage stilling<br />

til, s˚a man f˚ar langt større fleksibilitet, modularitet og udtrykskraft. Frem for et lager, hvis<br />

indhold varierer gennem beregningsforløbet, er det mentalt set enklere at have at gøre med<br />

bindinger mellem navne og værdier, hvor værdierne ikke ændrer sig, mens b˚andet best˚ar.<br />

Funktionsudtryk er sædvanligvis velstrukturerede og lette at afprøve og at ræsonnere om.<br />

Derfor er et funktionsorienteret sprog ogs˚a velegnet ved introducerende undervisning.<br />

15


1.6 Ind- og udlæsning<br />

Skal databehandlingsresultater forelægges mennesker, m˚a det ske ved p˚avirkning af en af<br />

vore fem sanser — i praksis synet og/eller hørelsen. Inddata m˚a frembringes af vores bevægeapparat:<br />

først og fremmest fingre og hænder, eventuelt tale.<br />

De første seriefremstillede datamater var forbundet med en elektrisk skrivemaskine, s˚aledes<br />

at inddata hentedes fra tastaturet, og uddata var maskinskrevet tekst p˚a papir.<br />

Megen ingeniørmæssig energi og opfindsomhed er investeret i at udvikle hurtigere og<br />

bedre m˚ader at kommunikere med maskinerne p˚a; i moderne datamater præsenteres resultater<br />

som billeder p˚a en skærm, eventuelt i farver, og som lyd eller musik, mens inddata kan<br />

komme fra tastaturet eller ved at der er brugt en “mus” og trykket p˚a musens knapper. Der<br />

er ogs˚a skærme, som kan reagere, n˚ar der peges p˚a dem, og man kunne forestille sig mange<br />

andre muligheder.<br />

Alle signaler kan digitaliseres; bag de farvebilleder, skærmen viser, ligger en repræsentation,<br />

som for hvert punkt p˚a skærmen angiver dets farve og lysintensitet. Psykologisk er<br />

det naturligvis af stor betydning, om man præsenteres for et rigtigt billede eller bare for en<br />

tabel af indgange af formen (x, y, farvekode), men det informationsteoretiske indhold er det<br />

samme.<br />

Programmer i et højere programmeringssprog formuleres som tekst, og i disse <strong>noter</strong> vil vi<br />

ogs˚a g˚a ud fra, at ind- og uddata er tekster. I princippet er dette tilstrækkeligt til simulering<br />

af enhver dataproces: Der er standard-programpakker, som kan sørge for, at brug af musen<br />

bliver til indgangstekster a la “venstre museknap nedtrykket i skærmkoordinat (x, y)”, og<br />

at udgangstekster med (x, y, farvekode) bliver vist som billeder p˚a skærmen.<br />

Kernen i at lære at programmere g˚ar derfor ud p˚a at konstruere programmer med tekst<br />

som ind- og uddata.<br />

1.7 Alfabeter<br />

Fejloversat fra engelsk kaldes et alfabet, som er en mængde af tegn (eng.:character set), ogs˚a<br />

for et tegnsæt. Forskellen p˚a en mængde og et sæt er, at der ikke er nogen organisation i en<br />

mængde, mens elementerne i et sæt er ordnet. Brugt som digitale data har tegn i et alfabet<br />

imidlertid hver deres kode eller vægt, og betegnelsen “tegnsæt” giver da alligevel mening,<br />

idet man kan tænke sig alfabetets tegn stillet op i rækkefølge efter vægt.<br />

Hvordan noget skal repræsenteres, er kun i meget ringe grad naturligt og kan vælges<br />

næsten fuldstændig frit. I begyndelsen, hvor hver computer var en autonom installation,<br />

og hvert fabrikat derfor omtrent havde sit eget alfabet, var et stort antal alfabeter i brug.<br />

Med udbredelsen af datakommunikation blev der behov for standardisering, og antallet af<br />

forskellige alfabeter er efterh˚anden reduceret. I det følgende omtales de tre alfabeter, man<br />

hyppigst støder p˚a i dag: ISOs 7 bit-kode, ISOs 8 bit-kode og “Unicode”.<br />

1.7.1 ISO’s 7 bit-kode<br />

Sædvanlig maskinskreven tekst er ogs˚a digital: Alfabetet best˚ar af de forskellige skrifttegn<br />

(ogs˚a kaldet grafemer), maskinen kan frembringe, og positionerne er anslagene mod papiret.<br />

En af mulighederne for en position er ikke at have noget grafem tilknyttet; denne mulighed<br />

16


egnes med blandt skrifttegnene og kaldes et blanktegn. Det mest almindeligt anvendte grafiske<br />

tegnsæt ASCII (American Standard Code for Information Interchange) har udviklet<br />

sig fra det internationale fjernskriveralfabet og er i let modificeret form blevet anerkendt<br />

af den internationale standardiseringsorganisation ISO som standarden ISO 646. Det har<br />

128 forskellige tegn, som b˚ade omfatter grafiske tegn (blanktegn, cifre, bogstaver og specialtegn)<br />

og forskellige ikke-grafiske tegn til opstilling, styring med mere, se tabel 1.1. Da man i<br />

totalssystemet netop f˚ar 128 talord ved at bruge 7 bit, kaldes koden ogs˚a for ISOs 7 bit-kode.<br />

kode 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15<br />

0 NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI<br />

16 DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US<br />

32 SP ! " £/# $/ ❡ % & ’ ( ) * + , - . /<br />

48 0 1 2 3 4 5 6 7 8 9 : ; < = > ?<br />

64 @ A B C D E F G H I J K L M N O<br />

80 P Q R S T U V W X Y Z Æ/[ Ø/\ ˚A/] ^<br />

96 ` a b c d e f g h i j k l m n o<br />

112 p q r s t u v w x y z æ/{ ø/| ˚a/} ¯/~ DEL<br />

Tabel 1.1: DS/ISO 646 og DS 2089.<br />

At alfabetet primært er konstrueret med henblik p˚a transmission, afspejler sig i det store<br />

antal ikke-grafiske tegn; nogle af disse styrer tegnenes opstilling eller andre terminalfunktioner<br />

og forklares i tabellen nedenfor (styretegnenes tilsigtede virkning kan ses i appendiks A).<br />

Som man kan se, stammer mange af betegnelserne fra den gang, hvor en terminal var en<br />

skrivemaskine med valse og slæde.<br />

kode forkortelse navn virkning<br />

7 BEL Bell egentlig “klokke”: f˚ar terminalen til at afgive lyd (sige<br />

“bip”)<br />

8 BS Backspace tilbagerykningstegn (én position mod venstre)<br />

9 HT Horizontal tabuleringstegn: fremrykning mod højre til næste ta-<br />

tabulation buleringsmærke<br />

10 LF Line feed linjeskift: en linje nedad; egentlig uden ændring af<br />

positionen i tværg˚aende retning (og skift til ny linje<br />

styres da af to tegn, i rækkefølgen CR+LF), men hvis<br />

udstyret tillader det, kan man vælge at lade dette<br />

tegn (som da ogs˚a forkortes NL, New line) betyde<br />

skift til forrest p˚a næste linje<br />

11 VT Vertical tabulation<br />

tabulering nedad<br />

12 FF Form feed sideskift<br />

13 CR Carriage<br />

return<br />

vognretur: tilbagerykning til linjens begyndelse<br />

32 SP Space blanktegn<br />

P˚a en skærm (og p˚a papir) er tegn stillet op i to dimensioner; omformningen til en endimensional<br />

række finder sted ved, at teksten opfattes som en sekvens af vandrette linjer,<br />

17


som opstilles efter hinanden, idet der efter hver linje indsættes et linjeskift; blanke dele<br />

yderst til højre p˚a linjerne er uden betydning og kan udelades. Som et eksempel vil teksten<br />

kunne sendes som sekvensen<br />

Her er et<br />

eksempel p˚a,<br />

hvorledes en tekst transmitteres.<br />

Her er et ✛eksempel ✁ p˚a, ✛hvorledes ✁ en tekst transmitteres. ✛✁<br />

hvor blanktegn (kode 32) er vist som og ny-linje-tegn (kode 10) som ✛. ✁<br />

Et tastatur behøver ikke at have en tast for hvert tegn, der skal kunne frembringes: flere<br />

taster kan benyttes i kombination. Der kan være parallelle skiftenøgler, som skal holdes nede,<br />

samtidig med at man trykker p˚a en anden tast. Princippet kendes fra den m˚ade, hvorp˚a man<br />

p˚a en sædvanlig skrivemaskine frembringer de store bogstaver, men ud over denne nøgle,<br />

betegnet “Shift”, kan en arbejdsplads have skiftenøgler betegnet “Option” (eller “Alt” eller<br />

“Select”) og “Ctrl”. Det er almindeligt at lade “Ctrl” virke p˚a den m˚ade, at man f˚ar dannet<br />

det tegn, hvis kode er 64 (eller 96) mindre end det tegn, man ellers ville f˚a (se tabel 1.1).<br />

I mange programmeringssprog kan en beregning, der er løbet løbsk, standses ved, at man<br />

sender ETX (kode 3); det sker s˚a ved, at “Ctrl” holdes nede, mens man taster c.<br />

Et serielt ikke-l˚asende skiftetegn eller undvigelsestegn (eng.:escape character) ændrer det<br />

umiddelbart efterfølgende tegns betydning; p˚a mange tastaturer fungerer accent-taster p˚a<br />

den m˚ade: accenten placeres over det efterfølgende tegn; tasten “Esc”, der sender tegnet<br />

ESC (kode 27), har en tilsvarende virkning. Man kan ogs˚a have l˚asende skiftetegn, hvor<br />

et s˚akaldt skift-ud-tegn ændrer betydning for alle efterfølgende tegn, indtil der kommer et<br />

skift-ind-tegn.<br />

I modsætning til konstruktionen p˚a en gammeldags skrivemaskine er det p˚a en arbejdsplads<br />

s˚adan, at den tast (mærket “Caps Lock”), der l˚aser til store bogstaver, ikke sætter<br />

den normale skiftenøgle ud af funktion: det er nemlig kun tasterne med bogstaver p˚a, hvis<br />

virkning ændres; for de andre taster er det stadig nødvendigt at kombinere med skiftenøglen,<br />

hvis man vil frembringe tegnet for oven p˚a tasten.<br />

Gruppering<br />

Trods standardiseringsbestræbelserne er der en række forhold, man ikke har kunnet enes<br />

om.<br />

Linjeskift. Svarende til, at overgangen fra en linje til den næste egentlig kombinerer to<br />

bevægelser (tilbage til venstre margin + en linje ned), skulle linjeskift oprindeligt angives<br />

CR (kode 13) + LF (kode 10), og s˚adan er konventionen ogs˚a i operativsystemerne MS<br />

Windows 95/98/. . .<br />

Standarden˚abner mulighed for, at man kan nøjes med med LF (kode 10); dette alternativ<br />

har man valgt under operativsystemet Unix.<br />

Ogs˚a p˚a Macintosh nøjes man med ét tegn, men det er (i strid med standarden) CR<br />

(kode 13).<br />

18


Mellemrum. Horisontal tabulering og blanktegn (og kombinationer af dem) danner mellemrum<br />

mellem de grafiske tegn, men der er ikke enighed om, hvorvidt blanktegnet selv skal<br />

klassificeres som grafisk eller ej.<br />

I programmeringssproget Standard ML har man en lang række prædikater til klassifikation<br />

af tegn (se [5, D.3.3]), og man har valgt, at om blanktegn skal Char.isPrint (is a<br />

printable character) være sandt, men Char.isGraph (is a graphical character) være falsk.<br />

De danske bogstaver<br />

Den oprindelige ide i ISO 646 var at holde syv pladser (koderne 64, 91, 92, 93, 123, 124,<br />

125, med mulighed for udvidelse til ti pladser ved ogs˚a at inddrage kode 94, 96 og 126)<br />

ledige til nationale udvidelser af alfabetet. Desuden kunne anførelsestegn, apostrof, komma,<br />

pilespids-opad og overstregning i kombination med tilbagerykningstegn bruges som accenterne<br />

henholdsvis trema (to prikker), aigu, cedille, cirkumfleks og tilde.<br />

Dansk Standardiseringsr˚ad foreskrev derfor i DS 2089, at æ, ø, ˚a, Æ, Ø og ˚A skulle<br />

placeres som vist i tabel 1.1.<br />

Desværre er de tegn, som derved bliver utilgængelige — først og fremmest kantede og<br />

krøllede parenteser — vanskelige at undvære inden for programmering, og det m˚a konstateres,<br />

at den danske standard aldrig rigtig er sl˚aet an. Hertil kommer, at otte bit, svarende til<br />

et repertoire p˚a 256 muligheder, er en mere naturlig enhed p˚a nutidens maskiner end syv 2 .<br />

ISO 646-koden afløses derfor i stigende grad af mere omfattende tegnsæt.<br />

1.7.2 ISO’s 8 bit-kode<br />

En gruppe p˚a 8 bit, der kaldes en byte 3 og kan rumme 256 forskellige muligheder, er en naturlig<br />

dataenhed for computere. Lagerstørrelser m˚ales som regel i byte, maskinens naturlige<br />

repræsentationer af heltal, reelle tal og ordrer vil være et helt antal byte, og ofte er en byte<br />

den mindste adresserbare lagerenhed.<br />

ISO 8859 standardiserer en familie af alfabeter indeholdende lutter grafiske tegn med<br />

koder mellem 0 og 255, s˚a de kan rummes inden for en byte. Af dem er det s˚akaldte “latinske<br />

alfabet nummer 1” indrettet med henblik p˚a at indeholde alle de tegn, som kræves<br />

i vesteuropæiske sprog (nærmere bestemt catalansk, dansk, engelsk, fransk, færøsk, irsk,<br />

islandsk, italiensk, nederlandsk, norsk, portugisisk, spansk, svensk og tysk). Til gengæld er<br />

det ikke meningen, at der skal bruges mere end én kode for noget grafisk tegn inden for<br />

denne standard (bogstaver med accent regnes med andre ord for selvstændige tegn og m˚a<br />

ikke dannes ved kombinationer og tilbagerykning). Som det fremg˚ar af tabel 1.2 er der ved<br />

denne udvidelse ikke alene blevet plads til æ/Æ, ø/Ø og ˚a/˚A, men ogs˚a til svensk og tysk<br />

ä/ Ä og ö/Ö, tysk ü/Ü og ß, islandsk og færøsk d-/D- med meget mere.<br />

Al standardisering er et kompromis, men det er alligvel forbavsende, at fransk œ/Œ ikke<br />

er lagt ind i steden for ÷/×. Forklaringen er, at medlemmerne af standardiseringskommissionen<br />

har defineret Πsom ligatur (typografisk sammenskrivning) af O og E, ligesom (i<br />

nederlandsk) ij og IJ er ligaturer, eller fi og fl er ligaturer for fi og fl.<br />

2 selv om den ottende bit ved asynkron transmission kunne bruges som paritetskontrol.<br />

3 De foresl˚aede fordanskninger til stavelse eller oktet er ikke sl˚aet an.<br />

19


kode<br />

0<br />

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15<br />

16<br />

0–31 ikke i tabel<br />

32 SP ! " # $ % & ’ ( ) * + , - . /<br />

48 0 1 2 3 4 5 6 7 8 9 : ; < = > ?<br />

64 @ A B C D E F G H I J K L M N O<br />

80 P Q R S T U V W X Y Z [ \ ] ^<br />

96 ` a b c d e f g h i j k l m n o<br />

112 p<br />

128<br />

q r s t u v w x y z { | } ~<br />

ikke i<br />

tabel<br />

144<br />

127–159 ikke i tabel<br />

160 NBSP ¡ c| £ ❡ Y= § ¨ c○ ā<br />

><br />

Ë<br />

^U<br />

1<br />

4<br />

Ì<br />

Ü<br />

1<br />

2<br />

Í<br />

´Y<br />

3<br />

4<br />

^I<br />

bp<br />

¿<br />

Ï<br />

ß<br />

224 à á ^a ~a ä ˚a æ ç è é ^e ë ì í ^ı ï<br />

240 d- ~n ò ó ^o ~o ö ÷ ø ù ú ^u ü ´y bp ¨y<br />

Tabel 1.2: ISO 8859 Part 1: Latin alphabet No. 1.<br />

Vores æ/Æ blev ogs˚a i begyndelsen kaldt en ligatur og først ved en senere rettelse accepteret<br />

som selvstændigt bogstav!<br />

HTML-kodning<br />

Ved at udnytte elektronisk lagrings muligheder for effektiv adgang til alverdens oplysninger<br />

kan man bygge systemer, der ud fra henvisninger i en tekst hurtigt finder frem til det,<br />

der henvises til. Med den slags systemer til r˚adighed ændrer begrebet “tekst” karakter:<br />

snarere end en lineær strøm fornemmes den som en struktur med dybde i, og betegnelsen<br />

“hypertekst” er foresl˚aet indført for at dække dette nye begreb. Kort kan en hypertekst<br />

m˚aske forklares som en “tekst med aktive henvisninger”, og det er klart, at der s˚a bliver<br />

brug for et særligt formuleringssprog, et “afmærkningssprog”, at skrive den slags tekster i.<br />

HTML st˚ar for “HyperText Markup Language” og er dels en betegnelse for selve begrebet<br />

(afmærkningssprog til at skrive hypertekster i), dels en betegnelse for et ganske bestemt<br />

s˚adant sprog, der er vidt udbredt og blandt andet benyttes af systemerne “Netscape” og<br />

“Internet Explorer”. De forhold, som omtales her, gælder version 2.0 af HTML. Sproget er<br />

s˚adan indrettet, at man ved udformning af kildeteksterne bag HTML-dokumenter kan nøjes<br />

med tegn fra ISOs 7 bit-sæt.<br />

For i teksten “se DIKUs hjemmeside” i et HTML-dokument at f˚a ordene “DIKUs hjemmeside”<br />

til at henvise til DIKUs hjemmeside skal man indføre et s˚akaldt “anker” i kildeteksten:<br />

se


for eksempel . . . fremhævet tekst. . . og stærk fremhævelse (som regel med fedt)<br />

angives . . . stærkt fremhævet tekst. . . .<br />

Som det ses, bruger HTML vinkelparenteser til den slags direktiver, og tegnene “mindre<br />

end” og “større end” kan derfor ikke bruges i en HTML-tekst i deres normale betydning,<br />

men kodes som henholdsvis &lt; og &gt;. Det betyder igen, at og-tegnet (&)<br />

ikke kan bruges, men kodes &amp;. Eksemplet ovenfor viser, at parametre i direktiver (her<br />

http://www.diku.dk/index.html) skal sættes i anførelsestegn; et eventuelt anførelsestegn<br />

i en s˚adan parameter skal derfor skrives &quot;. De grafiske tegn fra tabel 1.1, der ikke uden<br />

videre kan indg˚a i en HTML-tekst, er dermed<br />

tegnet skal i HTML-kildetekster skrives<br />

& &amp;<br />

< &lt;<br />

> &gt;<br />

" (i direktivparametre) &quot;<br />

Ogs˚a alle tegnene i ISO 8859-1 Latin 1 kan i HTML beskrives via sekvenser af 7 bittegn.<br />

Hovedreglen er, at tegnet med valør n (som decimalt talord) f˚as ved at skrive &#n;<br />

— et paragraftegn skrives alts˚a for eksempel &#167; — men for bogstaverne er der ogs˚a<br />

mnemotekniske koder som vist i nedenst˚aende tabel.<br />

HTML HTML HTML HTML HTML HTML<br />

À &Agrave; È &Egrave; Ì &Igrave; Ò &Ograve; Ù &Ugrave; Ç &Ccedil;<br />

Á &Aacute; É &Eacute; Í &Iacute; Ó &Oacute; Ú &Uacute; ´ Y &Yacute;<br />

 &Acirc; Ê &Ecirc; Î &Icirc; Ô &Ocirc; Û &Ucirc; D- &ETH;<br />

à &Atilde; Õ &Otilde; Ñ &Ntilde;<br />

Ä &Auml; Ë &Euml; Ï &Iuml; Ö &Ouml; Ü &Uuml;<br />

˚A &Aring; Æ &AElig; Ø &Oslash; bp &THORN;<br />

à &agrave; è &egrave; ì &igrave; ò &ograve; ù &ugrave; ç &ccedil;<br />

á &aacute; é &eacute; í &iacute; ó &oacute; ú &uacute; ´y &yacute;<br />

â &acirc; ê &ecirc; î &icirc; ô &ocirc; û &ucirc; d- &eth;<br />

ã &atilde; õ &otilde; ñ &ntilde;<br />

ä &auml; ë &euml; ï &iuml; ö &ouml; ü &uuml; ¨y &yuml;<br />

˚a &aring; æ &aelig; ß &szlig; ø &oslash; bp &thorn;<br />

1.7.3 Unicode<br />

Selv med 256 muligheder er der ikke plads til alle ønskelige tegn, og i 1989 indledtes arbejdet<br />

med at definere et mere omfattende tegnsæt. M˚alet var at løse problemet ved én gang for alle<br />

at sørge for et tilstrækkelig stort rum af muligheder. I 1991 blev “The Unicode Consortium”<br />

et aktieselskab med ansvar for at definere og vedligeholde en 16-bit-kode med plads til<br />

alle tegn i alle verdens sprog. Den seneste version 2.0 af “The Unicode Standard” f˚as som<br />

bog[16]; ud af de 65536 muligheder er mere end halvdelen udnyttet (helt præcist defineres<br />

38885 pladser), men s˚a er der ogs˚a b˚ade blevet plads til kyrilliske og græske bogstaver,<br />

lydskrift, arabiske tegn, thai, og til 20902 kinesisk-japansk-koreanske tegn samt en del mere.<br />

Man har sørget for, at de forreste 256 pladser falder sammen med ISO’s 8-bit-kode.<br />

21


Den internationale standardiseringsorganisation er g˚aet et skridt videre og har med standarden<br />

ISO/IEC 10646 ˚abnet mulighed for en 31-bit-kode. Foreløbigt er dog kun pladser<br />

blandt de første 2 16 udnyttet, og de stemmer overens med “Unicode”.<br />

1.8 Funktionsbegrebet<br />

Det fremg˚ar af betegnelsen, at begrebet funktion spiller en central rolle i <strong>funktionsprogrammering</strong>,<br />

men det kan være p˚a sin plads at gøre opmærksom p˚a nogle forskelle mellem<br />

matematikeres og datalogers brug af ordet.<br />

1.8.1 Den matematiske opfattelse<br />

Hvis to varierende størrelser x og y hører sammen p˚a en s˚adan m˚ade, at der til hver værdi<br />

af x netop findes én værdi af y, siges y at være en funktion af x, og x kaldes den uafhængige<br />

og y den afhængige variabel.<br />

Ordet funktion bruges ogs˚a mere abstrakt om selve den sammenhæng, der er mellem x og<br />

y, og i denne betydning taler man ogs˚a om en afbildning. Lad f betegne en abstrakt funktion<br />

eller afbildning. Den til en forelagt værdi v (som i denne forbindelse ogs˚a kaldes funktionens<br />

“argument”) af den uafhængige variabel hørende værdi af den afhængige variabel betegnes<br />

da f(v) og læses “f af v” eller “f anvendt p˚a v”.<br />

Fra skolen kendes funktioner, hvor x og y er tal, men funktionsbegrebet er ikke begrænset<br />

hertil: størrelserne kunne ogs˚a være tegn, tekster, figurer, strukturer eller hvad som helst<br />

andet.<br />

Total funktionel relation<br />

Mere generelt vil en matematiker kalde en forbindelse mellem en mængde af x-værdier og<br />

en mængde af y-værdier for en relation. Mængden af værdier, x kan antage, kan man kalde<br />

primærmængden for relationen, og mængden af mulige værdier af y betegnes da tilsvarende<br />

sekundærmængden.<br />

Relationen er en funktion, hvis der til hver værdi af x netop svarer én værdi af y. Dette<br />

krav kan opløses i to dele: At der til hver værdi af x højst svarer en værdi af y udtrykker<br />

man ved at sige, at relationen er funktionel, eller at y er en partiel funktion af x. At der til<br />

hver værdi af x mindst findes en værdi af y, betyder, at relationen er total.<br />

1.8.2 Den datalogiske opfattelse<br />

Hvis y er en partiel funktion af x, vil der for hver værdi af x i primærmængden enten ingen<br />

y-værdi findes, eller ogs˚a vil der netop findes én tilhørende værdi af y. Den partielle funktions<br />

definitionsmængde best˚ar af de værdier af x, for hvilke der faktisk findes en tilhørende værdi<br />

af y. En partiel funktion, der har hele primærmængden som sin definitionsmængde, siges at<br />

være total (eller blot “at være en funktion”).<br />

Funktioner er ofte nyttige ved beregninger, men for at kunne bruge en funktion i et<br />

program m˚a man beskrive den, det vil sige i programmet medtage en tekst, som forklarer,<br />

hvilken funktion der er tale om. At dataloger p˚a den m˚ade strengt taget ikke arbejder med<br />

22


“funktioner”, men med “funktionsbeskrivelser” eller “funktionstekster”, giver anledning til<br />

to vigtige afvigelser i forhold til det matematiske funktionsbegreb:<br />

1. Det er for det første et centralt resultat inden for teoretisk datalogi, at intet automatisk<br />

system ud fra en forelagt funktionstekst kan afgøre, om den beskrevne funktion er total<br />

eller ej. N˚ar man i programmer anvender en funktion f p˚a et argument v, m˚a man<br />

som hovedregel være forberedt p˚a, at det ikke nødvendigvis fører til en funktionsværdi<br />

— f kunne ogs˚a være udefineret for værdien v.<br />

For en datalog vil en “funktion” derfor næsten altid være det, en matematiker mere<br />

korrekt ville kalde “en partiel funktion”.<br />

2. Den anden forskel har at gøre med begrebet “lighed”. Vi vil i næste kapitel se, at<br />

nedenst˚aende erklæringer er korrekte funktionsdefinitioner i programmeringssproget<br />

Standard ML:<br />

fun f(n) = n + n<br />

fun g(n) = 2 * n<br />

Matematisk set er de to funktioner f og g ens, idet de har identisk samme virkning<br />

(de fordobler deres heltallige argument), men der er tale om to forskellige funktionsbeskrivelser<br />

og dermed to forskellige realiseringer.<br />

For dataloger er funktionsbeskrivelserne vigtige (forskellige realiseringer af samme matematiske<br />

funktion kunne have vidt forskelligt forbrug af køretid og andre ressourcer), og i<br />

datalogisk undervisningsmateriale skal termen “funktion” normalt forst˚as som “funktionsbeskrivelse”<br />

(programtekst til beregning af en ikke nødvendigvis total funktion).<br />

1.9 Litteratur<br />

Den opfattelse af faget datalogi, som kommer til udtryk i dette kapitel, er i høj grad præget<br />

af tanker, der i 1960’erne udgik fra en gruppe medarbejdere ved Regnecentralen (først et<br />

institut under Akademiet for de Tekniske Videnskaber, senere omdannet til aktieselskab) og<br />

blandt dem først og fremmest Peter Naur, senere professor ved Datalogisk Institut. Nogle af<br />

disse tidlige overvejelser er formuleret i [4] og senere samlet i [10]. En række af Peter Naurs<br />

bidrag til udformning af faget kan ses i [11].<br />

23


Kapitel 2<br />

Dialog med Standard ML<br />

De første højere programmeringssprog (Fortran, Lisp og Algol) blev konstrueret omkring<br />

1960. Helt s˚a gammelt er ML ikke, idet det blev fastlagt over en periode, der begyndte<br />

cirka 1978. Varianten Standard ML, som vi benytter her, blev defineret præcist i 1990 [7],<br />

[8] med en revision i 1997 [9]. I disse indledende <strong>noter</strong> benyttes dialekten Moscow ML (fra<br />

omkring 1998), se [14].<br />

Navnet er en forkortelse af Meta Language, og metasprog er logikeres betegnelse for<br />

sprog brugt til at forklare eller behandle andre sprog i. Oprindeligt blev ML konstrueret<br />

som et hjælpesprog i forbindelse med projektet LCF (Logic of Computable Functions), der<br />

gik ud p˚a at føre beviser om funktioner. Meningen var, at man i ML skulle kunne finde,<br />

udtrykke og f˚a verificeret beviser om et system af funktionsdefinitioner.<br />

Tilbage fra LCF-projektet st˚ar formalismen ML, der viste sig velegnet til alle former for<br />

beregninger. (Sproget ML er universelt i foreg˚aende kapitels forstand.)<br />

2.1 Dialogen<br />

Uden her at komme ind p˚a, hvordan det foreg˚ar (se eventuelt<br />

http://www.dina.kvl.dk/~sestoft/mosml.html) vil vi g˚a ud fra, at man har f˚aet installeret<br />

Moscow ML p˚a sin computer. N˚ar systemet mosml er blevet aktiveret og har<br />

præsenteret sig, slutter det af med et klar-symbol (eng.:prompt) (her best˚aende af linjeskift,<br />

bindestreg og blanktegn), hvorefter det afventer inddata fra brugeren. P˚a det tidspunkt, hvor<br />

disse <strong>noter</strong> blev skrevet 1 , s˚a det s˚aledes ud p˚a skærmen (idet vi lader dollartegnet st˚a for<br />

operativsystemets klar-symbol og viser den blinkende markør (eng.:cursor) som et udfyldt<br />

rektangel):<br />

$ > mosml<br />

Moscow ML version 2.01 (January 2004)<br />

Enter ‘quit();’ to quit.<br />

-<br />

Indtaster man her et udtryk (eng.:expression) (efterfulgt af semikolon og nylinjetegn),<br />

vil systemet beregne dets værdi, udskrive en respons, der angiver denne værdi, og derefter<br />

p˚any skrive et klar-symbol og s˚a afvente yderligere inddata:<br />

1 juni 2005<br />

25


2 + 2;<br />

> val it = 4 : int<br />

-<br />

For ML har hver værdi en bestemt type, og 4 er et heltal (eng.:integer), i ML forkortet<br />

til int. Det ses, at i tilgift til at beregne vores udtryks værdi oplyser systemet ogs˚a, hvilken<br />

type værdien har. Kolonet mellem værdi og type kan læses “af type”.<br />

Dette er ML-systemets grundlæggende virkem˚ade: I en fortsat dialog veksles der mellem,<br />

at brugeren angiver en s˚akaldt top-niveau-erklæring, og at systemet som svar rapporterer sin<br />

evaluering af den. En af de mulige former, en top-niveau-erklæring kan have, er et udtryk,<br />

og systemet vil svare med udtrykkets værdi og type.<br />

Læg mærke til, at indgangsudtryk skal afsluttes med semikolon. Man kan godt lade et<br />

udtryk strække sig over flere linjer:<br />

2 +<br />

Selv om vi har tastet et linjeskift, sker der ingenting, for udtrykket er ikke afsluttet. Vi<br />

fortsætter med den anden operand og et linjeskift:<br />

2<br />

men der sker stadig ikke noget: semikolonet mangler! N˚ar det sættes (og man bagefter taster<br />

linjeskift), udløses beregningen:<br />

;<br />

> val it = 4 : int<br />

-<br />

Udtryk kan naturligvis indeholde mere end én operation (her og i de følgende eksempler<br />

udelades klar-symbolet):<br />

4 - (1 + 2 + 3);<br />

> val it = ~2 : int<br />

Der er fem indbyggede operationer p˚a heltal: + (addition), - (subtraktion), * (multiplikation),<br />

div (heltallig divisionskvotient) og mod (rest ved heltalsdivision).<br />

2.1.1 Foranstillede (monadiske) operatorer<br />

P˚a nogle punkter afviger SML’s regler for, hvordan udtryk skal skrives, fra gængs matematisk<br />

notation. Det er blandt andet s˚adan, at SML’s m˚ade at analysere og gruppere udtryk<br />

p˚a gør det nødvendigt at kunne skelne mellem operatorer og funktioner: Intet symbol kan<br />

samtidigt b˚ade fungere som operator (med to operander) og som funktion (med et argument).<br />

Symboler, der skal kunne foranstilles en enkelt operand, klassificeres af SML som funktioner;<br />

derfor er det ikke muligt at give en dyadisk operator (som skal skrives mellem sine<br />

to operander) samme symbol som en monadisk operator (der stilles foran sin operand).<br />

Eftersom symbolet + er den dyadiske additionsoperator, er det ikke lovligt at bruge<br />

symbolet som monadisk operator:<br />

26


+ 10;<br />

! Toplevel input:<br />

! + 10;<br />

! ~~~~<br />

! Ill-formed infix expression<br />

Ulykken er naturligvis ikke s˚a stor; et foranstillet plus kan jo bare undværes. Værre er<br />

det med foranstillet minus — det ville være irriterende at være nødt til at skrive minus ti<br />

som 0 - 10. Den løsning, man har valgt i SML (men de fleste andre programmerinssprog<br />

h˚andterer problemet p˚a anden vis), er at bruge symbolet ~ (tilde) som monadisk minus:<br />

~ 10;<br />

> val it = ~10 : int<br />

Ud over monadisk minus er der en indbygget monadisk operator mere p˚a heltal: Den<br />

numeriske eller absolutte værdi |n| af et tal n skal i SML skrives abs n.<br />

Tilde har i øvrigt i princippet to forskellige funktioner i SML: Dels betegner symbolet<br />

som nævnt funktionen til skift af fortegn; dels bruges det, n˚ar man skal anføre en negativ<br />

talkonstant, hvor tilden skrives lige foran det forreste ciffer 2 .<br />

Her er et eksempel til illustration af forskellen. Først som negativmærke i en talkonstant:<br />

abs ~7;<br />

> val it = 7 : int<br />

Og derefter som monadisk operator:<br />

abs ~ 7;<br />

! Toplevel input:<br />

! abs ~ 7;<br />

! ~~~<br />

! Overloaded abs cannot be applied to argument(s) of type int -> int<br />

Fejlmeldingen skyldes, at SML (efter regler, vi senere vil forklare nærmere) vil gruppere de<br />

tre symboler fra venstre: (abs ~) 7. SML tror med andre ord, at vi vil tage den numeriske<br />

værdi af monadisk minus. N˚ar ~ bruges som fortegnsvendefunktion, kan det derfor være<br />

nødvendigt med en parentes:<br />

abs (~ 7);<br />

> val it = 7 : int<br />

2.1.2 Sandhedsværdi<br />

Ligesom de hele tal udgør sandhedsværdierne en grundtype, men den har kun to egentlige<br />

værdier: true og false:<br />

3 < 5;<br />

> val it = true : bool<br />

2 Vi s˚a allerede denne anvendelse i de to foreg˚aende systemsvar ~2 og ~10.<br />

27


Typen af sandhedsværdier eller logiske værdier (eng.:Booleans) forkortes bool, benævnt<br />

efter den britiske matematiker George Boole (1815–1864), der blandt andet arbejdede med<br />

algebraiske operationer med egenskaber analoge til logisk “og” og logisk “eller”.<br />

I valgudtryk af formen if ... then ... else ... skal der mellem if og then anbringes<br />

et udtryk af logisk type:<br />

if 1 + 1 = 2 then ~3 + ~3 else ~3 - ~3;<br />

> val it = ~6 : int<br />

2.1.3 Værdierklæring<br />

Eksemplerne har vist top-niveau-erklæringer i form af udtryk. En anden mulig form for<br />

top-niveau-erklæring er en værdi-erklæring, der bruges til at benævne en beregnet værdi:<br />

val Chr4aar = 1588 - 1648;<br />

> val Chr4aar = ~60 : int<br />

Herefter kan det indførte navn benyttes i udtryk, og virkningen vil være, som havde man<br />

anført værdien direkte:<br />

abs Chr4aar < 50;<br />

> val it = false : bool<br />

Systemets svar p˚a et indtastet udtryk har omtrent samme form som en værdierklæring<br />

af navnet it, og en top-niveau-erklæring af formen<br />

udtryk ;<br />

er simpelt hen en forkortet skrivem˚ade for<br />

val it = udtryk ;<br />

Navnet it er med andre ord til r˚adighed, n˚ar udtryk skrives ned. P˚a den m˚ade kan man<br />

bekvemt f˚a angivet mellemresultater, inden der regnes videre p˚a dem: (I hvilket omfang<br />

parenteser er nødvendige, og i hvilket omfang de kan undværes, vil blive behandlet i de<br />

følgende afsnit.)<br />

(8 * 7) div (1 * 2);<br />

> val it = 28 : int<br />

- it * 6 div 3;<br />

> val it = 56 : int<br />

- it * 5 div 4;<br />

> val it = 70 : int (alt i alt har vi nu f˚aet beregnet 8·7·6·5<br />

1·2·3·4 )<br />

2.1.4 Afslutning<br />

For igen at forlade SML-systemet skrives (husk parenteser, semikolon og linjeskift)<br />

$<br />

quit();<br />

hvorved man bringes tilbage til operativsystem-niveau.<br />

28


2.2 Gruppering<br />

Eksemplerne viser, hvordan udtryk i SML er bygget op af operatorer og operander, idet<br />

operatoren placeres mellem sine to operander. N˚ar et udtryk har mere end én operator,<br />

er yderligere afklaring nødvendig. Vi tillægger for eksempel 28 − 5 + 3 værdien 26 (nemlig<br />

3 lagt til differensen 28 − 5) og ikke 20 (der ville blive resultatet, hvis man trak summen<br />

♠+<br />

<br />

5 + 3 fra 28). Man kan vise grupperingen entydigt i et udtrykstræ ♠−<br />

❅<br />

3<br />

, men i<br />

❅<br />

28 5<br />

lineær notation kan det være nødvendigt at sætte parenteser. Hvis det, man gerne vil have<br />

♠−<br />

❅<br />

udregnet, er<br />

28<br />

♠+ , bliver man nødt til at skrive 28 − (5 + 3) (eller 28 − 5 − 3),<br />

❅<br />

5 3<br />

mens parentesen i (28 − 5) + 3 kan underforst˚as.<br />

2.2.1 Associering<br />

Man udtrykker gerne de beskrevne grupperingsregler ved at sige, at operatorerne + og - associerer<br />

fra venstre: i flerleddede konstruktioner med + og - virker operationerne fra venstre<br />

mod højre; hvis det er en anden rækkefølge, man er ude efter, kan man sætte parenteser.<br />

Ogs˚a operatorerne * og div associerer fra venstre: 120 div 4*3 vil i ML blive udregnet<br />

som (120 div 4) * 3; hvis der b˚ade skal divideres med 4 og med 3, m˚a man skrive<br />

120 div (4*3) (eller 120 div 4 div 3).<br />

I eksemplet (8 * 7) div (1 * 2) ovenfor er den første parentes derfor overflødig, men<br />

den anden ikke; uden parenteser kunne det skrives 8 * 7 div 1 div 2.<br />

Associerer en binær operator fra højre, betyder det naturligvis helt analogt, at der i<br />

et udtryk med flere forekomster af operatoren er underforst˚aet parenteser sat fra højre. Vi<br />

vil senere i forbindelse med lister møde operatoren :: (hvis betydning kan udtrykkes: “sat<br />

foran”), der i ML associerer fra højre. Udtrykket a :: b :: c :: x skal med andre ord<br />

forst˚as som a :: (b :: (c :: x)).<br />

Det har været hævdet, at konstruktioner i dagligsproget naturligt associerer fra højre<br />

— siger man “indgang ad døren i huset p˚a toppen af bakken”, s˚a betyder det jo “indgang<br />

ad (døren i (huset p˚a (toppen af bakken)))” — og i programmeringssproget APL tog man<br />

konsekvensen og lod alle operatorer associere fra højre. Alligevel er associering fra venstre<br />

langt det almindeligste i de fleste programmeringssprog og ogs˚a i SML (hvor vi kun skal<br />

møde tre undtagelser fra denne regel).<br />

2.2.2 Prioritet<br />

Selv om b˚ade - og * associerer fra venstre, giver SML 52-5 * 7 værdien 17, i overensstemmelse<br />

med den normale læsem˚ade.<br />

Det, som her gør sig gældende, er, at operatorer kan binde med forskellig prioritet: * og<br />

div binder stærkere end + og -. SML arbejder med ti forskellige prioriteter, som betegnes<br />

med cifrene fra 0 til 9 med et højere nummer som betegnelse for stærkere binding. De<br />

29


operatorer, der skal benyttes i denne del af kurset, er samlet i tabel 2.1, hvor de er vist efter<br />

aftagende prioritet tillige med deres associeringsretning. (En tilsvarende tabel findes som<br />

Table D.4 i lærebogen [5].)<br />

prioritet operator retning<br />

7 * div mod / associerer fra venstre<br />

6 + - ^ associerer fra venstre<br />

5 :: @ associerer fra højre<br />

4 = < >= associerer fra venstre<br />

3 o associerer fra venstre<br />

Tabel 2.1: Binære operatorer i mosml og deres associeringsretning og bindingsprioritet.<br />

Vi har ikke mødt alle disse symboler endnu, men =, , = benyttes i sammenligninger,<br />

/ betegner division af reelle tal, ^ binder tekster sammen, :: og @ virker i<br />

forbindelse med lister, og o betegner funktionssammensætning.<br />

2.3 Funktioner<br />

I et <strong>funktionsprogrammering</strong>ssprog opfattes en funktion som en værdi p˚a linje med andre<br />

værdier s˚asom tal, tekster, lister osv. Et udtryk kan s˚aledes godt evaluere til en værdi, der er<br />

en funktion (vi vil ikke længere anføre de tegn (- og >), som henholdsvis indleder brugerens<br />

inddata og systemets svar):<br />

abs;<br />

val it = fn : int -> int<br />

Selve værdien af udtrykket er det alt for omfattende at skrive ud 3 , s˚a vi f˚ar bare med<br />

forkortelsen fn at vide, at der er tale om en funktion, men ligesom for simple værdier gives<br />

der udførlig besked om typen: Betegnelsen τ -> τ ′ (hvor τ og τ ′ er typer) er MLs notation<br />

for typen af funktioner med argument af type τ og værdi af type τ ′ . Her f˚ar vi alts˚a at vide,<br />

at abs kan anvendes p˚a et heltal og i s˚a fald giver et heltal som funktionsværdi.<br />

(Hvis man ikke kan huske en funktions type, er det smart, at man bare kan bede om at<br />

f˚a evalueret selve funktionen (uden argumenter): s˚a vil systemets svar angive typen.)<br />

Tilde betegner funktionen til beregning af det modsatte af et tal:<br />

~ (Chr4aar);<br />

val it = 60 : int<br />

~;<br />

val it = fn : int -> int<br />

Man kan konstruere valgudtryk, der vælger mellem hvilke som helst værdier af samme type,<br />

ogs˚a for eksempel funktioner:<br />

3 En funktion fra mængden A til mængden B kan opfattes som en mængde af par (a, b) med lige s˚a mange<br />

par, som der er elementer i A.<br />

30


if 3 = 5 then ~ else abs;<br />

val it = fn : int -> int<br />

it (24);<br />

val it = 24 : int<br />

selv om det nok sjældent vil forekomme i praktiske anvendelser.<br />

2.3.1 Funktionserklæring<br />

I Standard ML er et stort antal nyttige funktioner defineret p˚a forh˚and. Vi har allerede<br />

mødt abs og ~, men man kan for eksempel i Appendix D i [5] se nogle af de mange andre<br />

biblioteksfunktioner.<br />

Man kan imidlertid ogs˚a konstruere sine egne funktioner (og størstedelen af et SMLprogram<br />

vil netop typisk best˚a af funktionserklæringer). En funktion lader til et argument<br />

svare en entydigt bestemt funktionsværdi, og i et programmeringssprog kan man naturligvis<br />

kun h˚andtere funktioner, hvor funktionsværdien kan beregnes ud fra argumentet. For at<br />

fastlægge en funktion kræves derfor<br />

• funktionens navn (s˚a den senere kan kaldes)<br />

• en anvisning p˚a, hvordan funktionsværdien findes ud fra argumentet<br />

En mulig m˚ade at præsentere disse oplysninger p˚a er<br />

fun funktionsnavn parametermønster = krop (2.1)<br />

Parametermønsteret kan indeholde et eller flere navne, som da kaldes funktionserklæringens<br />

formelle parametre; i simpleste tilfælde best˚ar parametermønsteret blot af en enkelt formel<br />

parameter. Ved igen at bruge de formelle parameternavne i funktionskroppen kan det vises,<br />

hvordan funktionsværdien skal beregnes.<br />

Her er nogle eksempler:<br />

fun kvadrat x = x * x<br />

fun skat indtgt = indtgt * 0.09 + (indtgt - 2125.0) * 0.42;<br />

val kvadrat = fn : int -> int<br />

val skat = fn : real -> real<br />

Vi bemærker, at selv om funktionerne blev defineret med nøgleordet “fun”, behandler systemet<br />

ikke funktioner anderledes end andre værdier: i kvitteringerne bruges “val” ligesom<br />

i taleksemplerne. Systemets svar viser ogs˚a, at det er funktionsnavnet, der huskes, mens<br />

parameternavne ikke omtales (de er kun “midlertidige pladsholdere” eller “bundne variable”,<br />

som man ogs˚a siger). Som nævnt ovenfor kan svaret ikke angive funktionens virkning (men<br />

blot skrive “fn” for “dette er en funktion”), hvorimod typen (efter kolonet) præciseres.<br />

Efter at man selv har defineret en funktion, kan den kaldes p˚a fuldstændig samme m˚ade<br />

som en biblioteksfunktion:<br />

kvadrat (317);<br />

val it = 100489 : int<br />

skat (15000.00);<br />

val it = 6757.5 : real<br />

31


De argumenter, funktionsnavne medgives, n˚ar de kaldes (her 317 og 15000.00), benævnes<br />

ogs˚a aktuelle parametre.<br />

2.3.2 Funktionskald, parenteser og prioritet<br />

I matematisk notation markeres funktionskald sædvanligvis ved, at der sættes parentes om<br />

argumentet: funktionen f kaldt med argumentet x skrives f(x).<br />

I Standard ML har man vedtaget, at funktionskald (som m˚a formodes at være den<br />

hyppigste operation i et <strong>funktionsprogrammering</strong>ssprog) kan underforst˚as og blot angives<br />

ved, at et udtryk (der derved opfattes som en funktion) stilles foran et andet (som s˚a<br />

opfattes som argument). I SML bruges parenteser først og fremmest til gruppering 4 , s˚a et<br />

deludtryk holdes sammen som en helhed. Ved funktionskald er det derfor tilladt, men ikke<br />

altid nødvendigt, at sætte parentes om argumentet:<br />

|123| kan i SML skrives abs 123 eller abs(123) eller (abs)123 eller (abs)(123)<br />

En eller anden form for adskillelse er der dog brug for: udtrykket abs123 ville blive opfattet<br />

som et nyt selvstændigt navnesymbol.<br />

Forestiller man sig et udtryk bygget op skiftevis af (ope)rander og (ope)ratorer<br />

rand ratorsymbol rand ratorsymbol rand . . . rand ratorsymbol rand<br />

s˚a har operationen “funktionsanvendelse” slet ikke noget symbol: Der kommer bare to operander<br />

lige efter hinanden.<br />

I udtryk, hvor der er flere funktionsanvendelser eller b˚ade funktionsanvendelse og almindelige<br />

operatorsymboler, bliver det nødvendigt med regler for associeringsretning og<br />

prioritet. Gruppering afklares af:<br />

Dette forklarer<br />

kvadrat 3+4;<br />

val it = 13 : int<br />

Funktionsanvendelse associerer fra venstre<br />

og har højere prioritet end nogen operator.<br />

— var det 7, man ville have kvadreret, skulle det have været skrevet kvadrat (3+4). Selv<br />

om det i starten kan føles lidt sært, er det nyttigt at lære sig SML’s regler, s˚a man kan<br />

slippe for at sætte s˚a mange parenteser; man vænner sig hurtigt til at skrive for eksempel<br />

f 1 + f 2 + f 3 i steden for f(1) + f(2) + f(3).<br />

M˚aske bør det nævnes, at selv om man normalt godt kan sætte parentes omkring funktionsdelen<br />

af et funktionskald (som i (f)317), er dette ikke tilladt i selve erklæringen (2.1).<br />

Alle udtryk underkastes en omhyggelig typekontrol: For at acceptere en funktionsanvendelse<br />

F -udtryk A-udtryk kræver SML, at F -udtryk har type τ → τ ′ og A-udtryk type<br />

τ; hele funktionsanvendelsesudtrykket f˚ar s˚a type τ ′ .<br />

4 Parenteser bruges ogs˚a i forbindelse med sæt (se afsnit 4.2) og sekvenser (se [7], [9] eller [12]).<br />

32


Som tidligere nævnt kan tilde indg˚a i en talkonstant, idet symbolet skrives umiddelbart<br />

foran det forreste ciffer som for eksempel i ~7. Skilles tilden fra det efterfølgende tal, bliver<br />

det til et applikationsudtryk med ~ som funktion og tallet som argument. Det samme<br />

gælder, hvis tilde st˚ar foran andet end et ciffer; ogs˚a ~Chr4aar vil blive opfattet som fortegnsskiftsfunktionen<br />

anvendt p˚a et argument (her Chr4aar). Dette er forklaringen p˚a, at<br />

selv om kvadrat ~7 lader sig beregne, g˚ar det galt, hvis man skriver kvadrat ~ 7 eller<br />

kvadrat ~Chr4aar: Eftersom funktionsanvendelse associerer fra venstre, ser systemet disse<br />

udtryk som (kvadrat ~) 7 henholdsvis (kvadrat ~) Chr4aar, hvilket ikke giver mening.<br />

(Kvadreringsfunktionen skal anvendes p˚a en heltallig værdi og kan ikke anvendes p˚a fortegnsskiftefunktionen.)<br />

2.3.3 Argumentsæt<br />

Funktioner af flere variable kan defineres, som disse eksempler viser:<br />

fun gaar op i (d,n) = n mod d = 0;<br />

val gaar op i = fn : int * int -> bool<br />

fun gnsnt (x,y,z) = (x + y + z) / 3.0;<br />

val gnsnt = fn : real * real * real -> real<br />

gaar op i (3,16);<br />

val it = false : bool<br />

gnsnt (1.2,~4.1,5.7);<br />

val it = 0.933333333333 : real<br />

Placerer man i en parentes to eller flere udtryk, adskilt af kommaer, dannes et sæt<br />

(eng.:tuple), hvis type er “det kartesiske 5 produkt” af de indg˚aende udtryks type. SML<br />

opfatter de viste funktionserklæringer som specialtilfælde af (2.1), hvor argumentets type<br />

blot er et sæt.<br />

2.4 Sprogsystemets faser<br />

Hvis parenteser skal sættes p˚a en bestemt m˚ade, for at et udtryk kan blive typemæssigt<br />

korrekt (s˚adan som tilfældet var med abs ~ 7 ovenfor), kunne man mene, at SML burde<br />

kunne bruge typeinformationen til at regne ud, hvor parentesen er underforst˚aet.<br />

Et af grundprincipperne i SML er imidlertid, at det normalt ikke skal være nødvendigt<br />

for en bruger at angive typen for navne og værdier, men at systemet af sammenhængen<br />

udleder, hvilken type de enkelte dele nødvendigvis m˚a have.<br />

En forudsætning for, at denne typeudledning kan lykkes, er, at udtryk er grupperet<br />

korrekt. Sprogsystemets rækkefølge er derfor, at først grupperes udtrykkets dele, og derefter<br />

udledes typerne. Følgelig kan typeinformation ikke styre grupperingen.<br />

I store træk kan man sige, at sprogsystemets evaluering af et udtryk gennemløber fire<br />

faser<br />

1. Strømmen af enkelttegn, som danner programmet, opdeles i grundsymboler<br />

5 efter René Descartes (1596–1650), koordinatsystemets opfinder.<br />

33


2. Grundsymbolerne grupperes i større helheder. (Man kan forestille sig, at alle de underforst˚aede<br />

parenteser nu tilføjes)<br />

3. Typer udledes og kontrolleres<br />

4. Udtrykkets værdi beregnes<br />

Flere detaljer af disse faser behandles senere 6 ; i dette kapitel er der kun grund til at<br />

nævne et enkelt forhold i forbindelse med skanderingen i grundsymboler, fordi det kan give<br />

anledning til fejlmeldinger, der ellers kan være svære at forst˚a.<br />

2.4.1 Navne<br />

Som et bogstav regner sprogsystemet de 26 latinske bogstaver (sm˚a og store bogstaver er<br />

forskellige) a. . . z og A. . . Z, og et ciffer er et af de decimale cifre 0. . . 9. Et alfanumerisk tegn<br />

er et bogstav, et ciffer, en understregning ( ) eller en apostrof (’).<br />

I Standard ML kan et navn (eng.:identifier) være af to forskellige former: Et alfanumerisk<br />

navn (eng.:alphanumeric identifier) er en sekvens af alfanumeriske tegn, der begynder med<br />

et bogstav 7 .<br />

Et symbolsk navn (eng.:symbolic identifier) er en sekvens af et eller flere af følgende tyve<br />

navnetegn:<br />

! % & $ # + - / ; < = > ? @ \ ~ ‘ ^ | *<br />

Operatorer betegnes ogs˚a med navne; vi har ovenfor b˚ade mødt operatorer med alfanumeriske<br />

navne (div og mod) og operatorer med symbolske navne (for eksempel +, -, *).<br />

Lange navne Større programmer opdeles bekvemt i separate programdele, som kaldes<br />

strukturer og navngives med alfanumeriske navne. For at f˚a fat i et navn, der er ligger inde<br />

i en struktur, benyttes et s˚akaldt langt navn (eng.:qualified identifier)<br />

strukturnavn.navn<br />

eller mere generelt (for et navn inde i struktur1 inde i struktur2 . . . )<br />

strukturnavnn. . .strukturnavn2.strukturnavn1.navn<br />

Skandering Selv om et udtryk som for eksempel Chr4aar · a1 − a0 opskrives helt uden<br />

adskillelse:<br />

Chr4aar*a 1-a 0<br />

kan SML-systemet godt finde ud af at skille det rigtigt ad i grundsymboler<br />

Chr4aar * a 1 - a 0<br />

fordi der skiftevis bruges alfanumeriske og symbolske navne.<br />

I andre tilfælde kan det g˚a galt. Vi kan nu forst˚a den fejlmelding, som fremkommer, hvis<br />

man forsøger at omdøbe fortegnsvendefunktionen p˚a følgende m˚ade:<br />

6 I teorien for formelle sprog svarer de fire faser henholdsvis til 1. leksikalsk analyse, 2. kontekstfri syntaksanalyse,<br />

3. kontekstsensitiv syntaksanalyse og 4. semantisk analyse. (De to sidste faser kaldes ogs˚a 3. statisk<br />

semantik og 4. dynamisk semantik.)<br />

7 Teknisk set henregner sprogdefinitionen ogs˚a typevariable, som er en sekvens af alfanumeriske tegn, der<br />

begynder med en apostrof, til alfanumeriske navne.<br />

34


fun modsat(n)=~n;<br />

! Toplevel input:<br />

! fun modsat(n)=~n;<br />

! ^<br />

! Syntax error.<br />

De to navnetegn lige efter hinanden opfattes af systemet som et nyt symbolsk navn =~.<br />

Man bør gøre sig det til en vane at skille operatorer og operander fra hinanden med<br />

blanktegn, og alle programeksempler i disse <strong>noter</strong> vil s˚a vidt muligt blive <strong>noter</strong>et p˚a den<br />

m˚ade. Eksemplet bør alts˚a skrives<br />

fun modsat n = ~ n;<br />

val modsat = fn : int -> int<br />

2.5 Fem simple typer<br />

I Standard ML er der fem simple grundtyper 8 : heltal int, brudne tal real, sandhedsværdier<br />

bool, tegn char og tekster string.<br />

Nye typer kan blandt andet dannes som produkttyper τ1 * τ2 * . . . * τn, som funktionstyper<br />

τ -> τ ′ og som listetyper τ list ud fra tidligere typer τ, τ ′ , τ1, τ2, . . . , τn. I senere<br />

kapitler vil vi have mere at sige om produkttyper, funktionstyper og listetyper.<br />

Figur 2.1 giver en oversigt over de fem simple typer og deres standardoperatorer og<br />

-funktioner. Vi vil senere vende tilbage med flere detaljer, men for at man hurtigt skal<br />

kunne komme i gang med at skrive sm˚a programmer, følger her en foreløbig gennemgang.<br />

2.5.1 To typer tal<br />

I de fleste programmeringssprog er hele og reelle tal to disjunkte typer. Det kan virke underligt,<br />

hvis man er vant til det matematiske synspunkt, hvor de hele tal regnes som en<br />

delmængde af de reelle ( Z ⊆ R), men kan m˚aske forsvares ud fra et modeldannelsessynspunkt:<br />

N˚ar man i en beskrivelse bruger tal, vil det normalt enten være om fænomener, der er<br />

kombinatoriske eller kan optælles (et antal personer, trappetrin, et kontant ørebeløb eller<br />

lignende) og derfor kan angives ved et heltal — eller det vil være om et eller andet fysisk<br />

fænomen (afstand, temperatur, tryk, tidsinterval, osv.), der kan angives ved et reelt tal. Et<br />

ark papir har 2 sider, men selv om man m˚aske angiver en bordplades længde som heltallet<br />

2 meter, s˚a er længden ikke p˚a samme m˚ade “to af en ting”. Bordet er muligvis 2.00 m og<br />

m˚aske ogs˚a 2.000 m langt, men næppe 2.0000 m — i praksis er m˚alinger af fysiske fænomener<br />

altid behæftet med en vis usikkerhed, og det er mere korrekt at sige, at bordpladens længde<br />

m˚alt i meter er det reelle tal 2.<br />

I Standard ML er der to taltyper 9 : int og real, og allerede talkonstanter tillægges en<br />

bestemt af de to typer. Konventionen er, at hvis der kun bruges cifre, eventuelt med en tilde<br />

umiddelbart foran<br />

8Sprogdefinitionen kræver ogs˚a typerne unit og word, og implementationen Moscow ML føjer hertil<br />

order og word8.<br />

9samt word og i Moscow ML yderligere word8. Disse typer skal vi dog ikke beskæftige os med her.<br />

35


~<br />

abs<br />

✬<br />

<br />

✣<br />

✫<br />

✬<br />

❄<br />

✫<br />

+ - *<br />

div mod<br />

chr<br />

int<br />

char<br />

✻❖<br />

ord<br />

✩<br />

✎<br />

✪<br />

size<br />

= <br />

< >=<br />

= <br />

< >=<br />

✩<br />

✪<br />

ceil<br />

floor<br />

round<br />

trunc<br />

real<br />

str<br />

✬<br />

<br />

✲<br />

✫<br />

= <br />

< >=<br />

✬<br />

❄<br />

<br />

❥<br />

✯<br />

✫<br />

✒<br />

= <br />

< >=<br />

✬<br />

<br />

✲<br />

✫<br />

+ - *<br />

/<br />

real<br />

= <br />

bool<br />

^<br />

string<br />

✢<br />

✢<br />

✩<br />

~<br />

abs<br />

✪<br />

✩<br />

not<br />

✪<br />

✩<br />

✪<br />

Figur 2.1: Grundtyperne og deres p˚a forh˚and ˚abnede standardoperatorer og -funktioner.<br />

36


5, 12, ~44, 007 : int<br />

s˚a er typen hel. Konstanter med enten en decimaldel eller en eksponentdel eller begge dele<br />

gives reel type:<br />

5.0, ~1.4142, 3e12, 1E~6, 0.67194e~22 : real<br />

Formerne mantisse e eksponent og mantisse E eksponent skal forst˚as som mantisse·10eksponent .<br />

Hver del af et regneudtryk skal enten have hel eller reel type, og regneoperatorernes to<br />

operander skal have samme type (som ogs˚a bliver resultatets type). For +, - og * skal enten<br />

begge operander være hele, eller de skal begge være reelle. Divisionsoperatoren / kræver, at<br />

begge operander er reelle, mens div og mod kun virker p˚a to hele værdier. Dette er illustreret<br />

af pilene i figur 2.1.<br />

Man kan derfor ikke skrive for eksempel x / 2 + 1, men m˚a enten vælge x div 2 + 1,<br />

hvor x s˚a tillægges hel type (og resultatet rundes ned med 1,<br />

hvis x er ulige), eller skrive<br />

2<br />

det x / 2.0 + 1.0 for x af reel type.<br />

En anden mulighed er x / real 2 + 1.0, idet real ud over at betegne typen af brudne<br />

tal ogs˚a er navnet p˚a den funktion, der omsætter fra hel til bruden type.<br />

Den anden vej, fra reel til hel type, kan man i SML vælge mellem hele fire funktioner:<br />

floor afrunder til det største heltal, der ikke er større (“afrunding mod −∞”), ceil afrunder<br />

til det mindste heltal, der ikke er mindre (“afrunding mod ∞”), trunc bortkaster brøkdelen<br />

(afskæring eller “afrunding mod 0”), og round tager det nærmeste heltal:<br />

floor ~2.6 = ~3; floor 2.6 = 2;<br />

ceil ~2.6 = ~2; ceil 2.6 = 3;<br />

trunc ~2.6 = ~2; trunc 2.6 = 2;<br />

round ~2.6 = ~3; round 2.6 = 3;<br />

Værdier, som ligger nøjagtig midt imellem to hele tal, afrundes til det nærmeste lige heltal:<br />

round 0.5 = 0; round 1.5 = 2; round 2.5 = 2;<br />

Et stort antal funktioner til regning med reelle tal finder man i biblioteksmodulerne Real<br />

og Math, som vil blive nærmere omtalt i næste kapitel.<br />

2.5.2 Sandhedsværdier<br />

Som figur 2.1 viser, er der en standardfunktion not : bool -> bool. Den ombytter de to<br />

sandhedsværdier.<br />

2.5.3 Tegn<br />

Enkelttegn (eng.:characters) er en grundtype i Standard ML 10 . Tegnkonstanter skrives s˚aledes:<br />

#"C" (bogstavet C), #"9" (et nital), # (et blanktegn). Funktionen ord giver koden for et<br />

forelagt tegn, og chr giver omvendt tegnet med en forelagt kode. Sammenligninger mellem<br />

tegn med


2.5.4 Tekster<br />

En sekvens af tegn kaldes en tekst (eng.:string). For at danne en tekstkonstant sætter man<br />

bare sekvensen i anførselstegn: "sekvens i anførselstegn". Antallet af tegn i en tekst<br />

findes med size, og operatoren ^ sætter to tekster sammen til en:<br />

"vand"^ "ring- "vandring"<br />

Ved sammenligning af tekster med ordningsoperatorerne


Opgave 2.2 Definer den funktion 11 , som giver negative heltal værdien -1, positive heltal<br />

værdien 1 og nul værdien 0.<br />

Opgave 2.3 Definer den funktion af type int -> bool, som ud fra en persons alder afgør,<br />

om personen har stemmeret.<br />

Opgave 2.4 Definer en funktion, der afgør, om et heltal ender p˚a “5”. (Egentlig har tal<br />

først cifre, n˚ar de repræsenteres som talord, s˚a opgaven skal naturligvis forst˚as som “. . . om<br />

et heltal, hvis det blev skrevet som talord i titalssystemet, ville ende p˚a “5”.)<br />

Opgave 2.5 Definer den funktion af type real -> real, som for et forelagt beløb (brudent<br />

tal) i kroner tillægger 25 % moms og afrunder resultatet til et betalbart kronebeløb (et<br />

multiplum af 25 øre).<br />

Opgave 2.6 Definer den funktion af type int -> int, som ud fra antallet af deltagere p˚a<br />

et kursus beregner, hvor mange øvelseshold der skal oprettes, n˚ar antallet af hold skal være<br />

mindst muligt, men der højst m˚a være 25 p˚a hvert hold.<br />

Opgave 2.7 Det er gratis at ringe til telefonnumre, hvis to første (af de otte) cifre er 80.<br />

Definer en funktion af type int -> bool, der undersøger, om det er gratis at ringe til det<br />

opgivne nummer.<br />

Opgave 2.8 Definer en funktion, der omsætter en temperaturangivelse fra grader fahrenheit<br />

til grader celsius (sammenhængen er lineær 12 og fremg˚ar af 0 ◦ C = 32 ◦ F og 100 ◦ C =<br />

212 ◦ F).<br />

Opgave 2.9 Definer en funktion, som finder det største af sine tre argumenter.<br />

Opgave 2.10 Definer en funktion, som for et engelsk navneord i ental danner flertalsformen.<br />

(Du kan g˚a ud fra, flertalsformen dannes ved tilføjelse af “s”, bortset fra ordene man,<br />

woman, mouse og sheep, der i flertal hedder men, women, mice og sheep.)<br />

Opgave 2.11 Definer en funktion af type char -> int, der beregner et bogstavs nummer<br />

i alfabetet. [Vink: brug biblioteksfunktionen ord.]<br />

Opgave 2.12 Definer en funktion, der undersøger, om et navn ender p˚a "sen". [Vink:<br />

brug biblioteksfunktionerne size og String.substring.]<br />

11Denne funktion er faktisk til r˚adighed i biblioteksmodulet Int under navnet Int.sign, men opgaven<br />

her er selv at definere den.<br />

12y siges at være en lineær funktion af x, hvis y = ax + b.<br />

39


Kapitel 3<br />

Programmeringssproget Standard ML<br />

Forrige kapitel gav netop tilstrækkeligt med oplysninger til, at man kunne komme i gang med<br />

praktiske programmeringsopgaver i Standard ML. I dette kapitel bliver disse oplysninger<br />

uddybet og suppleret med flere detaljer om sproget. I kapitlets sidste afsnit omtales den<br />

fuldstændige defintion af Standard ML.<br />

3.1 Funktionsdefinition med valg mellem flere parametermønstre<br />

Funktionsdefinitioner kan antage en mere generel form end den tidligere viste (2.1):<br />

fun funktionsnavn parametermønster 1 = krop 1<br />

| funktionsnavn parametermønster 2 = krop 2<br />

| . . .<br />

| funktionsnavn parametermønster n = krop n<br />

(3.1)<br />

Ved kald af en funktion, som er defineret p˚a den m˚ade, sammenholdes det aktuelle argument<br />

med parametermønstrene. Hvis parametermønster i er det første mønster, der passer,<br />

evalueres funktionskaldet ved hjælp af krop i. Læg mærke til, at mønstrene i (3.1) prøves<br />

forfra 1, 2, . . . , n; hvis mønstrene overlapper hinanden, har den rækkefølge, man anfører tilfældene<br />

i, derfor betydning.<br />

Her er et eksempel:<br />

fun reciprok 0 = 0.0<br />

| reciprok n = 1.0 / real n;<br />

val reciprok = fn : int -> real<br />

reciprok 4;<br />

val it = 0.25 : real<br />

reciprok 0;<br />

val it = 0.0 : real<br />

Ved at anføre de to mønstre 0 og n i den viste rækkefølge sikrer man sig mod division<br />

med 0.<br />

41


3.2 Evaluering af funktionskald<br />

Af hensyn til det følgende er det nyttigt at sl˚a helt fast, hvordan evaluering af funktionskald<br />

foreg˚ar: Møder systemet under sine beregninger et applikationsudtryk, det vil sige to udtryk<br />

F A, der er stillet sammen uden nogen operator imellem, opfattes F som funktionsdel og<br />

A som argumentdel i et funktionskald, der udføres p˚a følgende m˚ade:<br />

1. Den igangværende beregning stilles midlertidigt i bero.<br />

2. Funktionsdelen F evalueres, s˚a det bliver klart, hvilken funktion det er, der kaldes.<br />

Lad os tænke os, det er funktionen funktionsnavn med erklæringen (3.1).<br />

3. Argumentdelen A evalueres til en værdi a 1 .<br />

4. Et for et sammenholdes parametermønstrene i (3.1) med værdien a. Lad<br />

parametermønster i være det første, der passer. Dette mønster tilpasses nu værdien,<br />

hvilket indebærer, at mønsterets formelle parametre bindes til bestemte værdier.<br />

5. Med de etablerede bindinger i kraft evalueres krop i til en værdi v.<br />

6. Nu kan parameterbindingerne brydes og den suspenderede beregning genoptages, idet<br />

applikationsudtrykket F A erstattes med værdien v.<br />

Den beskrevne fremgangsm˚ade benyttes helt konsekvent; ogs˚a hvis funktionskaldet for<br />

eksempel selv st˚ar i en funktionskrop. Her er et simpelt eksempel:<br />

fun kvadrat x = x * x;<br />

fun kvadratsum (a,b) = kvadrat a + kvadrat b;<br />

kvadratsum (1+2,4);<br />

Beregningen af kvadratsum (1+2,4) foreg˚ar p˚a følgende m˚ade: Funktionsdelen er allerede<br />

en bestemt funktion (kvadratsum). Argumentdelen evalueres til (3,4). Sammenholdning af<br />

mønsteret (a,b) med (3,4) vil binde a til 3 og b til 4, hvorefter kvadrat a + kvadrat b<br />

skal evalueres. Da første del af dette udtryk p˚any er et funktionskald, m˚a denne beregning<br />

umiddelbart igen suspenderes, for at vi med x bundet til 3 kan evaluere x * x. Det bliver<br />

9, og den suspenderede beregning kan genoptages, idet vi nu skal beregne 9 + kvadrat b.<br />

Det kræver evaluering af kvadrat b, s˚a beregningen m˚a p˚any suspenderes, mens vi med x<br />

bundet til 4 beregner x * x. Resultatet er 16, s˚a den suspenderede beregning kan genoptages<br />

som evaluering af 9 + 16 til det ønskede udtryks værdi 25.<br />

3.3 Virkefelt<br />

N˚ar man i et programmeringssprog kan bruge navne, er det vigtigt at gøre sig klart, hvor<br />

disse navne er lovlige. Man taler om sprogets “virkefeltsregler”, idet man ved virkefeltet<br />

1 At argumentdelen ubetinget evalueres, inden det vides, om kroppen overhovedet skal bruge argumentet,<br />

er et særkende for de programmeringssprog, der som Standard ML siges at være ivrige eller strikse (eng.:eager<br />

eller strict). Sprogene C og Java er ogs˚a strikse.<br />

42


(eng.:scope) for et navn forst˚ar de steder i programmet, det p˚agældende navn kan bruges (i<br />

den p˚agældende betydning).<br />

Hovedreglen i SML er, at navne skal indføres, før de kan bruges. Vi har foreløbigt mødt to<br />

“navneindførende” konstruktioner, nemlig værdierklæringer (med val) og funktionserklæringer<br />

(med fun). Som defineret i Standard ML er formen for en værdierklæring i øvrigt<br />

lidt mere generel end nævnt i afsnit 2.1.3, idet venstre side kan være et generelt mønster.<br />

(Hvordan det kan udnyttes, vil vi komme nærmere ind p˚a i afsnit 4.4.1.)<br />

Værdi- og funktionserklæringer har forskellige virkefeltsregler (og det er netop for at<br />

minde om denne forskel, systemet tvinger os til at bruge to forskellige nøgleord):<br />

En værdierklæring<br />

val mønster = udtryk A ;<br />

Resten af programmet B<br />

indfører de navne, som indg˚ar i mønster og derved bliver til r˚adighed i resten af programmet<br />

B.<br />

Ved en funktionserklæring<br />

fun fnavn m1 = k1 A1<br />

| fnavn m2 = k2 A2<br />

| . . .<br />

| fnavn mn = kn An<br />

Resten af programmet B<br />

forekommer der flere navne. Virkefeltet for de formelle parametre, som indg˚ar i mønsteret<br />

m1 er A1 (det vil sige kroppen k1). For de formelle parametre i m2 er virkefeltet A2, og<br />

s˚a videre frem til de formelle parametre i mn, der har virkefelt An. Virkefeltet for selve<br />

funktionsnavnet fnavn er b˚ade A1, A2, . . . An og resten af programmet B.<br />

3.3.1 Redefinition<br />

Hvert navn bør normalt kun defineres én gang, men det er ikke forbudt at definere et navn<br />

flere gange. Hvis der inde i virkefeltet for et navn p˚any optræder en definition af dette navn,<br />

er reglen, at den nye defnition “skygger” for den gamle, som derved bliver utilgængelig i det<br />

nye virkefelt. Nytten heraf er først og fremmest, at fejl kan rettes med en redefinition (kun<br />

nogle af systemets kvitteringer vises):<br />

val rabatpct = 10;<br />

fun studenterpris bogpris = bogpris + bogpris * rabatpct div 100;<br />

val studenterpris = fn : int -> int<br />

studenterpris 31000 (* øre *);<br />

val it = 34100 : int<br />

(* Hovsa! Rabat skal ikke lægges til, men trækkes fra: *)<br />

fun studenterpris bogpris = bogpris - bogpris * rabatpct div 100;<br />

val studenterpris = fn : int -> int<br />

studenterpris 31000;<br />

val it = 27900 : int<br />

43


I en værdidefinition kan det navn vnavn, man er ved at definere, naturligvis ikke bruges<br />

i det definerende udtryk A, men er der tale om en redefinition, følger det af reglerne, at<br />

vnavn eventuelt kunne bruges i sin gamle betydning.<br />

Her udregnes en pris før og efter en momsnedsættelse p˚a 3%. Redefinitionen, hvor den<br />

nye værdi af navnet defineres ved hjælp af den gamle, er understreget (kun nogle af systemets<br />

kvitteringer vises):<br />

val bogpris = 648.00;<br />

val moms = 0.25;<br />

bogpris + moms * bogpris;<br />

val it = 810.0 : real<br />

val moms = moms - 0.03;<br />

bogpris + moms * bogpris;<br />

val it = 790.56 : real<br />

Denne form for redefinitioner harmonerer d˚arligt med applikativ programmeringsstil og<br />

bør undg˚as.<br />

Advarsel til tilstandsprogrammører<br />

Har man tidligere prøvet at programmere i et tilstandsorienteret sprog, er det vigtigt at<br />

<strong>noter</strong>e sig, at definitioner i SML binder navne til værdier og ikke som i visse andre programmeringssprog<br />

til lagerpladser. Denne udbygning af et af de tidligere eksempler illustrerer<br />

forskellen:<br />

val rabatpct = 10;<br />

fun studenterpris bogpris = bogpris - bogpris * rabatpct div 100;<br />

val studenterpris = fn : int -> int<br />

studenterpris 50000;<br />

val it = 45000 : int<br />

val rabatpct = 12;<br />

50000 - 50000 * rabatpct div 100;<br />

val it = 44000 : int<br />

studenterpris 50000;<br />

val it = 45000 : int<br />

I definitionen af funktionen studenterpris indgik rabatpct, men p˚a det tidspunkt var<br />

konstanten bundet til 10, og studenterpris bindes derfor til den funktion, som trækker<br />

10% fra prisen. Selv om bindingen af rabatpct senere ændres, forandrer det ikke værdien<br />

af studenterpris!<br />

Hvordan skal man da programmere det, hvis studenterprisen ogs˚a afhænger af en (varierende)<br />

rabatprocent?<br />

Svaret er simpelt: At studenterprisen afhænger af rabatprocenten vil med andre ord sige,<br />

at studenterprisen er en funktion af rabatprocenten, og s˚a skal definitionen tage rabatprocent<br />

med som funktionsparameter:<br />

fun studenterpris (bogpris,rabatpct)<br />

= bogpris - bogpris * rabatpct div 100;<br />

44


val studenterpris = fn : int * int -> int<br />

studenterpris (50000,10);<br />

val it = 45000 : int<br />

studenterpris (50000,12);<br />

val it = 44000 : int<br />

3.3.2 Rekursion<br />

Af reglerne i afsnit 3.3 ovenfor fremgik det, at et funktionsnavns virkefelt ogs˚a omfattede<br />

funktionsdefinitionens egen krop. En funktionsdefinition, der i sin egen krop kalder den<br />

funktion, som er ved at blive defineret, siges at være rekursiv. Rekursion giver mulighed<br />

for ulovlige cirkulære definitioner, men skal naturligvis bruges p˚a den m˚ade, at de indre<br />

funktionskald i en eller anden forstand skal være simplere end det kald, hvis værdi man er<br />

ved at definere.<br />

I virkeligheden er forholdene i denne situation, hvor en funktion f defineres ved kald af<br />

funktionen f, ikke spor anderledes, end hvis funktionen f defineres ved kald af en anden<br />

funktion g. Ovenfor blev kvadratsum defineret ved kald af kvadrat, som var en simplere<br />

funktion end kvadratsum. Hvis værdien af f for en vis parameterværdi p fastlægges ud fra<br />

kald af den samme funktion f, skal det naturligvis være kald, hvor argumenterne er simplere<br />

end p. Desuden m˚a der være nogle parameterværdier, de s˚akaldte basistilfælde, for hvilke<br />

værdien af f kan beregnes uden rekursive kald.<br />

N˚ar reglerne for evaluering af funktionskald ovenfor blev gennemg˚aet med s˚a stor omhu,<br />

er det, fordi disse regler fuldstændig uændret ogs˚a dækker tilfældet med rekursive funktionskald.<br />

Eksempel For et ikke-negativt heltal n kan dets bageste ciffer 2 (“eneren”) beregnes som<br />

n mod 10, og de øvrige cifre, efter at det bageste ciffer er fjernet, findes af n div 10.<br />

Ved et tals tværsum forst˚as summen af dets cifre (n˚ar det er skrevet i titalssystemet).<br />

Her er en funktion til beregning af et tals tværsum:<br />

fun tvaersum n<br />

= if n < 10 then n else tvaersum (n div 10) + n mod 10;<br />

val tvaersum = fn : int -> int<br />

tvaersum 79;<br />

val it = 16 : int<br />

Svaret er fundet p˚a følgende m˚ade:<br />

• n bindes til 79.<br />

• I disse omgivelser evalueres kroppen if n < 10 then n else tvaersum (n div 10)<br />

+ n mod 10. Da 79 < 10 er falsk, fører det til tvaersum (n div 10) + n mod 10<br />

med et indledende funktionskald.<br />

2 Begrebsmæssigt bør man skelne mellem et tal, som er det matematiske objekt, og et talord, som er dets<br />

repræsentation, og i steden for “n’s bageste ciffer” burde man sige “sidste ciffer i repræsentationen af n som<br />

decimalt talord”.<br />

45


• n div 10 evalueres til 7, og beregningen suspenderes.<br />

• – n bindes til 7.<br />

– I disse omgivelser evalueres kroppen if n < 10 then n else tvaersum (n div<br />

10) + n mod 10. Da 7 < 10 er sand, fører det til n, som var bundet til 7, hvilket<br />

afslutter den indskudte beregning.<br />

• Den suspenderede beregning (den, hvor n var bundet til 79) kan nu genoptages, idet<br />

7 + n mod 10 nu skal evalueres. Det fører via 7 + 9 til facit 16.<br />

3.4 Navnerum; biblioteksmoduler<br />

Det er vanskeligt at holde styr p˚a de mange navne, som optræder i et program, og det kan<br />

være svært at finde p˚a gode navne, som alle er forskellige. De fleste programmeringssprog<br />

giver derfor mulighed for, at beslægtede navne p˚a en eller anden m˚ade kan holdes sammen<br />

og benævnes med en fælles komponent.<br />

I SML kan værdier samles i moduler; nyttige biblioteksmoduler er blandt andet Char,<br />

Int, List, Math, Mosml, Random, String og TextIO.<br />

For at angive en værdi i et modul benyttes et langt navn med formen<br />

modulnavn.værdinavn<br />

Udvalgte dele af biblioteket fremg˚ar af [5, Appendix D]; en fuldstændig oversigt gives i [15].<br />

Man kan kun f˚a fat i værdier i moduler, der er “hentet ind”. I Mosml er følgende moduler<br />

hentet ind p˚a forh˚and: Array, Char, List, String, TextIO og Vector. Andre moduler hentes<br />

ind ved at skrive<br />

load "modulnavn";<br />

Funktionen, der omsætter alle bogstaver til sm˚a bogstaver, er for eksempel umiddelbart<br />

tilgængelig, mens π og √ først kan bruges, efter at matematikmodulet er hentet ind:<br />

(Char.toLower #"P",Char.toLower #"s");<br />

val it = (#"p", #"s") : char * char<br />

load "Math";<br />

val it = () : unit<br />

fun cirkelareal radius = Math.pi * radius * radius;<br />

val cirkelareal = fn : real -> real<br />

fun cirkelradius areal = Math.sqrt (areal / Math.pi);<br />

val cirkelradius = fn : real -> real<br />

3.5 Typen af heltal<br />

Figur 2.1 viste de p˚a forh˚and ˚abnede standardoperatorer og -funktioner for heltal (typen<br />

int). Ved at hente modulet Int ind<br />

load "Int";<br />

46


f˚ar man adgang til flere nyttige funktioner (se [5, Appendix D.2] eller [15]), af hvilke vi her<br />

blot anfører tre:<br />

navn type virkning<br />

Int.min int * int -> int det mindste af de to tal<br />

Int.max int * int -> int det største af de to tal<br />

Int.toString int -> string tallet fremstillet som decimalt talord<br />

3.5.1 Heltalsdivision<br />

Inden for de hele tal behøver division ikke at g˚a op; n div d er det hele antal gange, d er<br />

indeholdt i n, og n mod d er den tilhørende divisionsrest:<br />

365 div 7;<br />

val it = 52 : int<br />

365 mod 7;<br />

val it = 1 : int<br />

For q = n div d og r = n mod d gælder identisk<br />

n = q · d + r (3.2)<br />

og man vil normalt sørge for at skaffe sig en rest, der er mindre end divisor:<br />

0 ≤ |r| < |d| (3.3)<br />

Hvorledes heltalsdivision skal defineres, hvis dividend eller divisor er negativ, er der ikke<br />

enighed om. Det er klart, at kvotienten q skal findes som en afrunding af den reelle brøk n<br />

d ,<br />

men i afsnit 2.5.1 mødte vi jo hele fire forskellige afrundingsfunktioner!<br />

Standardoperatoren div svarer til afrunding mod minus uendelig, det vil sige<br />

n div d = floor (real n / real d). Resten n mod d fastlægges herudfra, s˚adan at (3.2)<br />

kommer til at gælde. Man kan indse, at det betyder, at divisionsresten altid f˚ar samme fortegn<br />

som divisor:<br />

For eksempel gælder<br />

0 ≤ n mod d < d eller d < n mod d ≤ 0<br />

17 div ~4 = ~5 og 17 mod ~4 = ~3<br />

Undertiden ønsker man, at divisionsresten skal have samme fortegn som dividenden. Det<br />

bliver resultatet, hvis kvotienten i stedet dannes ved afrunding mod nul, og de tilhørende<br />

funktioner er til r˚adighed som Int.quot og Int.rem. Med andre ord gælder Int.quot (n,d)<br />

= trunc (real n / real d) og Int.rem (n,d) = n - Int.quot (n,d) * d, idet man<br />

stadig sørger for at opretholde (3.2).<br />

Eksempel:<br />

Int.quot (17,~4) = ~4 og Int.rem (17,~4) = 1<br />

47


3.5.2 Euklids algoritme<br />

Ligningen (3.2) viser, at hvis et tal g˚ar op i d, vil det ogs˚a g˚a op i n hvis og kun hvis det<br />

ogs˚a g˚ar op i r. Dette forhold benyttede den græske matematiker Eυ ✁ κλε´ιδης (som levede i<br />

Alexandria omkring 300˚ar før vor tidsregning) til at beskrive en effektiv metode til beregning<br />

af to tals største fælles divisor (eller “største fælles m˚al”, som man tidligere sagde).<br />

Lad gcd (a,b) betegne den største fælles divisor for to heltal a og b (eng.:greatest<br />

common divisor). Da alle tal g˚ar op i 0, gælder<br />

gcd (a,0) = |a| for a = 0 (3.4)<br />

mens gcd (0,0) er udefineret. Ud over at være den største fælles divisor for a og b har<br />

gcd (a,b) imidlertid ogs˚a den egenskab at være det (ikke negative) tal, hvis divisorer netop<br />

er de fælles divisorer for a og b. Tager man udgangspunkt i denne egenskab, skal gcd (0,0)<br />

være 0, og gyldigheden af (3.4) kan udvides til alle hele tal a.<br />

Euklids metode er som følger: Lad a og b være to hele tal. Hvis det ene er 0, er deres<br />

største fælles divisor den numeriske værdi af det andet tal. Ellers kan vi antage 0 < |a| ≤ |b|,<br />

og i henhold til (3.2) kan vi i stedet for at opsøge største fælles divisor for a og b løse den<br />

opgave at finde største fælles divisor for b mod a og a, som ifølge (3.3) vil være numerisk<br />

mindre tal:<br />

fun gcd (a,b) = if a = 0 then abs b else gcd (b mod a,a);<br />

val gcd = fn : int * int -> int<br />

gcd (24,15);<br />

val it = 3 : int<br />

(I en opgave spørges der om, hvorfor programmet ikke sørger for at ordne |a| ≤ |b|.)<br />

3.6 Standardundtagelser<br />

At dividere med 0 er ikke matematisk muligt 3 . Forsøger man det i SML<br />

5 mod 0;<br />

! Uncaught exception:<br />

! Div<br />

reagerer systemet med en s˚akaldt undtagelse (eng.:exception). En undtagelse er ikke en<br />

egentlig værdi (standardnavnet it berøres ikke) og kan opfattes som en fejlmelding fra<br />

systemet. Hvis et deludtryk resulterer i en undtagelse (eller “rejser en undtagelse” eller<br />

“kaster en undtagelse”, som man ogs˚a siger), afbrydes beregningerne med melding om denne<br />

undtagelse.<br />

Matematiske funktioner kan kaste undtagelsen Domain:<br />

load "Math";<br />

val it = () : unit<br />

Math.sqrt ~1.0;<br />

! Uncaught exception:<br />

! Domain<br />

3 a<br />

Grunden til forbuddet mod division med 0 er jo, at uanset hvilken værdi man ville tillægge 0<br />

sædvanlige regneregler ikke kunne opretholdes, herunder først og fremmest, at a<br />

b · b = a.<br />

48<br />

, ville de


3.6.1 Ikke-udtømmende parametermønstre<br />

Vi har mødt endnu en konstruktion, som kan give anledning til fejlmeldinger. I den udvidede<br />

form for funktionsdefinition (3.1) bør man tilstræbe at klassedele de mulige parameterværdier.<br />

Reaktionen p˚a overlappende mønstre var som nævnt, at det første, der passede, blev<br />

benyttet.<br />

Hvis mønstrene ikke udtømmer (eng.:exhaust) alle muligheder, udsteder systemet i første<br />

omgang en advarsel:<br />

fun klokken 0 = "midnat"<br />

| klokken 12 = "middag"<br />

! Toplevel input:<br />

! ....klokken 0 = "midnat"<br />

! | klokken 12 = "middag".<br />

! Warning: pattern matching is not exhaustive<br />

val klokken = fn : int -> string<br />

Ikke-udtømmende parametermønstre behøver ikke at være en fejl — programmets logik<br />

kunne jo være s˚adan, at der aldrig blev brug for de manglende tilfælde — men hvis intet<br />

mønster passer, kastes undtagelsen Match:<br />

klokken 24;<br />

! Uncaught exception:<br />

! Match<br />

3.6.2 Overløb<br />

Mængden Z af alle hele tal har uendeligt mange elementer, men maskinens model af dem,<br />

som her er typen int, kan naturligvis kun være endelig. Nogle programmeringssprog kan<br />

regne eksakt med meget store hele tal, idet det accepteres, at heltalsord kan fylde en<br />

varierende del af lageret, men i Moscow ML har man valgt, at heltal kun m˚a fylde<br />

31 bit. Der er derfor 2 31 forskellige heltalsord, med værdier fra −2 30 = −1073741824 til<br />

2 30 − 1 = 1073741823. Beregninger, der fører ud over dette interval, siges at “løbe over” eller<br />

“give overløb” og rejser undtagelsen Overflow:<br />

val toi10 = 1024;<br />

val toi10 = 1024 : int<br />

~ toi10 * toi10 * toi10;<br />

val it = ~1073741824 : int<br />

it - 1;<br />

! Uncaught exception:<br />

! Overflow<br />

val stoerste = ~1 - it;<br />

val stoerste = 1073741823 : int<br />

stoerste + 1;<br />

! Uncaught exception:<br />

! Overflow<br />

49


Muligheden for overløb betyder, at mange af de sædvanlige matematiske omskrivningsregler<br />

mister deres gyldighed. Man kan for eksempel ikke være sikker p˚a, at<br />

~ (1 + stoerste);<br />

! Uncaught exception:<br />

! Overflow<br />

~1 - stoerste;<br />

val it = ~1073741824 : int<br />

− (a + b) = − a − b<br />

3.7 Typen af sandhedsværdier<br />

Den vigtigste anvendelse af udtryk af logisk type har vi allerede mødt flere gange, nemlig<br />

som betingelser, placeret mellem if og then i et valgudtryk.<br />

3.7.1 Striks eller efterladende strategi?<br />

I<br />

if betingelse then mulighed else alternativ<br />

vil betingelsen altid blive evalueret, men styret af dens værdi vil derefter kun det ene af<br />

udtrykkene mulighed og alternativ blive evalueret.<br />

Selv om et valgudtryk har tre ˚abne pladser, afviger det herved fra en funktion med tre<br />

argumenter. Man kunne godt definere en tilsvarende funktion if then else , men ingen<br />

funktion kan erstatte brugen af valgudtryk:<br />

fun if then else (i,t,e) = if i then t else e;<br />

val ’a if then else = fn : bool * ’a * ’a -> ’a<br />

fun gcd’ (a,b) = if then else (a = 0,abs b,gcd’ (b mod a,a));<br />

val gcd’ = fn : int * int -> int<br />

gcd’ (2,4);<br />

! Uncaught exception:<br />

! Div<br />

(Typevariablen ’a i systemets svar forklares i det senere afsnit 4.5.)<br />

Vi kopierede jo blot definitionen af gcd fra afsnit 3.5.2, s˚a hvad er dog g˚aet galt?<br />

Som nævnt i afsnit 3.2 udregnes funktionskald p˚a den m˚ade, at de aktuelle argumenter<br />

evalueres, for at man kan etablere de bindinger, der skal være i kraft under udregning af<br />

funktionskroppen.<br />

Helt konkret kræver gcd’ (2,4), at man først evaluerer<br />

• 2 = 0 (værdi false),<br />

• abs 4 (værdi 4) og<br />

• gcd’ (4 mod 2,2).<br />

Beregningen af gcd’ (2,4) m˚a derfor suspenderes, indtil man har f˚aet beregnet gcd’<br />

(0,2), hvilket igen kræver evaluering af<br />

• 0 = 0 (værdi true),<br />

50


• abs 2 (værdi 2) og<br />

• gcd’ (2 mod 0,0).<br />

Men her sker den division med nul, som fejlmeldes!<br />

At de aktuelle argumenter altid evalueres, beskriver man ved at sige, at ML har striks<br />

eller ivrig (eng.:strict eller eager) parameteroverføring. Alternativet, hvor argumenterne<br />

overføres som uevaluerede udtryk (og som blandt andet benyttes i <strong>funktionsprogrammering</strong>ssprogene<br />

Miranda og Haskell), kaldes efterladende (eng.:lenient eller non-strict)<br />

parameteroverføring.<br />

Valgkonstruktionen if betingelse then mulighed else alternativ er kun striks i betingelsen,<br />

men efterladende i de to andre deludtryk.<br />

3.7.2 Betingelse som skildvagt<br />

I den korrekte gcd-funktion er det tydeligt, hvordan deludtrykket<br />

if a = 0 then abs b else gcd (b mod a,a)<br />

forhindrer, at der nogen sinde kan blive divideret med 0: “b mod a” evalueres kun, hvis “a<br />

= 0” ikke var sand. Man kan sige, at “a = 0” her st˚ar som en slags “skildvagt”, der sikrer<br />

mod division med 0.<br />

Denne brug af udtryk af logisk type: som skildvagter, der vogter mod ulovlige deludtryk,<br />

er meget almindelig.<br />

3.7.3 Konjunktion og disjunktion<br />

Oversigten 2.1 over operatorer og funktioner viste, at negation (der i matematik ofte <strong>noter</strong>es<br />

som en overstregning eller med symbolet ¬) fandtes i ML under navnet not. Derimod viste<br />

figuren ikke de to andre almindelige logiske operationer konjunktion (logisk “og”, ofte <strong>noter</strong>et<br />

∧ eller &) og disjunktion (logisk “eller”, <strong>noter</strong>et ∨ eller |).<br />

Konjunktion og disjunktion er i SML sekventielle, det vil sige kun strikse i første operand,<br />

og kan derfor ikke opfattes som funktioner. For at man kan blive mindet om, at de evalueres<br />

sekventielt, har man ogs˚a valgt omstændelige navne til disse operationer i SML: konjunktion<br />

skal skrives andalso 4 og disjunktion orelse.<br />

a andalso b er kun sand, hvis b˚ade a og b er sande, men hvis a er falsk, bliver b slet<br />

ikke evalueret; p˚a samme m˚ade er a orelse b kun falsk, hvis b˚ade a og b er falske, men<br />

hvis a er sand, bliver b ikke evalueret. Man kan sige, at<br />

a andalso b evalueres som if a then b else false<br />

a orelse b evalueres som if a then true else b<br />

Eksempel Lad os kontrollere, om de tre variable (aar,maaned,dag) angiver en korrekt<br />

dato, det vil sige om m˚aneden ligger mellem 1 og 12, og om dagen ligger mellem 1 og<br />

m˚anedens længde (systemsvarene vises ikke):<br />

4 Faktisk findes and ogs˚a som reserveret ord, men det bruges til parallelle definitioner.<br />

51


val skudaar<br />

= if aar mod 100 = 0<br />

then aar mod 400 = 0<br />

else aar mod 4 = 0;<br />

val maanedslgd<br />

= if maaned = 2<br />

then if skudaar then 29 else 28<br />

else if maaned = 4 orelse maaned = 6<br />

orelse maaned = 9 orelse maaned = 11<br />

then 30 else 31;<br />

1


større end for heltal, men det er stadig begrænset: Der findes en største og en mindste værdi<br />

af type real, og overskridelse af talomr˚adet giver (ligesom for heltal) overløb (her forsøges<br />

beregning af 101010): load "Math";<br />

val it = () : unit<br />

Math.pow (10.0,10.0);<br />

val it = 10000000000.0 : real<br />

Math.pow (10.0,it);<br />

! Uncaught exception:<br />

! Overflow<br />

Hertil kommer en anden defekt: I maskinens model af de reelle tal ligger værdierne ikke<br />

uendelig tæt; fra et maskintal er der en vis endelig afstand hen til det nærmeste større og<br />

det nærmeste mindre maskintal. Regneoperationer kan derfor ikke altid udføres eksakt, men<br />

kan indebære afrundingsfejl. N˚ar det drejer sig om typen real, kan systemet med andre ord<br />

komme til at “regne forkert”. At lægge beregninger til rette, s˚a afrundingsfejl kun f˚ar ringe<br />

betydning, behandles i det selvstændige fagomr˚ade numerisk analyse.<br />

Afrundingsfejl er mere problematiske end de andre fejl, vi hidtil har mødt (for eksempel<br />

division med nul), fordi man ikke af resultatet kan se, at der er noget galt. I disse <strong>noter</strong> vil<br />

vi ikke gøre mere ud af emnet, men problemet kan illustreres med et lille eksempel, hvor<br />

man f˚ar to forskellige resultater ud af at beregne det samme udtryk p˚a to forskellige m˚ader:<br />

val lilletal = Math.pow (2.0,~53.0);<br />

val lilletal = 1.11022302463E~16 : real<br />

1.0 - 1.0 + lilletal;<br />

val it = 1.11022302463E~16 : real<br />

1.0 + lilletal - 1.0;<br />

val it = 0.0 : real<br />

3.8.2 Typerne af tegn og tekster<br />

I vores brug af SML vil data til og fra programmer i sidste ende være tekster opbygget af<br />

tegn. Hvis man selv vil styre formaterne for ind- og uddata, er det derfor vigtigt at kunne<br />

manipulere med værdier af typerne string og char. Da tekster er nært beslægtede med<br />

lister af tegn, udskydes detaljerne om disse typer imidlertid til efter indføring af typen af<br />

lister.<br />

3.9 Overlæssede symboler<br />

Man kan undre sig over typen i systemets svar p˚a definitionen<br />

fun sum (a,b) = a + b;<br />

val sum = fn : int * int -> int<br />

eftersom mange af eksemplerne ovenfor brugte “+” til addition af værdier af type real.<br />

Forklaringen er, at symbolet “+” i SML i virkeligheden st˚ar for to forskellige operatorer<br />

53


(to forskellige funktioner med infiks-status), idet op + b˚ade har type int * int -> int og<br />

type real * real -> real. Hvis den rigtige funktion fremg˚ar entydigt af sammenhængen,<br />

bruger systemet den, men hvis ikke 5 , formodes man at ønske den variant, der bruger<br />

heltallige operander og har et heltalligt resultat.<br />

Et symbol, der p˚a den m˚ade har flere betydninger, siges at være overlæsset (eng.:overloaded).<br />

For subtraktion og multiplikation er forholdet det samme, idet ogs˚a “-” og “*” b˚ade<br />

kan bruges mellem heltallige operander og give et heltalligt resultat og mellem operander<br />

af type real med et resultat af type real. Hertil kommer for “*”, som vi har set, tillige en<br />

tredje betydning som kartesisk produkt af typer. (Denne sidste brug af symbolet kan altid<br />

skelnes fra de andre, idet det vil være klart, om “*” er placeret mellem værdier eller mellem<br />

typer.)<br />

Der er yderligere tre grupper af overlæssede symboler:<br />

Funktionerne “abs” og “~”, som blev brugt i de indledende eksempler, har b˚ade type<br />

int -> int og type real -> real. Desuden kan “~” som nævnt indg˚a i negative talkonstanter.<br />

De fire ordningsoperatorer “=” kan b˚ade bruges p˚a typerne int, real,<br />

char og string.<br />

Lighedsoperatorerne “=” og “” kan bruges p˚a alle s˚akaldte “lighedstyper”, hvilket groft<br />

sagt vil sige alle typer, der ikke indeholder funktioner.<br />

Ogs˚a for “abs”, “~”, de fire ordninger samt “=” og “” gælder, at hvis typen ikke<br />

fremg˚ar af sammenhængen, falder SML tilbage p˚a heltalsudgaven.<br />

Man kan ikke selv definere nye overlæssede symboler.<br />

3.9.1 Typebegrænsning<br />

Hvis man i stedet for et udtryk u skriver u : τ, hvor τ er en type, tvinges udtrykket til at<br />

have type τ (og reglerne for typeudledning kan betyde, at typekravet forplanter sig videre<br />

til andre deludtryk). Med denne konstruktion (en “typebegrænsning”, (eng.:type constraint<br />

eller type cast)) kan man omg˚a den underforst˚aede heltalstype. Hver af linjerne nedenfor vil<br />

definere en funktion til addition af to reelle tal:<br />

fun sumr (a : real,b) = a + b;<br />

fun sumr (a,b) : real = a + b;<br />

fun sumr (a,b) = a + (b : real);<br />

fun sumr (a,b) = a + b : real;<br />

3.10 Definitionen af Standard ML<br />

Programmer i et højere programmeringssprog bør (som nævnt i afsnit 1.5.3) med uændret<br />

effekt kunne flyttes mellem forskellige implementerende systemer. En betingelse for, at det<br />

kan lade sig gøre, er imidlertid, at programmeringssproget er s˚a præcist defineret, at der ikke<br />

kan herske tvivl om, hvilke sprogkonstruktioner der er lovlige, og hvorledes de skal virke.<br />

5 Dette er en af nyhederne i SML97 [9] i forhold til SML90 [7].<br />

54


Antallet af programmeringssprog er meget stort, og langt de fleste af dem er ikke fastlagt<br />

mere præcist, end at der har været mulighed for at fortolke definitionen p˚a forskellige m˚ader,<br />

s˚adan at sprogets implementationer realiserer forskellige “dialekter”.<br />

Standard ML indtager en særstilling i denne henseende; ved at bruge en stringent matematisk<br />

notation sikrer det definerende dokument [9] en usædvanlig grad af entydighed.<br />

3.10.1 Skandering i grundsymboler<br />

ML er defineret med henblik p˚a, at programmer skal kunne skrives med tegnene fra ASCII<br />

(se afsnit 1.7.1). De 94 ikke-blanke grafiske tegn fra dette alfabet grupperes s˚aledes:<br />

bogstav a b c d e f g h i j k l m n o p q r s t u v w x y z<br />

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z<br />

ciffer 0 1 2 3 4 5 6 7 8 9<br />

alfanum.tegn ’ bogstav ciffer<br />

navnetegn ! # $ % & * + - / : < = > ? @ \ ^ ‘ | ~<br />

specialtegn , ; . "( ) [ ] { }<br />

Der skelnes mellem sm˚a og store bogstaver, men læg mærke til, at de danske æ, ø, ˚a, Æ, Ø<br />

og ˚A ikke regnes med. Ud over de viste tegn kan man i programmer bruge blanktegn (kode<br />

32), tabuleringstegn (kode 9) og linjeskift (kode 10); desuden kan kommentarer (se 3.10.1)<br />

samt tekst- og tegnkonstanter (se afsnit 7.1 nedenfor) i mosml indeholde andre tegn med<br />

koder mellem 0 og 255.<br />

Som nævnt samles enkelttegn til grundsymboler, og dem er der fire arter af: reserverede<br />

ord, typevariable, navne og konstanter.<br />

Reserverede ord<br />

De reserverede ord udgøres i mosml af følgende 58 enkelttegn eller følger af tegn. Intet af<br />

dem (med undtagelse af “=”) m˚a bruges som navn:<br />

abstype and andalso as case do datatype else end eqtype exception<br />

fn fun functor handle if in include infix infixr let local nonfix<br />

of op open orelse raise rec sharing sig signature struct structure<br />

then type val where with withtype while : :> | = => -> # , ; ...<br />

( ) [ ] { }<br />

Typevariable<br />

Reserverede ord i mosml<br />

En typevariabel er en apostrof efterfulgt af et eller flere alfanumeriske tegn, for eksempel:<br />

’a. Vi vil først møde typevariable i kapitel 4 nedenfor.<br />

Navne<br />

De to former for navne, alfanumeriske og symbolske, blev forklaret i afsnit 2.4.1 ovenfor.<br />

De reserverede ord er ikke med i klassen af navne; mens es, ## og |=| s˚aledes er navne,<br />

er as, # og | det ikke.<br />

55


Konstanter<br />

I mosml er der fem former for konstanter: Heltalskonstanter, flydende talkonstanter, tekstkonstanter,<br />

tegnkonstanter og ordkonstanter.<br />

Konstanter for hele og brudne tal blev forklaret i afsnit 2.5.1.<br />

Tekstkonstanter og tegnkonstanter behandles i afsnit 7.1 nedenfor.<br />

Ordkonstanter skal ikke benyttes p˚a dette kursus (se [14]).<br />

Kommentarer<br />

Selv om programmer skrives for at blive fortolket af maskiner, bliver de til i et konstruktionsforløb<br />

med stadige rettelser og udbygninger, hvorunder de i høj grad ogs˚a læses af<br />

mennesker, og et program skal ikke være ret langt, før det bør suppleres med forklaringer.<br />

At f˚a et program i hænde, man skrev for ˚ar eller blot f˚a m˚aneder siden, er en lærerig oplevelse:<br />

det er forbavsende, hvor lang tid det tager at komme ind i de gamle tankebaner og<br />

igen forst˚a, hvad der, dengang man konstruerede programmet, syntes s˚a oplagt.<br />

Et program skal derfor ikke være særlig omfattende, før det kan betale sig at forsyne<br />

det med kommentarer. I ML benyttes to-tegns-symbolerne “(*” og “*)” til henholdsvis at<br />

indlede og afslutte en kommentar, og imellem disse to symboler kan man skrive hvad som<br />

helst — tegn fra og med “(*” og til og med “*)” vil blive oversprunget, n˚ar ML skal fortolke<br />

et program.<br />

Beskrivelsen af, hvad der er en kommentar, er ikke helt præcis; her er en nøjagtig forklaring:<br />

Lad os bruge betegnelsen kommentarsymbol om enten en kommentar eller en følge af<br />

nul eller flere tegn, der ikke indeholder noget af symbolerne “(*” eller “*)”. En kommentar<br />

er da en vilk˚arlig følge af (nul eller flere) kommentarsymboler indesluttet mellem symbolerne<br />

“(*” og “*)”.<br />

˚Arsagen til denne omstændelige definition er, at ved siden af at give mulighed for indsættelse<br />

af forklaringer er der ogs˚a er en anden nyttig anvendelse af kommentarer:<br />

Skal man finde ud af, hvor i et program en fejl befinder sig, kan der være brug for at sætte<br />

dele af et program ud af kraft. Direkte at slette de p˚agældende dele er ikke attraktivt, for<br />

man risikerer senere møjsommeligt at skulle indtaste dem igen. Her kan man benytte sig af,<br />

at programdele fra “(*” til “*)” overspringes: det, som ønskes sat ud af kraft, “kommenteres<br />

ud” ved at blive omgivet af symbolerne “(*” og “*)”, der let igen senere kan fjernes. Den<br />

s˚aledes indesluttede programtekst kunne imidlertid selv indeholde kommentarer, og hvis reglen<br />

blot var, at en kommentar sluttede, s˚a snart systemet mødte symbolet “*)”, ville det g˚a<br />

galt. Derfor forlanges det, at “(*” og “*)” skal danne en korrekt indlejret parentesstruktur.<br />

Opstilling<br />

De enkelte tegn i et grundsymbol skal følge lige efter hinanden, men bortset herfra kan MLprogrammer<br />

stilles helt frit op p˚a linjerne: mellem grundsymboler kan man sætte vilk˚arlige<br />

følger af blanktegn, tabuleringstegn, linjeskift og kommentarer.<br />

Reglen er, at hvert grundsymbol strækker sig s˚a langt mod højre, som det er muligt i<br />

henhold til beskrivelserne ovenfor. Hvis to p˚a hinanden følgende grundsymboler kan læses<br />

som ét grundsymbol, m˚a de alts˚a adskilles af blanktegn, tabuleringstegn, linjeskift eller<br />

kommentar.<br />

56


3.10.2 Gruppering i syntaktiske helheder<br />

Som nævnt i afsnit 2.4 grupperes grundsymbolerne i større helheder. Til beskrivelse af syntaksen<br />

for SML benyttes en ret suggestiv formel grammatik, der er en variant af den s˚akaldte<br />

“Backus-Naur-form” (forkortet BNF 6 ), som blev brugt i rapporten om programmeringssproget<br />

Algol 60.<br />

Grammatikken definerer nogle forskellige syntaktiske klasser, blandt andet udtryk exp,<br />

infiks-udtryk infexp, typer ty, alternativer match og alternativ mrule.<br />

Hver syntaktisk klasse defineres i en eller flere linjer, som angiver de forskellige mulige<br />

udformninger af den klasses elementer. En lille del af grammatikken gengives i tabel 3.1,<br />

der viser definitionerne af exp 7 og match. Mange af de viste konstruktioner har endnu ikke<br />

været behandlet; vi vil møde dem senere.<br />

exp ::= infexp<br />

exp : ty typebegrænsning<br />

exp 1 andalso exp 2 konjunktion<br />

exp 1 orelse exp 2 disjunktion<br />

exp handle match gribning af undtagelse<br />

raise exp kast af undtagelse<br />

if exp 1 then exp 2 else exp 3 binært valg<br />

case exp of match flerdelt valg<br />

fn match funktion<br />

match ::= mrule 〈 | match〉<br />

Tabel 3.1: En lille del af syntaksen for udtryk i Standard ML<br />

Tekst med skrivemaskinetype skal anføres bogstaveligt; kursiverede navne er syntaktiske<br />

klasser, som grammatikken definerer. Dele i vinkelparenteser 〈. . .〉 kan medtages eller<br />

udelades. Den rækkefølge, mulighederne anføres i, har betydning, idet de først anførte binder<br />

stærkest. Grammatikken viser derfor, at konjunktion binder stærkere end disjunktion; i<br />

udtrykket<br />

a orelse b andalso c<br />

er parentesen underforst˚aet omkring konjunktionen:<br />

a orelse (b andalso c)<br />

Mere om definitionen af Standard ML kan findes i [5, Appendix B].<br />

3.11 Sammenfatning<br />

Funktionsdefinitioner kan bruge flere parametermønstre; de forskellige tilfælde adskilles da<br />

af en lodret streg. Mønstrenes variable har kun deres eget tilfælde som virkefelt; funktionsnavnet<br />

har alle mulighederne samt resten af programmet som virkefelt; et navn, der defineres<br />

med val, har resten af programmet som virkefelt.<br />

6 oprindeligt en forkortelse for “Backus normalform”.<br />

7 En enkelt mulighed (iteration), som hører til den imperative del af sproget, er udeladt her.<br />

57


Division med nul, argumenter uden for definitionsomr˚adet og overløb afbryder udførelsen<br />

med besked om en “undtagelse”.<br />

Logisk konjunktion og disjunktion er i SML ikke funktioner eller operatorer, men sekventielle<br />

og hedder andalso og orelse.<br />

Regning med reelle tal giver risiko for afrundingsfejl.<br />

Overlæssede funktioner og operatorer bruger hel type, hvis konteksten ikke p˚atrykker<br />

andet. Det kan ændres med en typebegrænsning.<br />

Tvivl om sprogkonstruktioner afklares fuldstændigt af sprogets formelle definition.<br />

3.12 Opgaver<br />

Opgave 3.1 Skriv en funktion, som finder ud af, om et naturligt tal n indeholder cifferet<br />

“5”. (Mere præcist: . . . som for en parameter n : int finder ud af, om “5” ville indg˚a, hvis<br />

n blev skrevet som talord i titalssystemet.)<br />

Opgave 3.2 Definer funktionen pythagoras : real * real * real -> bool, som undersøger,<br />

om tre tal kan være sidelængder i en retvinklet trekant. Der kan ikke forudsættes<br />

nogen bestemt ordning af tallene; man kan alts˚a ikke vide, hvilket af de tre tal der (i givet<br />

fald) er hypotenusen.<br />

Opgave 3.3 Ved en matrix forst˚as en rektangulær opstilling af elementer. Hvis opstillingen<br />

(for to positive hele tal m og n) har m rækker og n søjler, taler man om en m × n-matrix.<br />

Tallene m og n kan være ens, og s˚a er matricen kvadratisk, men de behøver ikke at være det.<br />

Matricen indeholder alts˚a mn elementer, og det er almindeligt at give elementerne to<br />

indices: det første for nummeret p˚a den række og det andet for nummeret p˚a den søjle,<br />

elementet st˚ar i. Det j-te element i den i-te række <strong>noter</strong>es derfor ai,j eller blot aij.<br />

I første del af denne opgave vil vi g˚a ud fra, rækker og søjler er nummereret startende<br />

med 0, s˚a matricen kommer til at se s˚aledes ud:<br />

⎧<br />

n<br />

<br />

⎧<br />

<br />

j<br />

<br />

⎪⎨<br />

a0,0 . . . a0,j−1 a0,j . . . a0,n−1<br />

i .<br />

. .<br />

.<br />

⎪⎨<br />

ai−1,0 . . . ai−1,j−1 ai−1,j . . . ai−1,n−1<br />

m ⎪⎩<br />

⎪⎩<br />

ai,0 . . . ai,j−1<br />

.<br />

am−1,0 . . . am−1,j−1<br />

.<br />

ai,j<br />

.<br />

am−1,j<br />

. . .<br />

. ..<br />

. . .<br />

ai,n−1<br />

.<br />

am−1,n−1<br />

Fordelen ved at begynde nummerering med 0 er, at der da kommer til at gælde den simple<br />

lovmæssighed, at der for ethvert naturligt tal k netop g˚ar k elementer forud for elementet<br />

med indeks k.<br />

58


Hvis elementerne i en matrix skal opstilles lineært, er det almindeligt at benytte enten<br />

rækkevis eller søjlevis udlægning. Lagt ud række for række bliver den viste matrix til (oven<br />

over nogle af elementerne vises deres løbenummer mellem 0 og mn − 1):<br />

0 . . . j − 1 j . . . n − 1 n . . . (i − 1)n . . . . . . in − 1<br />

. . .<br />

a0,0 . . . a0,j−1 a0,j . . . a0,n−1 a1,0 . . . ai−1,0 . . . ai−1,j−1 ai−1,j . . . ai−1,n−1<br />

in . . . . . . (i + 1)n . . . (m − 1)n . . . . . . mn − 1<br />

. . .<br />

ai,0 . . . ai,j−1 ai,j . . . ai,n−1 ai+1,0 . . . am−1,0 . . . am−1,j−1 am−1,j . . . am−1,n−1<br />

mens søjlevis udlægning naturligvis betyder, at elementerne kommer i rækkefølgen<br />

0 . . . i − 1 i . . . m − 1 m . . . (j − 1)m . . . . . . jm − 1<br />

. . .<br />

a0,0 . . . ai−1,0 ai,0 . . . am−1,0 a0,1 . . . a0,j−1 . . . ai−1,j−1 ai,j−1 . . . am−1,j−1<br />

jm . . . . . . (j + 1)m . . . (n − 1)m . . . . . . mn − 1<br />

. . .<br />

a0,j . . . ai−1,j ai,j . . . am−1,j a0,j+1 . . . a0,n−1 . . . ai−1,n−1 ai,n−1 . . . am−1,n−1<br />

Skriv en funktion, som ud fra antallet af rækker og søjler og et elements nummer i<br />

den rækkevise udlægning beregner, hvilket nummer det samme element vil have i søjlevis<br />

udlægning.<br />

a11 a12 . . . a1n<br />

a21 a22 . . . a2n<br />

.<br />

.<br />

. ..<br />

am1 am2 . . . amn<br />

En matrix med mn elementer i m rækker og n søjler<br />

a11 a12 . . . a1n a21 a22 . . . a2n . . . . . . am1 am2 . . . amn<br />

Elementerne udlagt rækkevis<br />

a11 a21 . . . am1 a12 a22 . . . am2 . . . . . . a1n a2n . . . amn<br />

Elementerne udlagt søjlevis<br />

Løs derefter samme opgave, hvor nummereringen begynder med 1 (se figuren).<br />

Opgave 3.4 Funktionen gcd (afsnit 3.5.2) følger ikke helt Euklids algoritme som beskrevet<br />

i teksten; blandt andet sørges der ikke for at ordne parametrene, s˚a |a| ≤ |b|. Hvorfor er det<br />

ikke nødvendigt?<br />

Opgave 3.5 Største fælles divisor for to tal a og b har følgende egenskaber (hvor der først<br />

og fremmest skelnes imellem, om tallene er lige eller ulige). For nemheds skyld vil vi her<br />

59<br />

.


antage, at hverken a eller b er negativt:<br />

gcd(a, 0) = a og tilsvarende, hvis det er a, som er 0<br />

gcd(a, b) = 2 · gcd( a<br />

gcd(a, b) =<br />

b , ) 2 2<br />

gcd(<br />

hvis a og b begge er lige<br />

a,<br />

b) 2 hvis a er lige og b ulige (og tilsvarende, hvis<br />

det omvendt er a, der er ulige, og b er lige)<br />

gcd(a, b) = gcd(a − b, b) hvis b˚ade a og b er ulige, og a ≥ b. (Tilsvarende,<br />

hvis de begge er ulige, og b ≥ a)<br />

Programmer den s˚akaldte binære gcd-funktion, der bygger p˚a de nævnte egenskaber.<br />

Opgave 3.6 Definer den faldende potensfunktion, for hvilken<br />

i SML.<br />

(a) Definitionen kan benytte egenskaberne<br />

x n = x · (x − 1) · · · (x − n + 1)<br />

<br />

n<br />

x 0 = 1<br />

x n = x · (x − 1) n−1 for n > 0<br />

Det er klart, at n skal være af hel type. Indret definitionen, s˚a x f˚ar reel type.<br />

(b) Udvid den faldende potensfunktion til negative hele eksponenter via definitionen<br />

x −n = 1<br />

(x+n) n = 1<br />

(x+1)···(x+n) for n > 0<br />

Opgave 3.7 Omform disse udtryk til en mere hensigtsmæssig form:<br />

if not a then b else true;<br />

if n mod 2 0 then false else if n mod 3 0 then false else true;<br />

Opgave 3.8 Argumenter for, at under evaluering af de to udtryk<br />

a andalso (b andalso c)<br />

og<br />

(a andalso b) andalso c<br />

udløses de samme beregninger. (Husk at medtage muligheden for, at a, b eller c kunne g˚a<br />

i uendelig løkke.) Udled heraf, at de to udtryk altid har samme værdi (alts˚a at sekventiel<br />

konjunktion er associativ).<br />

Analogt for sekventiel disjunktion.<br />

60


Kapitel 4<br />

Produkttype. Terminering. Blokke<br />

4.1 Strukturerede typer<br />

I ML er der en række grundtyper (for eksempel int og bool) og en række m˚ader at danne<br />

nye typer p˚a ud fra eksisterende typer (for eksempel funktionstypen τ -> τ ′ ud fra τ og τ ′ ).<br />

Tabel 4.1 viser de former for typer, vi vil bruge i dette kursus. (En fuldstændig oversigt<br />

kan ses i [5, Appendix B] eller [7, 9, 12, 14].) Et par af typerne skal vi først møde senere,<br />

men for overskuelighedens skyld er de taget med i tabellen.<br />

ty ::= unit enhedstypen<br />

bool sandhedsværdi<br />

order sammenligningsresultat<br />

int heltalsord<br />

real flydende talord<br />

char tegn<br />

string tekst<br />

tyvar typevariabel<br />

ty list liste<br />

ty 1 * ty 2 * . . . * ty n<br />

sæt (n ≥ 2)<br />

ty -> ty ′ funktion<br />

( ty )<br />

Tabel 4.1: Forenklet syntaks for typer i ML.<br />

Som nedskrevet er syntaksen ikke entydig, men flertydighederne løses ved at give konstruktionerne<br />

prioritet i den rækkefølge, de st˚ar i tabellen, s˚aledes at tidligere konstruktioner<br />

har forrang frem for senere. Desuden lader man operatorsymbolet “->” for funktionstypekonstruktion<br />

associere fra højre. (Hvad dette nærmere indebærer, vender vi tilbage til i<br />

kapitel 6.)<br />

4.2 Sæt<br />

En af typekonstruktionerne danner par, tripler, kvadrupler og s˚a videre, n-sæt for vilk˚arligt<br />

n = 2, 3, 4, . . .: Hvis u1 er et udtryk af type τ1, u2 et udtryk af type τ2, . . . , un et udtryk af<br />

61


type τn, betegner (u1,u2,. . .,un) sættet (eng.:tuple) af de n udtryks værdier, og typen af<br />

dette sæt betegnes τ1 * τ2 * . . . * τn.<br />

Som typekonstruktør er asterisk ikke associativ: Der skelnes mellem triplet (1,2,3) af<br />

type int * int * int, parret (1,(2,3)) af type int * (int * int) og parret ((1,2),3)<br />

af type (int * int) * int.<br />

Alle funktioner er af én variabel I tidligere eksempler har vi defineret funktioner “af<br />

flere variable”, for eksempel i afsnit 2.3.3 funktionerne gaar op i : int * int -> bool<br />

og gnsnt : real * real * real -> real og i afsnit 3.5 omtaltes biblioteksfunktionen<br />

Int.min : int * int -> int. Vi ved nu, at disse funktioners typer skal grupperes henholdsvis<br />

(int * int) -> bool, (real * real * real) -> real og (int * int) -> int.<br />

Der er med andre ord “i virkeligheden” ikke tale om funktioner af flere variable, men om<br />

funktioner af ét argument, hvor dette argument er et sæt (i eksemplerne: et par eller et<br />

tripel).<br />

At det forholder sig s˚adan, kommer frem i dette eksempel:<br />

val femtenfem = (15,5);<br />

val femtenfem = (15, 5) : int * int<br />

gaar op i femtenfem;<br />

val it = false : int<br />

Int.min femtenfem;<br />

val it = 5 : int<br />

I definitionerne af gaar op i og af gnsnt er der p˚a venstre side af lighedstegnet som<br />

mønster brugt et par og et tripel. Ved kald af disse funktioner er virkningen da (som nævnt<br />

i afsnit 3.2), at argumentets aktuelle værdi tilpasses dette mønster, hvorved man kan bestemme,<br />

hvilke værdier de formelle parametre skal bindes til i funktionens krop.<br />

Her er en udbygning af eksemplet:<br />

fun divmod (n,d) = (n div d,n mod d);<br />

val divmod = fn : int * int -> int * int<br />

divmod femtenfem<br />

val it = (3,0) : int * int<br />

gaar op i (divmod femtenfem);<br />

val it = true : bool<br />

4.2.1 Enhedstypen<br />

Typen bool havde to egentlige værdier, og som et udartet tilfælde er der i ML ogs˚a defineret<br />

en type, der kun har én egentlig værdi. Typen betegnes unit, og dens værdi skrives som et<br />

“tomt sæt”: ().<br />

I en type med kun én mulig værdi ligger naturligvis ingen information, men unit bruges<br />

i ML i de tilfælde, hvor man har brug for andre systemaktiviteter end at f˚a evalueret et<br />

udtryk til en værdi.<br />

Brugere af Moscow ML vil allerede have mødt et eksempel herp˚a, nemlig i den funktion,<br />

der kaldes, n˚ar man vil forlade systemet:<br />

62


quit;<br />

val it = fn : unit -> unit<br />

quit ();<br />

$ ><br />

Et andet eksempel er systemfunktionen load til indhentning af biblioteksmoduler som<br />

nævnt i afsnit 3.4. Som parameter skal den have navnet p˚a det modul, der skal hentes, men<br />

selve funktionsværdien skal ikke bruges; det er funktionens “bivirkning” p˚a navnerummet,<br />

man har glæde af. Det forklarer funktionens type:<br />

load;<br />

val it = fn : string -> unit<br />

4.3 Komponentvalg<br />

De eneste operationer, der fra systemets side er defineret p˚a sæt, er projektionsfunktionerne<br />

#1, #2, #3, . . . , der er specielle tilfælde af Standard MLs feltvalg (eng.:record-selectors).<br />

Virkningen af #n er simpelt hen at udvælge den n-te komponent af et sæt (idet nummerering<br />

begynder med 1). Vi fortsætter eksemplet fra før:<br />

femtenfem;<br />

val it = (15,5) : int * int<br />

#1 femtenfem;<br />

val it = 15 : int<br />

#2 femtenfem;<br />

val it = 5 : int<br />

#3 femtenfem;<br />

! Toplevel input:<br />

! #3 femtenfem;<br />

! ^^^^^^^^^<br />

! Type clash: expression of type<br />

! int * int<br />

! cannot have type<br />

! {1 : int, 2 : int, 3 : ’a, ...}<br />

! because record label 3 is missing<br />

Forsøg p˚a at udvælge en komponent med større nummer end sættets længde er naturligvis<br />

en fejl. N˚ar den melding, man f˚ar om fejlen, kan forekomme lidt kryptisk, hænger det sammen<br />

med, at sæt i ML opfattes som specialtilfælde af en mere generel konstruktion af poststrukturer<br />

(eng.:records), som vi ikke her vil komme nærmere ind p˚a.<br />

NB Læg mærke til, at der efter nummertegnet skal skrives en konstant. Man kan alts˚a ikke<br />

beregne sig frem til, hvilken komponent man vil bruge: beslutningen træffes, n˚ar et program<br />

skrives ned.<br />

63


4.3.1 Projektionsfunktionerne<br />

I forhold til andre funktioner indtager projektionsfunktionerne #1, #2, #3, . . . en særstilling;<br />

man kan ikke anføre dem helt isoleret:<br />

#1;<br />

! Toplevel input:<br />

! #1;<br />

! ^^<br />

! Unresolved record pattern<br />

Problemet er, at man ikke af en projektionsfunktion kan se, hvor langt et sæt den skal<br />

vælge fra. Da sæt af forskellig længde har forskellig type, kan systemet ikke bestemme typen<br />

af udtrykket, og det giver en fejlmelding.<br />

Definerer man selv sine projektionsfunktioner, opst˚ar problemet ikke:<br />

fun fst2 (x,y) = x;<br />

val (’a, ’b) fst2 = fn : ’a * ’b -> ’a<br />

fst2;<br />

val (’a, ’b) it = fn : ’a * ’b -> ’a<br />

(4.1)<br />

At den definerede funktion fst2 kan anvendes p˚a et hvilketsomhelst par, viser sig ved,<br />

at der forekommer typevariable i den af systemet udledte type. Udtryk, i hvis type der<br />

forekommer typevariable, siges at være polymorfe (egentlig fra græsk: af flere former). Vi vil<br />

have mere at sige om polymorfi i afsnit 4.5 nedenfor.<br />

I konkrete anvendelser fst2 x kan x have en hvilken som helst partype — for eksempel<br />

int * int, bool * int, (int * int) * int eller (bool -> int) * bool<br />

En anden mulighed for at undg˚a fejlmeldingen “Unresolved record pattern” er med<br />

den konstruktion<br />

exp : type<br />

der i afsnit 3.9.1 blev kaldt en “type-begrænsning”, at informere systemet om, hvilken type<br />

det drejer sig om:<br />

#1 : ’foerste * ’anden -> ’foerste;<br />

val (’foerste, ’anden) it = fn : ’foerste * ’anden -> ’foerste<br />

#1 : int * bool -> int;<br />

val it = fn : int * bool -> int<br />

En typebegrænsning kan lægges p˚a hele udtrykket eller p˚a deludtryk, og det er ofte<br />

tilstrækkeligt at indføre en begrænsning et enkelt sted — resten kan systemet udlede:<br />

fun fst2 (x,y) = x : int;<br />

val ’a fst2 = fn : int * ’a -> int<br />

64


Sætmønster eller projektioner?<br />

Om man vil bruge et sætmønster som parameter eller foretrækker at indsætte de nødvendige<br />

projektioner, er et spørgsm˚al om programmeringsstil; man skal naturligvis bruge den notation,<br />

som forekommer klarest. Her er flere forskellige udgaver af en funktion, der viser et<br />

klokkeslæt som timer og minutter:<br />

fun visKlokken (t,m)<br />

= Int.toString t ^ "timer og "^ Int.toString m ^ "minutter";<br />

val visKlokken = fn : int * int -> string<br />

Bruges projektionsfunktioner, skal længden af sættet p˚a en eller anden m˚ade fremg˚a. Her<br />

er to muligheder:<br />

fun visKlokken (kl : int * int)<br />

= Int.toString (#1 kl) ^ "timer og "<br />

^ Int.toString (#2 kl) ^ "minutter";<br />

val visKlokken = fn : int * int -> string<br />

fun visKlokken (0,0) = "midnat"<br />

| visKlokken kl<br />

= Int.toString (#1 kl) ^ "timer og "<br />

^ Int.toString (#2 kl) ^ "minutter";<br />

val visKlokken = fn : int * int -> string<br />

4.3.2 Joker<br />

Hvis der ikke er brug for at referere til en del af (eller hele) et mønster, kan man spare sig<br />

at finde p˚a et navn og det p˚agældende sted blot sætte et understregningstegn, der da vil<br />

fungere som joker (eng.:wildcard), det vil sige passe med enhver værdi. Funktionen fst2 fra<br />

(4.1) kunne have været erklæret<br />

fun fst2 (x, ) = x<br />

4.4 Blok<br />

Ofte skal det samme deludtryk bruges flere gange i en større kontekst<br />

. . . deludtryk . . . deludtryk . . . deludtryk . . .<br />

men hvis udtrykket er omfattende, er det ikke hensigtsmæssigt blot at indsætte det hver<br />

gang; dels kan man let komme til at skrive forkert en af gangene, og dels vil der g˚a tid<br />

med at genevaluere hver forekomst af deludtrykket (selv om alle evalueringer vil give samme<br />

resultat).<br />

Det er bedre med en værdierklæring at give deludtrykkets værdi et eller andet navn, og<br />

s˚a bruge dette navn i den større kontekst:<br />

val navn = deludtryk;<br />

. . . navn . . . navn . . . navn . . .<br />

65


— p˚a den m˚ade vil værdien kun blive beregnet én gang, og det omfattende deludtryk skal<br />

kun nedskrives én gang.<br />

Nu vil navn imidlertid være til r˚adighed i resten af dialogen, selv om der kun var brug<br />

for det inde i konteksten. For at undg˚a denne uhensigtsmæssighed er det i Standard ML<br />

med en s˚akaldt blok muligt at angive, at en erklæring kun skal have lokal effekt. Det ser<br />

s˚aledes ud<br />

let val navn = deludtryk in . . . navn . . . navn . . . navn . . . end<br />

Som nævnt i afsnit 3.3 har erklæringer p˚a yderste niveau gyldighed i resten af programmet.<br />

For lokale erklæringer markerer end afslutningen af deres virkefelt:<br />

let<br />

Indledende erklæringer A<br />

val mønster = udtryk B<br />

Øvrige erklæringer C<br />

in Blokkens krop D end<br />

Resten af programmet E<br />

Virkefeltet for de navne, som indg˚ar i mønster, er C og D.<br />

let<br />

Indledende erklæringer A<br />

fun fnavn . . .<br />

| fnavn mønster i = funktionskrop Bi<br />

| fnavn . . .<br />

Øvrige erklæringer C<br />

in Blokkens krop D end<br />

Resten af programmet E<br />

Virkefeltet for de parameternavne, der indg˚ar i mønster i er Bi, og for fnavn er det Bi,<br />

C og D.<br />

Læg mærke til, at der godt kan st˚a mere end en erklæring mellem let og in, og at hver<br />

erklæring i s˚a fald har adgang til de tidligere definerede navne.<br />

Eksempel Lad os konstruere en funktion, der ud fra et begyndelsestidspunkt (t0, m0, s0)<br />

angivet i timer, minutter og sekunder og en varighed s i sekunder beregner sluttidspunktet<br />

som et tilsvarende tripel.<br />

Her er funktionen:<br />

fun sluttid ((t0,m0,s0),s)<br />

= let val ss = s0 + s<br />

val mm = m0 + ss div 60<br />

val tt = t0 + mm div 60<br />

in (tt mod 24,mm mod 60,ss mod 60) end;<br />

val sluttid = fn : (int * int * int) * int -> int * int * int<br />

sluttid ((23,35,00),1800);<br />

val it = (0, 5, 0) : int * int * int<br />

66


Udgangsformat Det ville passe bedre med normal skrivem˚ade, hvis resultatet ovenfor<br />

var blevet vist som (0,05,00) , men hvordan man mere nuanceret selv kan styre uddatas<br />

format, kommer vi først ind p˚a i kapitlet om tegn og tekster.<br />

4.4.1 Analytisk brug af val<br />

Som nævnt i afsnit 3.3 kan venstre side af en værdierklæring være et mønster indeholdende<br />

flere navne. N˚ar dette mønster tilpasses højresiden, tillægges alle navnene værdi p˚a en gang.<br />

Denne “analytiske” brug af val illustreres ved, at vi bygger videre p˚a eksemplet fra<br />

afsnit 4.2:<br />

val (m,n) = femtenfem;<br />

val m = 15 : int<br />

val n = 5 : int<br />

fun spejltocifret n = let val (c1,c2) = divmod (n,10)<br />

in c2 * 10 + c1 end;<br />

fun spejltocifret = fn : int -> int<br />

spejltocifret 43;<br />

val it = 34 : int<br />

4.4.2 Udtryk og erklæringer; let og local<br />

Standard ML skelner mellem en erklæring (eng.:declaration) (som definerer et eller flere<br />

navne) og et udtryk (eng.:expression) (til beregning af en værdi). Konstruktionen<br />

let erklæring in udtryk end<br />

bruges, n˚ar en erklæring (som godt kan være flere erklæringer) skal virke lokalt i et udtryk.<br />

Der er en fuldstændig tilsvarende konstruktion, hvor let blot er skiftet ud med local<br />

local erklæring in erklæring end<br />

som man kan bruge, hvis en erklæring skal virke lokalt i en erklæring.<br />

Eksempel Her defineres en funktion, som omsætter en tidsangivelse i timer, minutter og<br />

sekunder til sekunder:<br />

local fun omregn60 (a,b) = a * 60 + b<br />

in fun sekunder (t,m,s) = omregn60 (omregn60 (t,m),s)<br />

end;<br />

val sekunder = fn : int * int * int -> int<br />

sekunder (9,15,00);<br />

val it = 33300 : int<br />

67


4.5 Polymorfi<br />

Betragt disse eksempler:<br />

fun id x = x;<br />

val ’a id = fn : ’a -> ’a<br />

fun f n = 0;<br />

val ’a f = fn : ’a -> int<br />

fun g n = g (n + 1);<br />

val ’a g = fn : int -> ’a<br />

fun vendpar (x,y) = (y,x);<br />

val (’a, ’b) vendpar = fn : ’a * ’b -> ’b * ’a<br />

ML-systemet udleder den mest generelle type, et udtryk kan have. Det kan føre til, at<br />

typen skal udtrykkes med typevariable, der i ML skrives som en apostrof efterfulgt af et eller<br />

flere alfanumeriske tegn. I afsnit 4.3.1 var ’foerste og ’anden for eksempel typevariable.<br />

Skal systemet generere typevariable, bruger det ’a, ’b, ’c, . . . (Nogle lærebøger bruger de<br />

græske bogstaver α, β, γ, . . . som typevariable.)<br />

Muligheden for at konstruere polymorfe funktioner er et afgørende element i ML’s store<br />

anvendelighed og fleksibilitet.<br />

4.5.1 Værdipolymorfi<br />

For at undg˚a problemer, n˚ar polymorfi kombineres med referencer, som er en avanceret<br />

ML-facilitet, vi ikke skal komme nærmere ind p˚a her, har det vist sig nødvendigt i bestemte<br />

situationer at begrænse polymorfien i udtryk.<br />

Pakker man linjerne (4.1) ovenfor sammen i en blok, kan det for eksempel give anledning<br />

til følgende overraskende dialog:<br />

let fun fst2 (x, ) = x in fst2 end;<br />

! Warning: Value polymorphism:<br />

! Free type variable(s) at top level in value identifier it<br />

val it = fn : ’a * ’b -> ’a<br />

val f = it;<br />

val f = fn : ’a * ’b -> ’a<br />

f (1.41,true);<br />

! Warning: the free type variable ’b has been instantiated to bool<br />

! Warning: the free type variable ’a has been instantiated to real<br />

val it = 1.41 : real<br />

f femtenfem;<br />

! Toplevel input:<br />

! f femtenfem;<br />

! ^^^^^^^^^<br />

! Type clash: expression of type<br />

! int * int<br />

! cannot have type<br />

! real * bool<br />

68


Svarene fra Moscow ML er forholdsvis udførlige: Værdipolymorfi betyder, at konstruktionen<br />

omkring den lokale fst2 ikke er “rigtig polymorf”. Dens type indeholder typevariable,<br />

men de er bare pladsholdere for typer, som endnu ikke er lagt fast. Første gang, funktionen<br />

kaldes, fastfryses disse typer, og man kan ikke senere anvende funktionen p˚a andre end dem.<br />

Da identitetsfunktionen id jo bare kopierer sit argument, skulle man ogs˚a tro, id id<br />

igen ville give en identitetsfunktion, der var lige s˚a anvendelig (og specielt lige s˚a polymorf)<br />

som den oprindelige. Men s˚adan er det ikke:<br />

id id;<br />

! Warning: Value polymorphism:<br />

! Free type variable(s) at top level in value identifier it<br />

val it = fn : ’c -> ’c<br />

it it;<br />

! Toplevel input:<br />

! it it;<br />

! ^^<br />

! Type clash: expression of type<br />

! ’d -> ’d<br />

! cannot have type<br />

! ’d<br />

! because of circularity<br />

Man siger, at ML har værdipolymorfi (eng.:value polymorphism) (se [5, afsnit 5.5], [13,<br />

afsnit 7] og [12, side 178 og 323–324]), hvorved forst˚as, at typesystemet er udbygget med<br />

en regel, ifølge hvilken kun visse udtryksformer, nemlig de s˚akaldt ikke-ekspansive udtryk<br />

(eng.:non-expansive expressions) (i [12] kaldet syntaktiske værdier (eng.:syntactic values) og<br />

i [5] værdiudtryk (eng.:value expressions)) p˚a yderste niveau m˚a have typevariable i deres<br />

type.<br />

Kort forklaret omfatter de ikke-ekspansive udtryk dem, der “har sig selv som værdi” og<br />

ikke kan beregnes yderligere, det vil sige navne, konstanter og lambdaudtryk (se afsnit 6.3).<br />

Blokke (let ...), betingede udtryk (if ..., ... orelse ..., ... andalso ...) og applikationsudtryk<br />

hører derimod ikke med blandt de ikke-ekspansive udtryk.<br />

Med passende typebegrænsninger kan man undg˚a de generelle typevariable og dermed<br />

fejlmeldingerne.<br />

4.6 Kast af standardundtagelse<br />

Standardundtagelser blev nævnt i afsnit 3.6. At kaste en undtagelse er ikke forbeholdt systemet:<br />

Med sprogelementet<br />

raise undtagelse<br />

kan man selv skrive programmer, der kaster undtagelser. Standardundtagelsen Fail skal<br />

have en tekst som parameter, s˚a man kan indbygge fejlmeldinger fra sine programmer med<br />

konstruktionen<br />

raise Fail tekst<br />

69


(Vi skal senere se, at man ogs˚a selv kan definere nye undtagelser.)<br />

Her kontrolleres et timetal før udskrift, mens umulige timetal fejlmeldes:<br />

fun visTimer timetal<br />

= if 0 string<br />

visTimer 11;<br />

val it = "11 timer": string<br />

visTimer ~2;<br />

! Uncaught exception:<br />

! Fail "ulovligt timetal"<br />

4.7 Robusthed<br />

Arealet T af en trekant med sidelængderne a, b og c kan findes af Herons formel<br />

<br />

T = s(s − a)(s − b)(s − c)<br />

idet s = a+b+c betegner trekantens halve omkreds. Vi programmerer beregningen (under<br />

2<br />

antagelse af, at man allerede har hentet kvadratrodsfunktionen ind med load "Math"):<br />

fun areal (a,b,c)<br />

= let val s = (a + b + c) / 2.0<br />

in Math.sqrt (s * (s - a) * (s - b) * (s - c))<br />

end;<br />

val areal = fn : real * real * real -> real<br />

areal (3.0,4.0,5.0);<br />

val it = 6.0 : real<br />

areal (2.1,3.2,5.4);<br />

! Uncaught exception:<br />

! Domain<br />

Stillede man dette program til r˚adighed for en bruger, der skulle beregne trekantsarealer,<br />

kunne systemundtagelsen “Domain” virke meget forvirrende.<br />

Lad os kalde et program korrekt, hvis det giver det rigtige svar for de inddata, det er<br />

beregnet til at virke p˚a. Ved programmelkonstruktion kan man ønske sig mere end korrekthed:<br />

Vi vil sige, at et program er robust, hvis det ud over at være korrekt ogs˚a reagerer p˚a<br />

en rimelig m˚ade for inddata uden for sit virkningsomr˚ade (s˚a alle værdier af argumentets<br />

type f˚ar et rimeligt svar).<br />

Typesystemet i Standard ML hjælper i høj grad med til, at programmer bliver robuste:<br />

Inddata af forkert type bliver jo afvist af relevante fejlmeddelelser. I nogle tilfælde udgør<br />

korrekte inddata imidlertid kun en delmængde af de data, hvis type passer.<br />

Funktionen areal ovenfor er korrekt, men ikke robust. Fejlmeldingen skyldes, at man<br />

forsøger at tage kvadratroden af et negativt tal, hvilket igen bunder i, at der ikke findes<br />

nogen trekant med sidelængder 2.1, 3.2 og 5.4 (fordi 2.1 + 3.2 < 5.4).<br />

Her er en robust udgave af arealfunktionen:<br />

70


local fun tjek (a,b,c) = 0.0 < a andalso a < b + c<br />

in fun areal (a,b,c)<br />

= if tjek (a,b,c) andalso tjek (b,c,a) andalso tjek (c,a,b)<br />

then let val s = (a + b + c) / 2.0<br />

in Math.sqrt (s * (s - a) * (s - b) * (s - c))<br />

end<br />

else raise Fail "umulig trekant"<br />

end;<br />

val areal = fn : real * real * real -> real<br />

areal (2.0,2.0,2.0);<br />

val it = 1.73205080757 : real<br />

areal (2.1,3.2,5.4);<br />

! Uncaught exception:<br />

! Fail "umulig trekant"<br />

Funktioner, der stilles til r˚adighed for andre brugere, bør gøres robuste. Ofte indeholder<br />

et program ogs˚a hjælpefunktioner, der kun kaldes “internt” af andre af programmets<br />

funktioner. S˚adanne hjælpefunktioner skal ikke nødvendigvis gøres robuste; ofte betyder<br />

programmets indre logik, at de ikke kan blive kaldt med ugyldige parametre 1 .<br />

4.8 Uendelig løkke<br />

Antallet af indbyrdes forskellige permutationer (opstillinger) af n forskellige objekter betegnes<br />

n! (læses: “n fakultet”) og har værdien n! = 1 · 2 · · · n.<br />

Fakultetsfunktionen er defineret for alle naturlige tal 2 gennem<br />

n! =<br />

og denne definition kan direkte nedskrives i ML:<br />

fun fact 0 = 1<br />

| fact n = n * fact (n - 1);<br />

val fact = fn : int -> int<br />

<br />

1 hvis n = 0<br />

n · (n − 1)! hvis n = 1, 2, 3, . . .<br />

Det har tidligere været nævnt, man bør tilstræbe at f˚a funktioner korrekte (i den forstand,<br />

at funktionssvarene er de rigtige) og robuste (s˚a der altid fremkommer et forst˚aeligt svar).<br />

Vores fakultetsfunktion er oplagt korrekt (idet den afspejler den matematiske definition),<br />

men den er ikke robust:<br />

Skridtene i beregningen fact 3 ❀ 6 kan sammenfattes s˚aledes:<br />

fact 3 ❀ 3 * fact 2 ❀ 3 * (2 * fact 1)<br />

❀ 3 * (2 * (1 * fact 0)) ❀ 3 * (2 * (1 * 1)) ❀ 3 * (2 * 1)<br />

❀ 3 * 2 ❀ 6.<br />

1Det følger af et resultat inden for teoretisk datalogi, at man ikke altid kan afgøre, om en funktion vil<br />

kunne blive kaldt med ugyldige parametre.<br />

2Overalt i disse <strong>noter</strong> betegner de naturlige tal IN mængden af ikke-negative heltal.<br />

71


Evaluering af fact ~1 vil føre til:<br />

fact ~1 ❀ ~1 * fact ~2 ❀ ~1 * (~2 * fact ~3)<br />

❀ ~1 * (~2 * (~3 * fact ~4)) ❀ ...<br />

Beregningen vil fortsætte i det uendelige og aldrig returnere et resultat. En s˚adan s˚akaldt<br />

uendelig løkke er en af mulighederne, n˚ar en beregning er sat i gang. Forst˚ar man derfor<br />

“værdi” som alt, hvad beregning kan føre til, er “uendelig løkke” ogs˚a en værdi. Ligesom<br />

undtagelser er den en uegentlig værdi; den betegnes med symbolet ⊥ (læses: bund<br />

(eng.:bottom)), alts˚a: fact ~1 ❀ ⊥.<br />

Beregningsresultater kan ordnes indbyrdes efter, hvor lidt eller hvor meget definerede de<br />

er. I den sammenhæng er ⊥ den mindst definerede af alle værdier; den ligger “i bunden” af<br />

den nævnte ordningsrelation — deraf navnet.<br />

Pladsmangel Prøver man det nævnte eksempel af i praksis, vil man opdage, at udredningen<br />

ovenfor er forkert. Formentlig vil man finde noget i retning af<br />

fact ~1;<br />

! Uncaught exception:<br />

! Out of memory<br />

Afvigelsen skyldes, at vi har ræsonneret, som om datamaten havde ubegrænset meget<br />

lager til r˚adighed, hvilket aldrig vil være tilfældet i praksis. At holde styr p˚a evalueringen af<br />

fact ~1 lægger beslag p˚a stadig mere af maskinens arbejdslager, og før eller siden slipper<br />

det op.<br />

Ogs˚a i den anden ende er der forskel p˚a teori og praksis: idet der (som nævnt i afsnit 3.6.2)<br />

er en snæver grænse for, hvor stor en værdi af type int kan blive, kan kun op til 12! beregnes<br />

p˚a den angivne m˚ade; fact 13 vil give overløb.<br />

Forskellige maskiner har forskellig kapacitet, og at tage en maskines kapacitet med i<br />

betragtning i denne form for overvejelser er vanskeligt. Vores forklaringer af sproget ML g˚ar<br />

derfor ud fra, lageret er s˚a stort, som der bliver brug for, og at hele tal kan blive vilk˚arligt<br />

store. Vi vil fastholde<br />

fact n ❀<br />

<br />

n! for alle naturlige tal n<br />

⊥ ellers<br />

Udefineret. Der kan være grund til at understrege den særlige natur af beregningsresultatet<br />

⊥: En p˚astand af formen exp ❀ ⊥ for et eller andet udtryk exp m˚a altid bygge p˚a<br />

et ræsonnement uden for beregningerne selv. Man kan naturligvis godt starte en evaluering<br />

af exp, men det er ikke i almindelighed muligt at sætte en øvre grænse for, hvor lang tid en<br />

beregning kan tage. Uanset, hvor længe man har ventet forgæves p˚a et beregningsresultat,<br />

er det derfor umuligt at vide, om der virkelig aldrig vil komme et, eller om beregningen ville<br />

være blevet færdig sekundet efter, at ens t˚almodighed var brugt op.<br />

Inden man sætter en beregning i gang, er det nyttigt at gøre sig klart, hvordan den kan<br />

standses, hvis man ved en fejltagelse skulle være kommet til at starte en uendelig løkke. Ved<br />

vores terminaler benyttes den udbredte konvention, at man kan sende tegnet ETX (End of<br />

TeXt; tegnet kan dannes som “Ctrl”+c) som nødstop. I implementeringen af Moscow ML<br />

p˚a Macintosh bruges punktet “Interrupt” i Command-menuen. Muligheden for at afbryde<br />

72


kan man ogs˚a have glæde af ved meget langvarige beregninger (fra et praktisk synspunkt<br />

er der ingen forskel p˚a en uendelig løkke og en beregning, der tager meget lang tid — for<br />

eksempel 100 ˚ar).<br />

N˚ar beregningen afbrydes, kvitterer ML med svarlinien<br />

> Interrupted.<br />

Her er et eksempel:<br />

fun bund n = bund n;<br />

val (’a, ’b) bund = fn : ’a -> ’b<br />

bund 2;<br />

Efter at have sluttet af med semikolon og linieskift-tegn venter man i meget lang tid,<br />

uden at der sker noget som helst, og n˚ar ens t˚almodighed er brugt op, tastes c med Ctrl<br />

nedtrykket.<br />

Interrupted.<br />

4.9 Sammenfatning<br />

Par, tripler, og s˚a videre skrives med runde parenteser og kommaer og kan indg˚a som andre<br />

værdier: De kan navngives, de kan være funktionsparametre, og de kan være funktionsresultater.<br />

Deres type er et produkt (vist med * (asterisk)) af komponenternes type. Komponenter<br />

kan udvælges med projektionsfunktionerne #1, #2, . . .<br />

Typen unit indeholder netop én værdi, som <strong>noter</strong>es ().<br />

Mønsteret er en joker, der passer p˚a alt.<br />

Med let . . . in . . . end kan man indføre en erklæring lokalt i et udtryk, og med<br />

local . . . in . . . end kan man indføre en erklæring lokalt i en erklæring.<br />

Fejlmeldinger gives med raise Fail tekst.<br />

Funktioner, man stiller til r˚adighed for andre, bør ud over at være korrekte ogs˚a være<br />

robuste, det vil sige give forst˚aelige fejlmeldinger for inddata uden for definitionsomr˚adet.<br />

En beregning kan afbrydes med “Ctrl”+c (p˚a Macintosh med punktet “Interrupt” i<br />

Command-menuen).<br />

4.10 Opgaver<br />

Opgave 4.1 Skriv en funktion, der kontrollerer, om dets argument er et korrekt klokkeslæt<br />

p˚a formen (timetal ,minuttal ,sekundtal ).<br />

Opgave 4.2 Skriv en robust ML-definition af n!.<br />

Opgave 4.3 Hvorfor kan man ikke ved passende valg af en værdi for (−1)! udvide ligningen<br />

n! = n · (n − 1)! til at gælde alle hele tal n ≥ 0 (det vil sige alle naturlige tal)?<br />

73


Opgave 4.4 Skriv en funktion, der ud fra et naturligt tal som argument danner produktet<br />

af tallets cifre (n˚ar det fremstilles som talord uden foranstillede nuller).<br />

Opgave 4.5 Skriv en funktion, der for et vilk˚arligt helt tal ≥ 2 finder det mindste hele<br />

tal større end 1, der g˚ar op i tallet (med andre ord tallets mindste primfaktor).<br />

Opgave 4.6 Indtast denne definition<br />

fun halve 0 = 0<br />

| halve n = if n mod 2 = 0 then 1 + halve (n - 2)<br />

else halve (n - 2);<br />

og prøv at kalde funktionen dels med lige tal, dels med ulige tal som argument.<br />

Hvad mon programmøren har ment?<br />

74


Kapitel 5<br />

Konstruktion af funktioner<br />

5.1 Problemløsning<br />

Opgaver, der skal løses med et SML-program, g˚ar ud p˚a at konstruere en funktion, der fører<br />

fra visse inddata (af en bestemt SML-type) til visse uddata (af den samme eller en anden<br />

SML-type). Undertiden er der en standardoperator eller standardfunktion, som direkte løser<br />

opgaven, men i langt de fleste tilfælde er det nødvendigt at g˚a indirekte frem og bruge<br />

en kombination af indbyggede funktioner, eventuelt suppleret med funktioner, man selv<br />

definerer undervejs.<br />

5.1.1 Et eksempel: Hexadecimale tegnkoder<br />

Vi varmer op med et lille eksempel, der drejer sig om manipulation med nogle tegnkoder.<br />

En grundig gennemgang af tegn og tekster kommer i kapitel 7. Vi f˚ar her blot brug for,<br />

at tegnkonstanter <strong>noter</strong>es i SML som disse eksempler viser:<br />

bogstavet “N” #"N"<br />

cifferet “5” #"5"<br />

et lighedstegn #-"<br />

og at biblioteksfunktionerne chr : int -> char og ord : char -> int formidler den omsætning<br />

mellem tegn og de tilhørende koder, som fremg˚ar af tabel 1.2.<br />

Tænkte man sig tabel 1.2 udvidet til at dække hele talomr˚adet fra 0 til 255 (standarden<br />

mangler koderne 0–31 og 127–159), ville det blive en tabel med 16 rækker og 16 søjler.<br />

Størrelser, der har 10 mulige værdier, kan betegnes med et af de sædvanlige decimale<br />

cifre fra 0 til 9. N˚ar der er 16 mulige værdier, bruger man undertiden et s˚akaldt hexadecimalt<br />

ciffer, det vil sige en værdi blandt<br />

0 1 2 3 4 5 6 7 8 9 A B C D E F<br />

To hexadecimale cifre kan derfor bruges til angivelse af henholdsvis en række og en søjle<br />

i den udvidede version af tabel 1.2.<br />

Opgave Konstruer en funktion iso8 : char * char -> char, som ud fra et par af hexadecimale<br />

cifre, der betegner en række og en søjle i ISO-tabellen, angiver tegnet p˚a den<br />

p˚agældende plads. Der skal for eksempel gælde<br />

iso8 (#"3",#"D") = #-"<br />

75


iso8 (#"E",#"6") = #"æ"<br />

Løsning 1 Ingen biblioteksfunktion løser direkte opgaven, men den kan klares med en<br />

sindrig kombination af standardfunktioner. I stedet for at kode hexadecimale cifre ind i<br />

løsningen som “magiske konstanter” g˚ar vi ud fra 1 , at cifrene fra 0 til 9 har koder, der følger<br />

lige efter hinanden, og at bogstaverne fra A til Z har koder, der følger lige efter hinanden:<br />

fun iso8 (cr,cs)<br />

= chr ((if ord cr - ord #"0≪ 10 then ord cr - ord #"0"<br />

else ord cr - ord #"A"+ 10)<br />

* 16 +<br />

(if ord cs - ord #"0≪ 10 then ord cs - ord #"0"<br />

else ord cs - ord #"A"+ 10))<br />

Selv om dette program løser opgaven, kan man rette en del indvendinger imod det:<br />

• det giver anledning til en del gentagne (og derfor overflødige) beregninger<br />

• i de mange linjer, som ligner hinanden, men alligevel ikke er helt ens, kan man let<br />

komme til at skrive forkert<br />

• hvis man laver fejl, kan den skjule sig i den lange formel og være svær at finde<br />

• funktionens virkem˚ade er uoverskuelig<br />

Det sidste punkt kunne afhjælpes med kommentarer eller andre ledsagende forklaringer,<br />

men det er bedre at dele funktionen op i flere mindre dele. Den overordnede gang i funktionen<br />

er, som figuren viser, at de to indkomne tegn første omsættes til tal, dernæst sættes de to<br />

tal sammen til et, og ud fra det findes det resulterende funktionsværditegn.<br />

char ✲ int<br />

char ✲ int<br />

❍ ❍ ❍❥<br />

✟ ✟ ✟✯ int ✲ char<br />

Løsning 2 Det er bedre at uddelegere de indg˚aende omformninger. Lad for eksempel<br />

fromChar : char -> int omsætte et hexadecimalt ciffer til dets værdi, og lad<br />

hex2 : int * int -> int beregne værdien af et tocifret hexadecimalt talord:<br />

fun fromChar c<br />

= let val n = ord c<br />

val d = n - ord #"0"<br />

in if d < 10 then d else n - ord #"A"+ 10 end<br />

fun hex2 (h1,h0) = h1 * 16 + h0<br />

fun iso8 (cr,cs) = chr (hex2 (fromChar cr,fromChar cs))<br />

I denne version af programmet har vi sørget for ikke unødigt at gentage beregninger. Desuden<br />

er det lettere at f˚a programmet korrekt, n˚ar hver af de indg˚aende dele er en selvstændig<br />

funktion, der kan konstrueres og afprøves isoleret.<br />

1 hvad der vil være opfyldt for mange andre koder end netop ISO<br />

76


5.2 Rekursion<br />

Under nedbrydning af et problem i delproblemer kan man komme ud for, at et eller flere af<br />

delproblemerne er af samme art som det oprindelige problem.<br />

Helt magen til det oprindelige problem m˚a det nye problem selvfølgelig ikke være, for s˚a<br />

er man inde i en “ond cirkel”, der ikke kommer løsningen nærmere, men meget ofte er det<br />

nye problem mindre eller p˚a anden m˚ade simplere end det oprindelige.<br />

I <strong>funktionsprogrammering</strong>, hvor et “problem” udtrykkes som en funktion (fra inddata til<br />

uddata), vil denne situation vise sig ved, at en funktions definition indeholder kald af denne<br />

selvsamme funktion.<br />

I en definitionsligning<br />

begreb = redegørelse<br />

vil det jo normalt være s˚adan, at der p˚a venstre side af lighedstegnet st˚ar noget ukendt, der<br />

nu skal lægges fast, mens den definerende højreside indeholder lutter kendte størrelser.<br />

En definition, der i sin redegørelse benytter det, man er ved at definere, siges at være<br />

rekursiv (af lat. re- = tilbage, igen og currere = løbe). Selv om det kan være uvant og p˚a<br />

nogen nærmest virke mystisk eller selvmodsigende at bruge en rekursiv beskrivelse, behøver<br />

den ikke at være det ringeste suspekt. Det kræves blot, at forklaringen i en eller anden<br />

forstand er enklere end det, der skal forklares.<br />

Har man forst˚aet, hvordan funktionskald foreg˚ar i SML (den igangværende beregning<br />

suspenderes, en beregning af funktionskaldets værdi skydes ind, hvorefter den suspenderede<br />

beregning genoptages med værdien p˚a funktionskaldets plads), s˚a ved man ogs˚a, hvad<br />

der sker ved et rekursivt funktionskald. Der er nemlig slet ingen forskel! Det foreg˚ar p˚a<br />

fuldstændig samme m˚ade.<br />

Lad os betragte et eksempel.<br />

5.2.1 Fakultetsfunktionen<br />

Antallet af indbyrdes forskellige permutationer (opstillinger) af n forskellige objekter betegnes<br />

n! (læses: “n fakultet”) og har værdien n! = 1 · 2 · · · n.<br />

Fakultetsfunktionen er fastlagt for alle naturlige tal gennem<br />

n! =<br />

<br />

1 hvis n = 0<br />

n · (n − 1)! hvis n = 1, 2, 3, . . .<br />

og disse egenskaber kan direkte nedskrives i ML:<br />

fun fakultet n = if n = 0 then 1 else n * fakultet (n - 1);<br />

val fakultet = fn : int -> int<br />

(5.1)<br />

(5.2)<br />

Definitionerne (5.1) og (5.2) ses at være rekursive.<br />

Vi indser, at fakultetsfunktionen alligvel er veldefineret for alle naturlige tal, idet 0! har<br />

en fastlagt værdi, hvilket ogs˚a giver 1! en værdi, som igen fastlægger værdien for 2!, og s˚a<br />

videre. Ved matematisk induktion indser man, at n! har en entydig værdi for alle naturlige<br />

tal n.<br />

77


5.2.2 Virkning af funktionskald<br />

Den her forklarede fremgangsm˚ade kan bruges, hvad enten der er tale om en rekursiv funktion<br />

eller ej, men nogle vil finde det illustrativt at se, hvordan kald af fakultetsfunktionen afvikles.<br />

Ved at bruge notationen udtryk ❀ værdi for at betegne evaluering af et udtryk til en<br />

værdi og notationen udtryk ❀ udtryk ′ for et enkelt skridt undervejs i en evaluering kan vi<br />

forklare, hvordan evaluering af fakultet 2 finder sted:<br />

fakultet 2 ❀ [p˚a grund af definitionen (5.2)]<br />

if 2 = 0 then 1 else 2 * fakultet (2 - 1) ❀ [idet 2 = 0 ❀ false]<br />

if false then 1 else 2 * fakultet (2 - 1) ❀<br />

[idet if false then d else e ❀ e]<br />

2 * fakultet (2 - 1) [beregningen suspenderes]<br />

fakultet (2 - 1) ❀ [subtraktion]<br />

fakultet 1 ❀ [p˚a grund af definitionen (5.2)]<br />

if 1 = 0 then 1 else 1 * fakultet (1 - 1) ❀ [idet 1 = 0 ❀ false]<br />

if false then 1 else 1 * fakultet (1 - 1) ❀<br />

[idet if false then d else e ❀ e]<br />

1 * fakultet (1 - 1) [beregningen suspenderes]<br />

fakultet (1 - 1) ❀ [subtraktion]<br />

fakultet 0 ❀ [p˚a grund af definitionen (5.2)]<br />

if 0 = 0 then 1 else 0 * fakultet (0 - 1) ❀ [idet 0 = 0 ❀ true]<br />

if true then 1 else 0 * fakultet (0 - 1) ❀<br />

[idet if true then d else e ❀ d]<br />

1<br />

❀ [den suspenderede beregning genoptages]<br />

1 * 1 ❀ [multiplikation]<br />

1<br />

❀ [den suspenderede beregning genoptages]<br />

2 * 1 ❀ [multiplikation]<br />

2<br />

5.2.3 Rekursiv problemløsning<br />

I <strong>funktionsprogrammering</strong> benytter man ofte rekursive funktioner. De dukker op i de situationer,<br />

hvor et problem ikke kan løses “i et snuptag” ved passende kombination af biblioteksfunktioner<br />

og allerede dannede funktioner. Den rekursive funktionskrop viser da, hvordan<br />

man uden at løse problemet til bunds dog kan komme løsningen “et lille skridt nærmere”.<br />

I tilfældet med fakultetsfunktionen er ræsonnementet: Vi skal gange tallene fra 1 til n<br />

sammen, men der er ikke nogen standardfunktion, som danner dette produkt p˚a en gang.<br />

Hvis vi imidlertid allerede kendte værdien af fakultetsfunktionen for argument n−1, s˚a ville<br />

man kunne finde den ønskede værdi ved at gange dette tal med n.<br />

Mere almindeligt har den rekursive problemløsningstankegang denne form: I simple tilfælde<br />

kan løsningen direkte angives, men for mere komplicerede inddata beskriver vi, hvordan<br />

man ville kunne bygge en løsning op under forudsætning af, at løsning(er) p˚a et eller flere<br />

simplere tilfælde af problemet allerede var kendt.<br />

78


5.2.4 Ræsonneren om funktionskald<br />

Rammerne viser, hvordan systemet ved at suspendere og genoptage beregninger udfører de<br />

rekursive kald, men sin forst˚aelse af en funktion bør man ikke bygge p˚a dette dynamiske billede.<br />

Antallet af “rammer indlejret i hinanden” vil afhænge af de aktuelle argumentværdier,<br />

og ræsonnementer om et program bør derfor bygge p˚a programteksten.<br />

Argumentation for egenskaber ved en rekursiv funktion kan have form af induktionsræsonnementer:<br />

Under beviser for, funktionen har en vis egenskab, antages alle indre kald<br />

allerede at have egenskaben.<br />

5.3 Indsættelse<br />

S˚a snart der bliver brug for mere end et par linjers definitioner, er det ubekvemt at indtaste<br />

dem direkte til ML-systemet. Hertil kommer, at alle definitioner tabes, n˚ar systemet forlades,<br />

og alts˚a m˚a indtastes p˚any, hvis de senere skal bruges igen.<br />

Sædvanligvis arbejder man derfor p˚a den m˚ade, at definitioner udarbejdes i en eller flere<br />

filer ved hjælp af et s˚akaldt tekstbehandlingsprogram eller redigeringsprogram (eng.:editor).<br />

P˚a øvelserne og i det tilhørende undervisningsmateriale beskrives tekstredigeringsværktøjerne<br />

nedit, vi, vim og emacs.<br />

Nogle tekstbehandlingsprogrammer er beregnet til at styre opstilling af tekst i kapitler<br />

og afsnit, med overskrifter i flere niveauer, skriftvarianter som fed og kursiv, hævede og<br />

sænkede tegn og mange andre raffinementer. Den form for tekstbehandlingsprogrammer (for<br />

eksempel Microsoft Word eller L ATEX) er ikke relevante ved skrivning af programmer. I den<br />

situation skal man bare bruge de simple værktøjer, som gratis følger med operativsystemet.<br />

P˚a Macintosh er det SimpleText og under Microsofts operativsystemer Notepad.<br />

Under operativsystemet Unix kan filnavne blandt andet indeholde bogstaver, cifre, punktum<br />

og bindestreg; filer med ML-erklæringer bør gives navne, hvis fire sidste tegn er .sml.<br />

N˚ar man er tilfreds med sine definitioner, kan de indsættes p˚a topniveau med standardfunktionen<br />

“use”. Antag, vi med tekstbehandlingsprogrammet har konstrueret filen<br />

“fak.sml”:<br />

fun fakultet n<br />

= if n = 0 then 1<br />

else n * fakultet (n - 1);<br />

I en ML-dialog kan man da skrive:<br />

use "fak.sml";<br />

[opening file "fak.sml"]<br />

val fakultet = fn : int -> int<br />

[closing file "fak.sml"]<br />

val it = () : unit<br />

← Indholdet af filen<br />

“fak.sml”<br />

og virkningen vil være den samme, som hvis man direkte havde tastet linjerne fra filen ind.<br />

Læg mærke til, at filnavnet skal omgives af anførelsestegn, hvorved det kan behandles af<br />

ML som data af type “tekst” (eng.:string). (I et senere kapitel vil vi komme nærmere ind<br />

p˚a tekster.) Dette forklarer typen af “use”:<br />

79


use;<br />

val it = fn : string -> unit<br />

Funktionen “use” har (ligesom tilfældet var for “quit” og “load”) kun interesse p˚a grund<br />

af sin bivirkning; funktionskaldet selv evalueres bare til det tomme sæt (af enhedstypen).<br />

5.4 Programnedskrivning<br />

Selv om et program er en instruks til en maskine om, hvordan beregninger skal styres, læses<br />

programmer i vidt omfang ogs˚a af mennesker: som regel er programmer ikke korrekte, første<br />

gang de skrives ned, og at finde fejl i et program kan kræve omhyggelig nærlæsning; ofte<br />

gennemg˚ar programmer ogs˚a mange tilpasninger, hvor de afstemmes i forhold til ændrede<br />

eller udvidede specifikationer.<br />

N˚ar programmer skrives ned, er det derfor vigtigt, at man ikke kun overholder de formelle<br />

syntaksregler (som er en forudsætning for, at systemet vil acceptere programmet), men ogs˚a<br />

benytter en læsevenlig programmeringsstil. At f˚a et program i h˚anden, man selv har skrevet<br />

for nogle m˚aneder siden, og opdage, hvor vanskeligt det er at komme tilbage i de tankebaner,<br />

som syntes s˚a klare, dengang programmet blev skrevet, kan være en frustrerende oplevelse.<br />

I dette afsnit gives nogle gode r˚ad med hensyn til navnevalg, opstilling og kommentarer.<br />

5.4.1 Navnevalg<br />

Som afsnit 2.4.1 viste, er der stor frihed med hensyn til, hvorledes funktioner, konstanter og<br />

parametre benævnes. Prøv ikke at finde p˚a “morsomme” navne (som “hyl” eller “idiot”),<br />

men vælg betydende navne, der alligevel ikke er for lange; prøv eventuelt at følge en eller<br />

anden systematik. I et program til skatteberegning kan man f˚a brug for “bundbeskatningsgrundlag”,<br />

“mellembeskatningsgrundlag”, “topbeskatningsgrundlag” og “indkomstbeskatningsgrundlag”.<br />

I sig selv vil disse navne normalt være for lange, men det er heller ikke smart<br />

at kalde dem “a”, “b”, “c” og “d” eller “bg”, “tg”, “mg” og “ig”. Navne som “bundgrundl”,<br />

“top grundl”, “melmgrundl” og “indkgrundl” kunne m˚aske bruges.<br />

5.4.2 Opstilling<br />

For ML tjener mellemrum (sekvenser af blanktegn, tabuleringstegn og ny-linje-tegn) blot<br />

til at adskille grundsymboler. For afviklingssystemets skyld kunne fakultetsfunktionen godt<br />

skrives<br />

fun fakultet<br />

n= if n=0then<br />

1else<br />

n * fakultet(n-1);<br />

men man st˚ar sig ved at stille programmer systematisk op.<br />

80


5.4.3 Kommentarer<br />

Som det blev nævnt i afsnit 3.10.1, kan vilk˚arlig tekst indsættes som kommentar i et program.<br />

Denne mulighed for at gøre en programtekst klarere ved hjælp af kommentarer bør man altid<br />

benytte sig af.<br />

Kommentarer skal vælges, s˚a de støtter forst˚aelsen af programmet uden at gentage, hvad<br />

man direkte kan læse sig til af programkonstruktionerne. Her er et eksempel p˚a, hvordan<br />

kommentarer ikke bør udformes:<br />

NB D˚ARLIGE KOMMENTARER NB<br />

NB NB<br />

NB fun fakultet n = NB<br />

NB if n = 0 then 1 NB<br />

NB (* hvis n er 0, da 1 *) NB<br />

NB else n * fakultet (n - 1); NB<br />

NB (* og ellers n gange fakultet taget af n-1 *) NB<br />

NB NB<br />

NB D˚ARLIGE KOMMENTARER NB<br />

Som et mindstem˚al bør hver funktionsdefinition ledsages af en kort kommentar, der<br />

forklarer, hvad det er, den beregner, men ofte kan kommentarer derudover ogs˚a være nyttige.<br />

Kommentarer bør formidle en overordnet indsigt, der ikke direkte fremg˚ar af programmet,<br />

men støtter forst˚aelsen af det. Her er en acceptabel udgave af fakultetsfunktionen:<br />

(* Fakultetsfunktionen n! er<br />

produktet 1*2*...*n *)<br />

fun fakultet n =<br />

if n = 0 then 1<br />

else n * fakultet (n - 1);<br />

Her i <strong>noter</strong>ne er mange eksempler ikke forsynet med de kommentarer, de i henhold til<br />

r˚adene i dette afsnit burde have, idet den omgivende tekst fungerer som kommentar!<br />

5.4.4 Systematik<br />

Vær under nedskrivning af et program opmærksom p˚a, at det skal kunne læses og forst˚as<br />

(af et menneske og ikke kun af en maskine). Brug de enklest mulige konstruktioner, som<br />

tjener det givne form˚al.<br />

Hvis et delproblem kan løses p˚a flere forskellige m˚ader og dukker op flere gange, s˚a vælg<br />

konsekvent samme løsning hver gang. Hvis løsningen ikke er oplagt eller fylder mere end<br />

omkring en linje, kan det ofte betale sig at trække et delproblem ud som en ny selvstændig<br />

funktion. Vi vil have mere at sige om programkonstruktion i et senere afsnit.<br />

5.5 Afprøvning<br />

N˚ar man arbejder med EDB, vil man gøre den forbavsende iagttagelse, at man jævnligt<br />

beg˚ar fejl. Fejl i programmer kan vise sig p˚a flere m˚ader:<br />

81


En s˚akaldt syntaksfejl ytrer sig ved, at ML-systemet giver en fejlmelding, fordi teksten<br />

ikke overholder reglerne. Strengt taget er det forkert at tale om en “fejl i programmet”, for<br />

i det tilfælde er teksten jo slet ikke et program! og det er sjældent, der kun er én mulig<br />

rettelse, som vil gøre den til et program.<br />

Husk, at ML-systemet vil melde fejl hvor de findes, og at det ikke behøver at være der,<br />

hvor de skal rettes.<br />

For at syntaksfejl ikke skal blive for svære at finde, er det vigtigt at opbygge et program<br />

af sm˚a overskuelige dele, der kan fortolkes af ML hver for sig.<br />

Hvis et program overholder syntaksreglerne, men alligevel ikke virker som forventet, taler<br />

man om semantiske fejl. En semantisk fejl kan ytre sig ved, at uddata ikke er de rigtige, eller<br />

(og det er den allervanskeligste type fejl at finde) ved, at programmet slet ingen resultater<br />

giver og tilsyneladende er g˚aet i uendelig løkke. I det tilfælde er der ingen anden udvej end<br />

at g˚a systematisk frem og prøve at isolere fejlen, idet man hele tiden m˚a huske, systemet jo<br />

ikke kan gætte, hvad man har i tankerne, men blot slavisk udfører de nedskrevne ordrer.<br />

5.5.1 Bevis eller afprøvning?<br />

Mens funktionsdefinitioner konstrueres, er det vigtigt, man overbeviser sig om, at de er<br />

korrekte, og i princippet kunne man sikre sig mod programfejl ved at føre et formelt bevis<br />

for, at programmet overholdt sin specifikation. I praksis er den vej ikke fremkommelig:<br />

Formelle beviser er b˚ade for vanskelige og for omfattende, og ligesom ved programmering er<br />

der tale om symbolmanipulation, som man kunne komme til at beg˚a fejl i!<br />

Ved siden af at ræsonnere om sit program kommer man derfor ikke uden om ogs˚a at<br />

afprøve det. Et ML-program best˚ar af en række erklæringer, og afprøvning af det g˚ar ud p˚a<br />

at anvende de definerede funktioner p˚a udvalgte argumenter og sammenligne resultaterne<br />

med det forventede.<br />

Det kan stærkt tilr˚ades, at man tilegner sig en bevidst og hensigtsmæssig afprøvningsdisciplin.<br />

I næste del af dette kursus vil spørgsm˚alet om programafprøvning blive taget mere<br />

systematisk op; her er foreløbigt opsamlet en række gode r˚ad om det, som sidenhen vil<br />

blive kaldt intern afprøvning. Nogle af r˚adene er, at man bør prøve med eksempler med<br />

kendt forventet udfald, og at b˚ade testdata og det forventede resultat bør indarbejdes i selve<br />

programteksten.<br />

5.5.2 Afprøvningsdata i programteksten<br />

Anbefalingen g˚ar ud p˚a, at man i programfilen umiddelbart efter hver definition af en funktion<br />

f indsætter passende kald f a1 = b1; f a2 = b2; . . .<br />

I den førnævnte fil “fak.sml” med fakultetsfunktionens definition kunne der for eksempel<br />

st˚a:<br />

fun fakultet n<br />

= if n = 0 then 0<br />

else n * fakultet (n - 1);<br />

fakultet 0 = 1;<br />

fakultet 1 = 1;<br />

fakultet 3 = 6;<br />

82


N˚ar filen ved et kald af “use” læses ind, giver selve definitionen anledning til oplysning<br />

om funktionens type, og hver af afprøvningslinjerne resulterer i en sandhedsværdi. I det<br />

aktuelle tilfælde f˚as<br />

use "fak.sml";<br />

[opening file "fak.sml"]<br />

val fakultet = fn : int -> int<br />

val it = false : bool<br />

val it = false : bool<br />

val it = false : bool<br />

[closing file "fak.sml"]<br />

val it = () : unit<br />

som klart afslører, at der er en fejl. Nøjere undersøgelse af funktionsdefinitionen viser, at vi<br />

er kommet til at skrive forkert i anden linje, hvor der skulle have st˚aet<br />

= if n = 0 then 1<br />

5.5.3 Udformning af testdata<br />

Det betaler sig at ofre lidt opmærksomhed p˚a at lægge afprøvning fornuftigt til rette. Her<br />

er nogle tommelfingerregler, som efterfølgende kommenteres:<br />

1. Brug simple testeksempler<br />

2. Sørg for at prøve alle dele af programmet<br />

3. Afprøv i en s˚adan rækkefølge, at det er let at henføre fejl til bestemte programdele<br />

4. Konstruer testdata sideløbende med, at funktionen konstrueres<br />

5. Udnyt al den information, systemets fejlmeldinger giver<br />

ad 1: Det kan være fristende at prøve en funktion af p˚a noget “rigtigt svært” og for<br />

eksempel skrive<br />

fakultet 10 = 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10;<br />

snarere end<br />

fakultet 3 = 6<br />

men fra et programmeringsmæssigt synspunkt prøver første eksempel ikke større dele af<br />

programmet end sidste, og hvis der optræder en fejl, er den meget nemmere at lokalisere,<br />

n˚ar man kan følge beregningerne i hovedet.<br />

ad 2: Først og fremmest skal man sørge for, at hvert valgudtryk if b then d else e b˚ade<br />

bliver prøvet med falsk og med sand betingelse b. Kan dette ikke lade sig gøre, indeholder<br />

programmet jo overflødige dele!<br />

Er der rekursive kald (hvad enten der er tale om direkte kald af funktionen i dens egen<br />

krop eller om en kæde af funktionskald, som gensidigt kalder hinanden og til sidst kalder<br />

funktionen selv igen), bør der b˚ade være testdata, for hvilke det rekursive kald ikke bliver<br />

83


udført, og testdata, for hvilke det bliver (eventuelt b˚ade nogle, hvor det bliver udført netop<br />

én gang, og andre, hvor der kaldes mere end en gang).<br />

De tre test, vi valgte for fakultetsfunktionen:<br />

fakultet 0 = 1;<br />

fakultet 1 = 1;<br />

fakultet 3 = 6;<br />

vil netop b˚ade prøve med falsk og med sand værdi af n = 0 og sørge for henholdsvis ingen,<br />

netop et og flere end et rekursivt kald.<br />

For disjunktion og konjunktion gælder noget tilsvarende. Hvis ens funktion indeholder<br />

et deludtryk af formen u1 orelse u2 orelse u3, bør fire forskellige tilfælde dækkes med<br />

testdata: u1 sand; u1 falsk, men u2 sand; u1 og u2 falske, u3 sand; alle tre udtryk u1, u2 og u3<br />

falske. Hvis ikke alle fire tilfælde kan forekomme, er et eller flere af udtrykkene overflødige!<br />

For konjunktioner (med andalso) forholder det sig p˚a samme m˚ade, blot med omvendte<br />

sandhedsværdier.<br />

ad 3: Hvis filen fak.sml havde indeholdt<br />

fun fakultet n<br />

= if n = 0 then 0<br />

else n * fakultet (n - 1);<br />

fakultet 1 = 1;<br />

fakultet 0 = 1;<br />

fakultet 3 = 6;<br />

ville et forsøg “use "fak.sml";” p˚a at indsætte den stadig fejle for alle tre testlinjer, men<br />

da n = 0 med denne testsekvens først er falsk og siden (for det indre rekursive kald) sand,<br />

er det umuligt at lokalisere fejlen til en bestemt del af funktionen. Det er meget bedre at<br />

starte med den testlinje “fakultet 0 = 1;”, som kun berører programlinje 2.<br />

ad 4: Programkonstruktion er s˚a fængslende en aktivitet, at det er let at blive grebet af<br />

den og udskyde afprøvning til behovet opst˚ar, det vil sige indtil der dukker fejl op. M˚aske<br />

ligger fejlen i en funktion, man skrev for længe siden, og det kan øge vanskelighederne med<br />

at finde fejlen.<br />

Det anbefales stærkt, at man skriver afprøvningsdata ned med det samme. Lige n˚ar man<br />

har konstrueret en funktion i ML, er man godt inde i den, og da kan man uden stort besvær<br />

skrive nogle testdata ned til afprøvning af den. Dem skal man lade blive st˚aende i samme<br />

fil som funktionen, s˚a ML-systemet vil udføre testene, n˚ar funktionen hentes ind, og r˚abe<br />

gevalt, hvis noget g˚ar galt.<br />

Skal funktionen senere udbygges eller modificeres, er det en stor fordel at have de gamle<br />

testdata til r˚adighed til afprøvning af den nye udgave af funktionen.<br />

Arbejdsformen med integrering af konstruktion og afprøvning bevirker, at man kun<br />

behøver beskæftige sig indg˚aende med hver funktion én gang (forudsat man f˚ar den udtrykt<br />

korrekt). Tager man i stedet programmering for sig og afprøvning for sig, skal man<br />

beskæftige sig med hver funktion to gange: n˚ar man konstruerer den, og n˚ar man afprøver<br />

den. For at kunne lave en fornuftig afprøvning er det da nødvendigt at genlæse og “genforst˚a”<br />

hver funktion, hvilket er spild af tid.<br />

84


5.6 Programkonstruktion<br />

Fakultetsfunktionen har været det gennemg˚aende eksempel. Efterrationalisering af, hvordan<br />

vi bar os ad med at programmere den, kan tage sig s˚aledes ud:<br />

Fakultetsfunktionen er egentlig defineret ved<br />

n! = 1 · 2 · · · n (5.3)<br />

men s˚adan en definition “med prikker i” kan ikke forst˚as af en maskine 2 . For sm˚a værdier af<br />

n finder man af (5.3) 3! = 6, 2! = 2 og 1! = 1. Hvilken værdi 0! skal have, er mindre oplagt.<br />

Ved at gruppere de første n − 1 faktorer i (5.3) sammen er det imidlertid simpelt at udlede<br />

gældende for hele tal n ≥ 3, s˚a man samlet har<br />

n! = (1 · 2 · · · (n − 1)) · n = (n − 1)! · n (5.4)<br />

⎧<br />

⎪⎨<br />

n! =<br />

⎪⎩<br />

den for 0! valgte værdi for n = 0<br />

1 for n = 1<br />

2 for n = 2<br />

(n − 1)! · n for n ≥ 3<br />

Værdien af 0! vælges ud fra et grundlæggende logisk princip:<br />

Læg definitioner og ræsonnementer til rette, s˚a antallet af specialtilfælde<br />

bliver s˚a lille som muligt, og s˚a identiteter og andre<br />

egenskaber f˚ar det størst mulige gyldighedsomr˚ade.<br />

(5.5)<br />

Følges dette princip, f˚ar man mindst at tænke over, og programmer bliver nemmere at<br />

overskue og nemmere at f˚a korrekte.<br />

I det foreliggende tilfælde kan antallet af linjer i ligning (5.5) reduceres fra fire til to<br />

under forudsætning af, at vi vælger 0! = 1; derved genfindes ligning (5.1):<br />

n! =<br />

<br />

1 for n = 0<br />

n · (n − 1)! for n ≥ 1<br />

Fakultetsfunktionens program dannedes nu simpelt hen ved, at egenskaben (5.1) blev<br />

ophøjet til definition.<br />

Med valget 0! = 1 er gyldigheden af ligning (5.4) udvidet til alle hele tal n ≥ 1.<br />

5.6.1 Hanois t˚arn<br />

Lad os illustrere dette delafsnit om rekursiv programkonstruktion ved at løse puslespillet<br />

“Hanois t˚arn” (eng.:Tower of Hanoi). Det best˚ar af otte cirkelskiver, der har forskellig<br />

størrelse og i midten er forsynet med et hul, s˚a de kan sættes ned over spillebrættets tre<br />

pinde.<br />

Ved spillets begyndelse ligger alle otte skiver efter størrelse p˚a den første pind. Opgaven<br />

g˚ar ud p˚a at flytte dem over p˚a pind nummer to under iagttagelse af følgende regler:<br />

2 I hvert tilfælde ikke uden inddragelse af teknikker fra omr˚adet “simuleret intelligens” (eng.:artificial<br />

intelligence), hvilket falder uden for dette kursus.<br />

85


Figur 5.1: Udgangsstillingen i puslespillet “Hanois t˚arn”.<br />

• Et træk best˚ar i at flytte den øverste skive fra en af pindene til en af brættets andre<br />

pinde (kun en skive ad gangen).<br />

• Ingen skive m˚a lægges oven p˚a en mindre skive.<br />

Løs den tilsvarende opgave med to og med tre skiver (men stadig med tre pinde), inden<br />

du læser videre 3 .<br />

Det er rimeligt at søge at udtrykke løsningen for et vist antal skiver ved hjælp af løsninger<br />

for færre skiver, men i modsætning til det der sker, n˚ar puslespillet faktisk løses, er det ikke<br />

den mindste skive, man skal se bort fra. (Se dog opgave 5.4.) Det er væsentlig mere frugtbart<br />

at fokusere p˚a den største skive. Hvis alle skiverne skal flyttes fra pind 1 til pind 2, m˚a der<br />

være et træk, i hvilket den underste skive flyttes fra pind 1. Løsningen f˚ar færrest træk, hvis<br />

den da flyttes over p˚a pind 2. I det træk, som flytter den største skive fra pind 1 til pind<br />

2, m˚a alle de øvrige skiver befinde sig p˚a pind 3. Hele løsningen kan derfor opsplittes i tre<br />

dele: Først flyttes de 7 mindste skiver i et vist antal træk (hvorunder den største skive bliver<br />

liggende) fra pind 1 til pind 3, s˚a den største skive i et enkelt træk fra pind 1 til pind 2,<br />

og til sidst flyttes igen de 7 mindste skiver i et vist antal træk (hvorunder den største skive<br />

bliver liggende), nu fra pind 3 til pind 2.<br />

Ræsonnementet er ikke specielt for et t˚arn af 8 skiver, men kan bruges i en generel rekursiv<br />

løsning. Man kunne vælge n = 2 eller n = 1 som basistilfælde, men i overensstemmelse<br />

med princippet om at søge den størst mulige enkelhed gennem den bredest mulige generalisering<br />

indser vi, at man faktisk kan lade basistilfældet være et tomt t˚arn, n = 0. Bemærk,<br />

at hvis pindene nummereres 1, 2, 3, og i og j er to af pindene, bliver 6 − i − j nummeret p˚a<br />

den tredje pind.<br />

fun hanoi (0, , ) =<br />

| hanoi (n,fra,til)<br />

= let val tredje = 6 - fra - til<br />

in hanoi (n - 1,fra,tredje)<br />

^ Int.toString fra ^ ---> "^ Int.toString til ^ "\n"<br />

^ hanoi (n - 1,tredje,til)<br />

end<br />

3 Den franske matematiker Edouard Lucas, som lancerede spillet i 1883, ledsagede det af følgende skrøne:<br />

Ved tidernes begyndelse opstillede Gud “Brahmas t˚arn” med 64 skiver af det pure guld og tre diamantn˚ale,<br />

og han forordnede, at en gruppe munke skulle flytte dem over p˚a en af de andre n˚ale efter de nævnte regler.<br />

Munkene arbejder uophørligt dag og nat p˚a opgaven, og n˚ar den er løst, vil t˚arnet falde sammen og jorden<br />

g˚a under.<br />

86


Funktionen hanoi (n,i,j) genererer som en tekst de træk, der skal til for at flytte<br />

n skiver fra pind i til pind j. Den bedste m˚ade at f˚a trækkene at se p˚a er at kalde<br />

print (hanoi (n,1,2)) p˚a yderste niveau (print bliver forklaret nærmere i kapitel 7).<br />

Prøv at indtaste og køre programmet.<br />

5.6.2 Egenskaber og definitioner<br />

N˚ar en egenskab ophøjes til definition, g˚ar det enten godt eller grumme galt. Her er et eksempel<br />

p˚a det sidste: Ligning (5.4) (nu for alle hele tal n ≥ 1) kan formuleres som egenskaben<br />

n! =<br />

(n + 1)!<br />

n + 1<br />

(5.6)<br />

gældende for alle naturlige tal n. Man kan sige, at det var denne egenskab, vi sørgede for at<br />

opretholde i definitionen af 0!. Ophøjes (5.6) til definition<br />

fun fakultet’ n = fakultet’ (n + 1) div (n + 1); (5.7)<br />

finder man imidlertid<br />

fakultet’ 3 ❀ fakultet’ 4 div 4<br />

❀ fakultet’ 5 div 5 div 4<br />

❀ fakultet’ 6 div 6 div 5 div 4 ❀ ...<br />

og det er klart, at fakultet’ 3 ❀ ⊥.<br />

Den indledende bemærkning om, at det enten g˚ar “godt” eller “grumme galt”, kan mere<br />

præcist formuleres<br />

Antag, f er en funktion, om hvilken der gælder en egenskab af formen<br />

f x = . . ., og at denne egenskab ophøjes til definition. For hvert aktuelt<br />

argument a vil beregningen af f a da enten finde den korrekte<br />

funktionsværdi eller g˚a i uendelig løkke.<br />

Tager man alts˚a for eksempel en eller anden egenskab n! = . . ., hvor n og ! kan indg˚a<br />

p˚a højre side, og bruger den til definition af fakultet, vil der for hvert tal n enten gælde<br />

fakultet n ❀ n! eller fakultet n ❀ ⊥.<br />

5.7 Kaldtræer<br />

Den sidstnævnte mulighed: at en beregning g˚ar i uendelig løkke, prøver man naturligvis at<br />

undg˚a. Mere almindeligt er man interesseret i, at en beregning ud over at være korrekt ogs˚a<br />

forløber effektivt, det vil sige ikke tager unødigt lang tid og i øvrigt har et lavt ressourceforbrug.<br />

Et af de forhold, som har stor betydning for, hvor lang tid et funktionskald tager, er,<br />

hvilke andre funktionskald det giver anledning til. Dette kan bekvemt vises i et s˚akaldt<br />

kaldtræ, hvor man lader et funktionskald f x svare til et punkt (en s˚akaldt knude i træet)<br />

og for hvert indre funktionskald g y, som f x direkte giver anledning til, tegner en streg<br />

(en s˚akaldt kant i træet) fra knuden for f x ned til en ny underliggende knude for g y.<br />

87


fakultet 4<br />

fakultet 3<br />

fakultet 2<br />

fakultet 1<br />

fakultet 0<br />

fakultet’ 4<br />

fakultet’ 5<br />

fakultet’ 6<br />

Figur 5.2: Kaldtræerne for fakultet 4 og for fakultet’ 4<br />

Figur 5.2 viser kaldtræerne for fakultet 4 og for fakultet’ 4; sidstnævnte træ er<br />

uendeligt — som billede p˚a, at beregningen ikke standser.<br />

For at vise, kaldtræer kan forgrene sig, betragtes et kombinatorisk eksempel: Antallet<br />

af m˚ader, hvorp˚a en mængde med n elementer kan inddeles i netop k delmængder (ingen<br />

af dem tomme) skrives { n k} og kaldes Stirlingtallet af anden art af n og k. Med simple<br />

kombinatoriske ræsonnementer kan man indse følgende identiteter:<br />

{ n n} = 1 { n 0} = 0 for n > 0<br />

{ n k} = k · { n−1<br />

k } + {n−1 k−1 } for 0 < k < n<br />

som man kan bygge en funktionsdefinition p˚a:<br />

fun stirlto (n, k)<br />

= if k = n then 1<br />

else if k = 0 then 0<br />

else k * stirlto (n - 1, k) + stirlto (n - 1, k - 1);<br />

val stirlto = fn : int * int -> int<br />

Uden at komme nærmere ind p˚a, hvilke værdier funktionen har, kan vi se, at dens kaldtræ<br />

for argumentparret (4,2) er som vist i figur 5.3.<br />

{ 4 2}<br />

✘✘<br />

✘✘✘<br />

{ 3 ❳ ❳❳❳❳<br />

2}<br />

{ 3 1}<br />

✟<br />

✟✟<br />

{ 2 ❍<br />

❍❍<br />

2} { 2 ✟<br />

✟✟<br />

1} { 2 ❍<br />

❍❍<br />

1} { 2 0}<br />

<br />

{ 1 1}<br />

❅<br />

{ 1 0}<br />

<br />

{ 1 1}<br />

❅<br />

{ 1 0}<br />

Figur 5.3: Kaldtræet hørende til { 4 2}.<br />

Læg mærke til, at funktionskald som for eksempel { 1 1} forekommer flere gange; for større<br />

værdier af n og k vil der være overordentlig mange gentagelser i kaldtræet.<br />

88<br />

.


At beregne samme funktionskald flere gange er naturligvis spild af tid (resultatet m˚a<br />

nødvendigvis blive det samme hver gang), men med de dele af ML, vi har mødt indtil nu,<br />

er det meget vanskeligt at undg˚a de gentagne kald — det bliver lettere, n˚ar lister og andre<br />

strukturerede værdier st˚ar til r˚adighed.<br />

Spørgsm˚alet om at konstruere funktioner effektivt vender vi tilbage til i kapitel 11.<br />

5.8 Opgaver<br />

Opgave 5.1 Skriv en funktion, som for et helt tal n beregner det største hele tal m, for<br />

hvilket m 2 ≤ n. Der er ikke noget krav om, at funktionen skal være særlig snedig eller<br />

effektiv.<br />

Opgave 5.2 Antallet af delmængder med k elementer af en mængde med n elementer<br />

betegnes ( n k) (læses: n over k) og kaldes ogs˚a antallet af k-kombinationer af n elementer.<br />

Symbolet ( n k) defineres for alle hele tal n og k, og simple kombinatoriske ræsonnementer<br />

(som det ikke er opgaven at gennemføre her) viser identiteterne (5.8) og (5.9):<br />

( n k) =<br />

( n k) =<br />

<br />

0 hvis k < 0 eller k > n<br />

n!<br />

k!(n−k)!<br />

⎧<br />

⎪⎨ 0<br />

1<br />

⎪⎩<br />

hvis 0 ≤ k ≤ n<br />

hvis k < 0 eller k > n<br />

hvis 0 = k ≤ n eller 0 ≤ k = n<br />

) hvis 0 < k < n<br />

( n−1<br />

k<br />

) + (n−1<br />

k−1<br />

P˚a grund af ligning 5.9 kan kombinationer stilles op i Pascals trekant:<br />

( 0 0)<br />

( 1 0) ( 1 1)<br />

( 2 0) ( 2 1) ( 2 2)<br />

( 3 0) ( 3 1) ( 3 2) ( 3 3)<br />

( 4 0) ( 4 1) ( 4 2) ( 4 3) ( 4 4)<br />

. . .<br />

=<br />

1<br />

1 1<br />

1 2 1<br />

1 3 3 1<br />

1 4 6 4 1<br />

. . .<br />

(5.8)<br />

(5.9)<br />

Betragt de to definitioner (5.8) og (5.9) som givet, og skriv funktioner til beregning af<br />

antallet af k-kombinationer af n elementer i henhold til b˚ade den ene og den anden definition.<br />

Afprøv funktionerne og konstater derved, at der er tal n og k, for hvilke (5.9) kan beregne<br />

( n k), mens (5.8) giver overløb. Hvordan kan det forklares?<br />

Tegn kaldtræet for ( 4 2) beregnet med den funktion, som følger (5.9).<br />

Opgave 5.3 Man kan bevise, to tals største fælles divisor altid kan udtrykkes som en<br />

linearkombination med hele koefficienter af de to tal, og opgaven er at skrive en funktion til<br />

beregning af disse koefficienter. Mere konkret: Funktionen gcd fra afsnit 3.5.2 skal udbygges,<br />

s˚a den for to naturlige tal a og b, der ikke begge er 0, finder to hele tal s og t, s˚adan at<br />

s · a + t · b = gcd (a,b) (5.10)<br />

89


Her er nogle vink til opgaven:<br />

Operatorerne til heltalsdivision er valgt s˚adan, at der altid gælder<br />

b = b div a * a + b mod a<br />

Den nye funktion kan defineres rekursivt og følge nogenlunde samme skabelon som for<br />

den oprindelige gcd-funktion.<br />

I basistilfældet a = 0, hvor gcd (0,b) = b, er det let at konstruere s og t.<br />

I det rekursive tilfælde, hvor gcd (a,b) blev bestemt ud fra gcd (b mod a,a), kan man<br />

p˚a tilsvarende m˚ade konstruere s og t i (5.10) ud fra s ′ og t ′ , for hvilke<br />

s ′ · (b mod a) + t ′ · a = gcd (b mod a,a)<br />

Opgave 5.4 Løs denne opgave, n˚ar typen af lister er til r˚adighed. Læg mærke til, hvordan<br />

den mindste skive bevæger sig under løsning af puslespillet “Hanois t˚arn”: Hvert andet træk<br />

foreg˚ar med den mindste skive. Hvilken systematik er der i disse træk? Bemærk ogs˚a, at et<br />

træk er entydigt bestemt, hvis det ikke er den mindste skive, som skal flyttes. Benyt disse<br />

iagttagelser til at programmere en løsning p˚a puslespillet.<br />

90


Kapitel 6<br />

Højereordensfunktioner<br />

En funktion, der tager argumenter af type α og har funktionsværdier af type β, har type<br />

α → β. En konsekvens af det allerede i afsnit 2.3 nævnte forhold, at funktioner regnes for<br />

værdier p˚a linje med tal, tegn, tekster, tupler og alle andre størrelser, er, at argumenttypen<br />

α eller funktionsværditypen β selv igen kan være en funktionstype.<br />

Hvis det er tilfældet, siges funktionen at være af højere orden (eng.:higher order). Hvad<br />

mulighederne for højereordensfunktioner indebærer, undersøger vi i dette kapitel, idet vi<br />

først betragter funktioner med funktioner som værdi og derefter funktioner med funktioner<br />

som argument.<br />

6.1 Funktion som funktionsværdi<br />

I et af eksemplerne i afsnit 3.3 betragtedes udtrykket<br />

bogpris - bogpris * rabatpct div 100<br />

der b˚ade afhænger af bogpris og af rabatpct. Opfattes en bogs reelle pris for en studerende<br />

som funktion af bogpris, er der tale om en parametriseret funktion med rabatpct som<br />

parameter. Denne opfattelse afspejles i definitionerne<br />

fun studpris rabatpct<br />

= let fun pris bogpris = bogpris - bogpris * rabatpct div 100<br />

in pris end;<br />

val studpris = fn : int -> int -> int<br />

studpris 10;<br />

val it = fn : int -> int<br />

it 31000;<br />

val it = 27900 : int<br />

(studpris 10) 50000;<br />

val it = 45000 : int<br />

(studpris 12) 50000;<br />

val it = 44000 : int<br />

Her er studpris en funktion, defineret med rabatpct som formel parameter, men med<br />

en funktionsværdi, lokalt benævnt pris, der selv er en funktion (defineret med bogpris som<br />

formel parameter).<br />

91


N˚a vi derfor kalder studpris med argument 10, kan resultatet af dette funktionskald<br />

ikke direkte vises frem, for det er selv en funktion (af type int -> int). Men kaldes denne<br />

funktion (her med argumentet 31000), f˚as et synligt resultat (her 27900).<br />

Som de to sidste eksempellinjer viser, kan man ogs˚a levere de to argumenter (rabatpct<br />

og bogpris) p˚a en gang.<br />

6.1.1 Parentesregler ved funktionskald og funktionstype<br />

Vi har set, hvordan studpris skal kaldes p˚a formen (studpris r) p, idet den har typen<br />

studpris : int -> (int -> int). I afsnit 2.3.2 blev det imidlertid nævnt, at funktionsanvendelse<br />

er venstreassocierende. N˚ar funktionen kaldes, er parentesen derfor overflødig,<br />

og man kan bare skrive studpris r p. Det er derfor ogs˚a besluttet, at parentesen i typebeskrivelsen<br />

skal kunne undværes: studpris : int -> int -> int. Med andre ord gælder 1 ,<br />

at<br />

Funktionstypepil associerer fra højre.<br />

Vi s˚a i eksemplet ovenfor, hvordan SML-systemet benyttede denne konvention, idet funktionsdefinitionen<br />

blev besvaret med ekkoet val studpris = fn : int -> int -> int.<br />

6.1.2 Sekventialisering<br />

Med nøjagtig samme betydning som i definitionen ovenfor tillader SML ogs˚a, at man skriver<br />

fun studpris rabatpct bogpris<br />

= bogpris - bogpris * rabatpct div 100<br />

val studpris = fn : int -> int -> int<br />

Her er et andet eksempel: Anvendt p˚a det reelle tal d vil funktionen foroeg som funktionsværdi<br />

give den funktion, der forøger sit reelle argument med d.<br />

fun foroeg d r = r + d : real;<br />

val foroeg = fn : real -> real -> real<br />

foroeg 0.5;<br />

val it = fn : real -> real<br />

foroeg 0.5 2.0;<br />

val it = 2.5 : real<br />

(En lille advarsel om syntaks: Selv om man ved funktionskald frit kan vælge, om parenteserne<br />

skal skrives eller underforst˚as ((studpris 10) 31000 eller studpris 10 31000),<br />

s˚a er den slags parenteser forbudt i en funktionserklæring. Definitionen af Standard ML<br />

kræver, at funktionsnavnet følger umiddelbart efter det reserverede ord fun.)<br />

1 Det kan umiddelbart virke ulogisk, at funktionstypepil associerer i den modsatte retning af de operatorer,<br />

vi hidtil har mødt. Et andet sted, hvor retningen virker forkert, er ved funktionssammensætning: Den<br />

sammensatte funktion g ◦ f er som bekendt den, der i punktet x har værdien (g ◦ f)(x) = g(f(x)), s˚a at g ◦ f<br />

skal læses fra højre: først f, s˚a g. Miseren kan siges at stamme fra, at det fra først af var forkert at <strong>noter</strong>e<br />

funktionen f anvendt p˚a argumentet x som f(x); man skulle, som det gøres i visse matematiske kredse, have<br />

skrevet (x)f. Sammensat funktion ville derved blive ((x)f)g, og en funktion, der for argumenter af type α<br />

leverede funktioner fra β til γ, ville f˚a type (γ ← β) ← α, i begge tilfælde fint læsbare fra venstre.<br />

92


En funktion, der p˚a denne m˚ade “tager sine argumenter et ad gangen”, vil vi kalde<br />

sekventialiseret (eng.:curried) (efter den engelske logiker Haskell Brooks Curry 2 (1900–82)).<br />

6.1.3 Funktion af tupel eller sekventialiseret funktion?<br />

Det er tidligere blevet nævnt, at SML kun kan h˚andtere funktioner af én variabel, og<br />

vi har hidtil dannet funktioner, der tilsyneladende havde flere variable, ved at lade dem<br />

være funktioner, hvis argument var et tupel. Sekventialiserede funktioner er imidlertid ogs˚a<br />

funktioner af flere variable: Der skal kun et øjebliks overvejelse til for at indse, at noget, der<br />

forelagt et argument giver en funktion fra et andet argument til en værdi, jo netop svarer<br />

til en funktion, der fra et argument og et andet argument leverer værdien.<br />

Det er instruktivt at sammenligne foroeg med en funktion, der tager de to parametre<br />

p˚a en gang:<br />

fun adder (d,r) = r + d : real;<br />

val adder = fn : real * real -> real<br />

De to funktioner har naturligvis omtrent samme funktionalitet: for alle reelle værdier<br />

d og r vil foroeg d r = adder (d,r). Forskellen er, at man er nødt til at kende begge<br />

argumenter, før adder kan kaldes, mens foroeg allerede kan kaldes, s˚a snart første argument<br />

foreligger (selv om den først kan give et tal, n˚ar andet argument ogs˚a kendes).<br />

Læg ogs˚a mærke til parallelliteten i type. En funktion, der til argumenter af type α og<br />

β lader svare værdier af type γ har i det ene tilfælde typen α * β -> γ, i det andet typen<br />

α -> β -> γ. Bemærk tillige, at dette i første tilfælde grupperes (α * β) -> γ, i andet<br />

α -> (β -> γ).<br />

Har man brug for en funktion af to variable, er der med andre ord<br />

to forskellige m˚ader at “simulere” dette p˚a i SML (som i realiteten<br />

kun kender begrebet “funktion af én variabel”): Man kan danne en<br />

funktion af et par, eller man kan danne en sekventialiseret funktion,<br />

der kaldt med den ene komponent leverer en funktion af den anden<br />

komponent.<br />

Hvilken af de to metoder er s˚a bedst? Det afhænger af sammenhængen. P˚a en vis m˚ade<br />

er den sekventialiserede form mest generel, for i den kan funktioner bruges p˚a hele tre m˚ader:<br />

helt uden argumenter, med et argument eller med begge argumenter. Funktioner af par kan<br />

kun bruges p˚a to m˚ader: uden eller med argumentpar. Den sekventialiserede form kan ogs˚a<br />

virke mest elegant, fordi man ikke som ved tupler skal gruppere argumenterne med kommaer<br />

og parenteser.<br />

Skal en funktion g levere to resultater, kan det imidlertid kun ske ved, at g returnerer<br />

parret af de to resultater. Skal det sidenhen efterbehandles i sammensætningen f(g . . .), m˚a<br />

f nødvendigvis være en funktion af par.<br />

2 selv om Moses Schönfinkel brugte metoden i en artikel fra 1924 og ideen allerede forefindes i Gottlob<br />

Freges “Grundgesetze der Arithmetik” fra 1893.<br />

93


6.1.4 Operator som funktion<br />

Har man defineret<br />

fun sum (a,b) = a + b;<br />

val sum = fn : int * int -> int<br />

kan 28 - 5 + 3 ogs˚a skrives sum (28 - 5,3), og mere almindeligt er der stor overensstemmelse<br />

mellem operatorer, som skal skrives mellem deres to operander, og funktioner, hvis<br />

argumenter er et par. I SML har man valgt, at disse to begreber dybest set skal være ens:<br />

operatorer opfattes som funktioner, der har par som argumenttype, men som syntaktisk har<br />

f˚aet “infiks-status” i den forstand, at de ikke som normale funktioner kaldes ved at blive<br />

skrevet foran deres argumentpar, men ved at blive skrevet imellem parrets to komponenter.<br />

Denne “infiks-status” ophæves af symbolet “op”; hvis “$” er en operator, kan man alts˚a<br />

i steden for “a $ b” skrive “op $ (a,b)”, og specielt er op + synonym med den ovenfor<br />

definerede funktion sum. Hvis man i en sammenhæng, hvor der skal anføres en funktion<br />

(vi skal senere se eksempler p˚a den slags kontekster), ønsker at bruge en operator $ (med<br />

infiks-status), kan man alts˚a skrive op $.<br />

Uden sine operander er en operator ikke et lovligt udtryk:<br />

+;<br />

! Toplevel input:<br />

! +;<br />

! ^<br />

! Ill-formed infix expression<br />

men funktioner er lovlige værdier, s˚a med op kan man for eksempel se, hvilke typer en<br />

operator forventer af sine operander:<br />

op ^;<br />

val it = fn : string * string -> string<br />

it ("vand","ring");<br />

val it = "vandring": string<br />

6.2 Funktion som funktionsargument<br />

For ML er der intet ekstraordinært i, at en af en funktions parametre selv kan være en<br />

funktion. Som et simpelt eksempel herp˚a kunne man konstruere en funktion, der evaluerede<br />

sin parameter i nulpunktet:<br />

fun nulvaerdi f = f 0.0;<br />

val ’a nulvaerdi = fn : (real -> ’a) -> ’a<br />

load "Math";<br />

val it = () : unit<br />

nulvaerdi Math.sqrt;<br />

val it = 0.0 : real<br />

nulvaerdi Math.exp;<br />

val it = 1.0 : real<br />

94


Som typen (real -> α) -> α viser (parentesen kan ikke undværes), skal nulvaerdi<br />

kaldes med et argument af type real -> α. Her er en af de situationer, hvor man kan have<br />

glæde af, at foroeg er sekventialiseret:<br />

nulvaerdi (foroeg ~0.6)<br />

val it = ~0.6 : real<br />

(Parentesen kan ikke undværes.)<br />

Som et andet eksempel konstrueres en funktion, der danner b i=a f(i), det vil sige<br />

f(a) + f(a + 1) + . . . + f(b − 1) + f(b)<br />

fun sum (f,a,b)<br />

= if a > b then 0.0 else f a + sum (f,a+1,b);<br />

val sum = fn : (int -> real) * int * int -> real<br />

sum (real,1,10);<br />

val it = 55.0 : real<br />

Vi regnede med, det ville være mest nyttigt, hvis f kunne have reelle funktionsværdier,<br />

men udbuddet af standardfunktioner af type int -> real er ikke stort. En af dem er real,<br />

der omsætter et heltal til reel type (se figur 2.1), og vi har s˚a fundet summen af tallene fra<br />

1 til 10. Ved at definere et passende f kan vi beregne<br />

fun reciprok n = 1.0 / real n;<br />

val reciprok = fn : int -> real<br />

sum (reciprok,1,100);<br />

val it = 5.18737751764 : real<br />

1 + 1 1 1<br />

+ + . . . +<br />

2 3 100<br />

Undertiden er det naturligt at kombinere funktioner som argument med sekventialisering.<br />

Som et afsluttende eksempel defineres den funktion until af tre argumenter, for hvilken<br />

until p f x gennemg˚ar følgen x, f(x), f(f(x)), f 3 (x), . . . og som funktionsværdi har det<br />

første element, der opfylder p. Her tænker vi os, p er et s˚akaldt prædikat, det vil sige en<br />

funktion med bool som resultattype. Et element f i (x) siges at opfylde p, hvis p(f i (x))<br />

bliver true.<br />

fun until p f<br />

= let fun g x = if p x then x else g (f x)<br />

in g end;<br />

val ’a until = fn : (’a -> bool) -> (’a -> ’a) -> ’a -> ’a<br />

Med until er formuleret en meget generel skabelon, der passer p˚a mange approksimationsproblemer.<br />

Man kan for eksempel vise, at hvis x er en tilnærmelse til √ a<br />

x+ x a, vil 2 være<br />

en bedre tilnærmelse. Vi kan derfor bruge until til en beregning af √ 5 med stor nøjagtighed:<br />

95


fun godnok x = abs (x * x - 5.0) < 1E~4<br />

fun naeste x = (x + 5.0 / x) / 2.0;<br />

val godnok = fn : real -> bool<br />

val naeste = fn : real -> real<br />

until godnok naeste 1.0;<br />

val it = 2.23606889564 : real<br />

it * it;<br />

val it = 5.00000410606 : real<br />

Man kan more sig med at bruge until til definition af en udgave af fakultetsfunktionen:<br />

fun fak n<br />

= let fun godnok (i, ) = i = n<br />

fun naeste (i,p) = (i+1,(i+1)*p)<br />

in #2 (until godnok naeste (0,1)) end;<br />

val fak = fn : int -> int<br />

fak 7;<br />

val it = 5040 : int<br />

6.3 Anonym funktion<br />

Den funktion, som fører parametermønster over i krop, <strong>noter</strong>es i SML<br />

fn parametermønster => krop<br />

idet det formelle parametermønster er en pladsholder, hvis forekomster i krop erstattes af<br />

det faktiske argument, n˚ar funktionen kaldes. Med denne form for udtryk, som ogs˚a kaldes<br />

et lambdaudtryk, fordi en udbredt matematisk notation for det er λ parametermønster.krop,<br />

kan man alts˚a arbejde med funktioner uden at være tvunget til at give dem et navn.<br />

Her er for eksempel kvadreringsfunktionen:<br />

fn n => n * n;<br />

val it = fn : int -> int<br />

it 5;<br />

val it = 25 : int<br />

Skal en hjælpefunktion blot bruges som argument til en anden funktion, kan man ofte<br />

spare at give den et navn. Her er et par af de tidligere eksempler med anonyme hjælpefunktioner:<br />

sum (fn n => 1.0 / real n,1,100);<br />

val it = 5.18737751764 : real<br />

until (fn x => abs (x * x - 5.0) < 1E~4)<br />

(fn x => (x + 5.0 / x) / 2.0)<br />

1.0;<br />

val it = 2.23606889564 : real<br />

96


6.3.1 Funktionserklæring med val<br />

At lave en funktion og give den et navn<br />

val fnavn = fn x => krop<br />

er næsten det samme som at levere funktionserklæringen<br />

fun fnavn x = krop<br />

Forskellen ligger i virkefeltsreglerne: Som nævnt i afsnit 3.3 er fnavn til r˚adighed i krop i<br />

andet tilfælde, men ikke i første. Formen med val kan derfor ikke bruges til erklæring af en<br />

rekursiv funktion 3 .<br />

Det er forskellen mellem de to virkefeltsregler i afsnit 3.3, der er afgørende for, om<br />

man skal bruge val eller fun, og ikke spørgsm˚alet om, hvorvidt det er en funktion, der skal<br />

erklæres. Skriver man fun fnavn, s˚a skal der ogs˚a følge et eller flere parametermønstre efter.<br />

Gør der ikke det, skal man bruge val:<br />

val efterfoelger = foroeg 1.0;<br />

val efterfoelger = fn : real -> real<br />

efterfoelger 29.0;<br />

val it = 30.0 : real<br />

Og selv om der bruges fun f x = . . ., kan det godt være, f har andre parametre end x (se<br />

for eksempel definitionen af until).<br />

6.4 Sammenfatning<br />

I SML er funktioner specielle værdier, og overalt, hvor der skal bruges en type, kan det<br />

specielt være en funktionstype.<br />

Alle funktioner har ét argument. Hvis argumentet har type τ og funktionsværdien type<br />

τ ′ , har funktionen type τ → τ ′ . Effekten af flere parametre kan opn˚as ved at lade argumentet<br />

være et tupel, s˚a funktionens type bliver τ1 * . . . * τn -> τ, eller ved at lade funktionen<br />

være sekventialiseret, med type τ1 -> . . . -> τn -> τ.<br />

En funktion skal kaldes i overensstemmelse med sin erklæring; er argumentet et tupel, skal<br />

man bruge parenteser og kommaer; er funktionen sekventialiseret, tager den sine argumenter<br />

et ad gangen, og de skal bare skrives efter hinanden.<br />

Operatorer er funktioner, der har f˚aet syntaktisk infiks-status. Nøgleordet op (som binder<br />

stærkere end nogen anden sprogkonstruktion) ophæver en operators infiks-status, s˚a den i<br />

stedet bliver en funktion af par.<br />

Hvis en funktion ikke er rekursiv, behøver man ikke at give den et navn, men kan i stedet<br />

bruge notationen fn mønster => krop.<br />

Erklærer man en funktion uden at anføre et parametermønster, skal nøgleordet være<br />

val.<br />

3 I den formelle definition af Standard ML indeholder det s˚akaldte “kernesprog” kun erklæringer med<br />

val, idet “fun fnavn x = krop” opfattes som en “afledt form” ækvivalent med<br />

“val rec fnavn = fn x => krop”, hvor rec signalerer den ønskede variant af virkefeltsreglerne.<br />

97


Selv om en erklæring benytter val, kan det godt være,<br />

den erklærer en funktion.<br />

Det er unødvendigt at skrive parametermønstre, som ikke bruges.<br />

6.5 Opgaver<br />

Selv om en fun-erklæring benytter et vist antal formelle<br />

parametermønstre, kan det godt være, den erklærede<br />

funktion har flere parametre end dem.<br />

Opgave 6.1 Brug sum til at beregne summen af kvadrattallene fra 1 2 til 10 2 .<br />

Opgave 6.2 Generaliser den metode, som i afsnit 6.2 blev brugt til beregning af en tilnærmet<br />

værdi af √ 5, s˚a der defineres en kvadratrodsfunktion.<br />

Opgave 6.3 Definer en sekventialiseret funktion, der beregner differenskvotienten<br />

f(x + h) − fx<br />

h<br />

Funktionen skal tage sine tre variable i rækkefølgen først h, s˚a f, og sidst x.<br />

Opgave 6.4 Definer deriv som værdien af funktionen defineret i foreg˚aende opgave i<br />

punktet, hvor h er en titusindedel. For “pæne” funktioner f vil deriv f være en god tilnærmelse<br />

til differentialkvotienten f ′ .<br />

Opgave 6.5 Newtons metode til bestemmelse af et nulpunkt for en funktion f g˚ar ud p˚a,<br />

at hvis y er en tilnærmelse til roden, bliver y − f(y)<br />

f ′ en bedre tilnærmelse. Den formel for<br />

(y)<br />

tilnærmelse til kvadratrødder, der blev brugt i afsnit 6.2, opst˚ar ved Newtons metode, idet<br />

man for f(x) = x2 − a f˚ar f ′ (x) = 2x og x − f(x)<br />

f ′ (x) = x − x2 a<br />

−a x+ x = 2x 2 .<br />

Definer en funktion newton ved, at newton f skal være until godnok naeste, hvor<br />

f(x)<br />

godnok tester, om |fx| er mindre end en titusindedel, og naeste x er x −<br />

deriv f x .<br />

Kunne man lade godnok og naeste være anonyme?<br />

Opgave 6.6 Brug funktionen newton fra foreg˚aende opgave til definition af en kubikrodsfunktion.<br />

[Vink: Beregn 3√ a som et nulpunkt for x 3 − a.]<br />

98


Kapitel 7<br />

Tegn, tekster, udskrivning<br />

Hidtil har vi fremstillet det, som om resultaterne fra systemet direkte havde form af tal,<br />

sandhedsværdier, sæt, lister, og s˚a videre, men derved ses der bort fra, at kommunikationen<br />

i virkeligheden foreg˚ar ved hjælp af skærm og tastatur. I transmissionen er indskudt passende<br />

omsætninger mellem ydre og indre repræsentationer, og det er naturligt at opfatte det, der<br />

dannes fra tastaturet, og som fremkommer p˚a skærmen, som tekster opbygget af enkelttegn.<br />

7.1 Typen string af tekster<br />

Hvis man selv vil kunne styre, hvordan resultater præsenteres, er det nødvendigt at kunne<br />

arbejde med tekster i programmerne, og hertil har ML grundtypen string.<br />

Tekstkonstanter blev omtalt i afsnit 3.10.1. I Moscow ML skrives en tekstkonstant som<br />

en følge af nul eller flere tegn mellem et par anførelsestegn ("). De tegn, der kan indg˚a i en<br />

tekstkonstant, er tegnene fra ISO’s 8-bit-alfabet (se tabel 1.2). Bortset fra anførelsestegn og<br />

spejlvendt skr˚astreg kan de grafiske tegn (med andre ord tegnene med koder 32, 33, 35–91,<br />

93–126, 160–255) anføres direkte; dertil kommer nogle særlige betegnelser, hvor spejlvendt<br />

skr˚astreg (eng.:backslash) er brugt som undvigelsestegn, se tabel 7.1.<br />

Form˚alet med konstruktionen \ff . . . f\ nævnt nederst i tabellen er at gøre det muligt<br />

at dele lange tekster over flere linjer: man sætter bare en spejlvendt skr˚astreg sidst p˚a en<br />

linje og forrest p˚a en ny linje — s˚a vil det have samme virkning, som hvis man var fortsat<br />

p˚a samme linje.<br />

Funktionen print skal kaldes med en tekst som argument; selve funktionsværdien er<br />

uinteressant (det tomme sæt), men forinden bliver argumentet som bivirkning overført til<br />

uddata, idet alle opstillingstegn respekteres. P˚a den m˚ade kan man selv bestemme, hvad<br />

der skal komme frem p˚a skærmen — blot kan man ikke slippe for, at der allersidst tilføjes<br />

“> val it = () : unit”:<br />

print;<br />

val it = fn : string -> unit<br />

print "Her\n og\"\nder";<br />

Her<br />

og"<br />

der> val it = () : unit<br />

99


\a tegnet BEL (som f˚ar terminalen til at afgive lyd)<br />

\b tilbagerykningstegn, BS<br />

\t tabuleringstegn, HT<br />

\n linjeskift, LF eller NL<br />

\v vertikal tabulering, VT<br />

\f sideskift, FF<br />

\r vognretur, CR<br />

\^c c skal være et af tegnene med kode mellem 64 og 95, og symbolet<br />

her st˚ar da for det tegn, hvis kode er 64 mindre (se tabel 1.1)<br />

\ddd De tre decimale cifre ddd skal angive et tal mellem 0 og 255, og<br />

symbolet angiver da tegnet med den p˚agældende kode<br />

\uxxxx Her skal xxxx være fire hexadecimale cifre, som angiver et tal mellem<br />

0 og 255, og hele symbolet angiver da tegnet med den p˚agældende<br />

kode<br />

\" Anførelsestegnet selv<br />

\\ Spejlvendt skr˚astreg<br />

\ff . . . f\ Idet ff . . . f mellem de to skr˚astreger st˚ar for et eller flere opstillingstegn<br />

(for eksempel blanktegn, tabuleringstegn, linjeskift, sideskift),<br />

repræsenterer hele konstruktionen en tom følge af tegn.<br />

Tabel 7.1: ML’s særlige symboler for tegn i tekst- og tegnkonstanter.<br />

7.2 Typen char af tegn<br />

I den oprindelige definition [7] af Standard ML m˚atte enkelttegn h˚andteres ved hjælp af<br />

deres koder, men Moscow ML følger revisionen [9] ved ogs˚a at have tegn som grundtype,<br />

under betegnelsen char (eng.:character).<br />

Tegnkonstanter anføres simpelt hen som den tilsvarende tekstkonstant af længde 1, men<br />

med et nummertegn (#) umiddelbart foran. Notationen for et blanktegn er for eksempel #.<br />

En tekst er naturligvis omtrent det samme som en liste af tegn, blot m˚a man forvente, at<br />

systemet kan h˚andtere tekster mere effektivt end generelle lister. Tabel 7.2 viser standardfunktioner<br />

og -operatorer p˚a tegn og tekster, og her finder man netop funktionerne explode<br />

og implode, som etablerer den nævnte korrespondance.<br />

Der er grund til at fremhæve, at selv om Standard ML 1 opfatter de normale symboler<br />

for ordning (=) som virkende p˚a heltal, n˚ar andet ikke er angivet<br />

op bool<br />

s˚a kan de alts˚a ogs˚a anvendes p˚a operander af type real og, som tabellen viser, af typerne<br />

char eller string. (Men de to operander skal have samme type.)<br />

1 Dette blev indført med revisionen i 1997<br />

100


status navn type virkning<br />

ord char -> int tegnets kode eller vægt<br />

chr int -> char tegnet med den p˚agældende<br />

op =<br />

<br />

string * string -> bool<br />

char * char -> bool<br />

kode<br />

tegn og tekster kan sammenlignes<br />

med de sædvanlige ulighedstegn.<br />

For tekster bruges<br />

leksikografisk orden<br />

str char -> string teksten best˚aende af det<br />

p˚agældende tegn<br />

op ^ string * string -> string sammenstillen af to tekster<br />

explode string -> char list listen af de tegn, som indg˚ar i<br />

teksten<br />

implode char list -> string teksten med de p˚agældende<br />

tegn i<br />

size string -> int antallet af tegn<br />

String.substring string * int * int<br />

-> string<br />

String.substring (s,i,n)<br />

udtager n p˚a hinanden<br />

følgende tegn fra s regnet fra<br />

det i-te (idet nummerering<br />

begynder med 0)<br />

print string -> unit bivirkning: overføring af argumentet<br />

til uddata<br />

7.3 Typeerklæring<br />

Tabel 7.2: Operatorer og funktioner p˚a tegn og tekster.<br />

Under arbejde med tekster vil typen “char list” ofte optræde. P˚a samme m˚ade kan der<br />

i andre sammenhænge være visse typer, man flere gange f˚ar brug for; i geometriske anvendelser<br />

m˚aske punkter i rummet af type real * real * real. For at det ikke skal være<br />

nødvendigt at gentage komplicerede typeudtryk, indeholder ML muligheden for, at man<br />

med en typeerklæring kan indføre korte navne for typeudtryk. I de nævnte tilfælde kunne<br />

man skrive<br />

type linje = char list;<br />

type linje = char list<br />

type rumpunkt = real * real * real;<br />

type rumpunkt = real * real * real<br />

og i resten af programmet kan linje og rumpunkt bruges som typesynonymer med den<br />

indførte betydning. Forkortelserne benyttes ikke i svar fra systemet. Her er en funktion, der<br />

indsætter blanktegn mellem hvert af en linjes tegn:<br />

fun spatier ([] : linje) : linje = []<br />

| spatier [c] = [c]<br />

| spatier (c :: cr) = c :: # :: spatier cr;<br />

101


val spatier = fn : char list -> char list<br />

implode (spatier (explode "Prøv selv!"));<br />

val it = "P r ø v s e l v !": string<br />

Tabel 7.3 viser typeerklæringers syntaks. Som det ses, kan man ogs˚a indføre typer med<br />

typbind ::= tyvarseq identifier = ty typekonstruktør<br />

tyvarseq ::= uden typeparametre<br />

tyvar med én typeparameter<br />

( tyvar , . . . , tyvar ) med en eller flere typeparametre<br />

Tabel 7.3: Forenklet syntaks for binding af navn til type i ML.<br />

parametre. Man kan for eksempel vælge, at en α matrix skal være en liste af lister af<br />

elementer af type α:<br />

type ’element matrix = ’element list list;<br />

type ’element matrix = ’element list list<br />

og derefter er der mulighed for at skrive int matrix, real matrix, rumpunkt matrix og<br />

s˚a videre.<br />

7.4 Eksempler<br />

7.4.1 Majuskler<br />

Lad os overveje, hvordan et program kan omforme en tekst, s˚a sm˚a bogstaver erstattes af<br />

store. For at f˚a adgang til de enkelte tegn i teksten m˚a den ændres til en liste af tegn,<br />

s˚a en delopgave m˚a g˚a ud p˚a at foretage den nævnte ændring p˚a et enkelt tegn. Bogstaverne<br />

mellem a og z ligger samlet, og der er en systematisk forskydning i koden, som vil<br />

transformere dem til store bogstaver A til Z, men æ, ø og ˚a m˚a behandles separat:<br />

fun stort c = if #"a≪= c andalso c


val majuskler = fn : string -> string<br />

majuskler "Side 3/linje 4";<br />

val it = "SIDE 3/LINJE 4": string<br />

7.4.2 Decimale talord<br />

Tal kan repræsenteres som tekster (med fortegn, cifre, decimalpunkt og s˚a videre) eller<br />

som værdier af type int eller real. Nu kan vi skrive funktioner, der omsætter mellem<br />

repræsentationerne, men lad os for nemheds skyld nøjes med ikke negative hele tal.<br />

N˚ar en følge af cifre skal tolkes som et tal, er det nyttigt at gøre sig klart, at det er en<br />

konsekvens af den sædvanlige titalsnotation, at hvis et talord med m + n cifre deles i to<br />

talord med henholdsvis m og n cifre:<br />

m+n<br />

<br />

dd . . . d<br />

dd . . . d<br />

m n<br />

s˚a er der følgende simple sammenhæng mellem talværdierne:<br />

værdien af<br />

m+n<br />

<br />

dd . . . ddd . . . d = værdien af dd . . . d<br />

<br />

m<br />

· 10 n + værdien af dd . . . d<br />

<br />

n<br />

Denne sammenhæng gælder endda for m = 0 eller n = 0, hvis man lader værdien af en tom<br />

cifferfølge være 0.<br />

Omsætning fra et tal til en følge af cifre kan nu programmeres, og derved kan man ogs˚a<br />

komme fra tal til talord. Vi udnytter, at cifrene i tegnsættet ligger samlet i rækkefølge, s˚a<br />

man systematisk med udtrykket chr (ord #"0"+ d) kan komme fra værdien d til cifferet<br />

med den værdi:<br />

fun divmod (a,b) = (a div b,a mod b);<br />

val divmod = fn : int * int -> int * int<br />

fun tilciffer n = chr (ord #"0"+ n);<br />

val tilciffer = fn : int -> char<br />

fun tilcifre n<br />

= if n = 0 then []<br />

else let val (q,r) = divmod (n,10)<br />

in tilcifre q @ [tilciffer r] end;<br />

val tilcifre = fn : int -> char list<br />

fun tiltekst n = if n < 0 then -"^ implode (tilcifre (~ n))<br />

else if n = 0 then "0"<br />

else implode (tilcifre n);<br />

val tiltekst = fn : int -> string<br />

tiltekst 0;<br />

val it = "0": string<br />

tiltekst 007;<br />

val it = "7": string<br />

103


I tiltekst fanges 0 som et særtilfælde, s˚a det ikke bliver skrevet ud som en tom tekst,<br />

: string, og negative talord vises med almindelig bindestreg (-) som minus-tegn. Som<br />

tidligere nævnt rummer modulet Int en tilsvarende funktion Int.toString (der dog bruger<br />

~ som minus-tegn).<br />

Vil man bruge denne metode i modsat retning, skal man kunne dele en liste op i en<br />

forreste del og et sidste element. Det kan for eksempel skrives s˚aledes, idet vi supplerer<br />

List.last med en funktion, der tager resten af listen p˚anær sidste element:<br />

fun init xr = rev (tl (rev xr));<br />

val init = fn : ’a list -> ’a list<br />

fun fraciffer c = ord c - ord #"0";<br />

val fraciffer = fn : char -> int<br />

fun fracifre dr<br />

= if null dr then 0<br />

else fracifre (init dr) * 10 + fraciffer (List.last dr);<br />

val fracifre = fn : char list -> int<br />

fun fratekst s = fracifre (explode s);<br />

val fratekst = fn : string -> int<br />

fratekst "007";<br />

val it = 7 : int<br />

Hvis man vil opspalte teksten i et forreste ciffer og halen, er det hensigtsmæssigt at<br />

beregne de tilhørende vægte sideløbende med tallene. For hvert nyt ciffer foran skal vægten<br />

ganges med 10, og den nye værdi findes ud fra den gamle ved tilføjelse af cifferet, men vægtet<br />

med den gamle vægt:<br />

fun vaegtOgVaerdi [] = (1,0)<br />

| vaegtOgVaerdi (d :: dr) = let val (v,n) = vaegtOgVaerdi dr<br />

in (v * 10,fraciffer d * v + n) end;<br />

val vaegtOgVaerdi = fn : char list -> int * int<br />

fun fratekst s = #2 (vaegtOgVaerdi (explode s));<br />

val fratekst = fn : string -> int<br />

fratekst "007";<br />

val it = 7 : int<br />

Det bør i øvrigt bemærkes, at modulet Int indeholder en funktion Int.fromString til<br />

omsætning fra tekst til heltal, s˚a man behøver ikke selv at programmere funktioner som<br />

fratekst ovenfor. Int.fromString kan oven i købet ogs˚a h˚andtere fortegn.<br />

7.4.3 Opstilling<br />

Man f˚ar ofte brug for at stille en tekst op i et felt med en bestemt bredde, for eksempel venstrestillet<br />

eller højrestillet i feltet eller i midten. Her er en funktion, der venstrestiller en tekst,<br />

hvor vi indfører en hjælpefunktion, der danner en liste af blanktegn, og en hjælpestørrelse,<br />

der holder styr p˚a, hvor meget af det afsatte felt der er ledigt:<br />

fun ljustify (w,s)<br />

104


= let fun blanke n = if n = 0 then [] else # :: blanke (n-1)<br />

val restlaengde = w - size s<br />

in s ^ implode (blanke restlaengde) end;<br />

val ljustify = fn : int * string -> string<br />

ljustify (5,"˚Ar");<br />

val it = "˚Ar ": string<br />

Lad os til sidst konstruere en funktion, der giver en grafisk fremstilling af et talsæt som<br />

histogram eller søjlediagram med liggende søjler; noget i retning af<br />

********************<br />

***********<br />

**************<br />

******************<br />

**********************************<br />

****************<br />

****<br />

Hvis tallenes størrelsesorden ikke er kendt p˚a forh˚and, vil det være praktisk at normere<br />

dem, for eksempel s˚adan at den længste søjle fylder hele tekstens bredde.<br />

For at give programmet større anvendelighed indføres tekstens bredde som en konstant<br />

— s˚a skal man kun ændre ét sted, hvis den skal laves om.<br />

Tallene behøver ikke at være hele, men vi vil g˚a ud fra, intet af dem er negativt. Funktionen<br />

soejler danner en liste af søjler ud fra en liste af heltal, og det endelige diagram f˚as<br />

ved, at man skyder et linjeskift-tegn ind efter hver søjle:<br />

val bredde = 50 (* maksimale søjlestørrelse er global konstant *);<br />

val bredde = 50 : int<br />

fun max (x : real,y : real) = if x < y then y else x;<br />

val max = fn : real * real -> real<br />

fun maximum [] = 0.0<br />

| maximum (tal :: talliste) = max (tal,maximum talliste);<br />

val maximum = fn : real list -> real<br />

fun normer talliste<br />

= let val stoerste = maximum talliste<br />

fun norm [] = []<br />

| norm (t :: tr) = round (t / stoerste * real bredde)<br />

:: norm tr 2<br />

in norm talliste end;<br />

val normer = fn : real list -> int list<br />

fun soejle n = if n = 0 then [] else #"*":: soejle (n - 1);<br />

val soejle = fn : int -> char list<br />

fun soejler [] = []<br />

| soejler (n :: nr) = soejle n :: soejler nr;<br />

val soejler = fn : int list -> char list list<br />

2 Husk, at reglerne for prioritet og associering indebærer, at højresiden grupperes som<br />

(round((t/stoerste)*(real bredde)))::(norm tr).<br />

105


fun histogram talliste<br />

= let val soejleliste = soejler (normer talliste)<br />

fun vis [] = []<br />

| vis (s :: sr) = s @ [#"\n"] @ vis sr<br />

in implode (vis soejleliste) end;<br />

val histogram = fn : real list -> string print (histogram [25.0,2.0,15.0,16.0,9.<br />

**************************************************<br />

****<br />

******************************<br />

********************************<br />

*******************<br />

*************<br />

*******<br />

*****<br />

****<br />

**<br />

*<br />

val it = () : unit<br />

7.5 Listefunktionalen map<br />

N˚ar man arbejder med lister, f˚ar man meget ofte brug for skabelonen<br />

fun F [] = []<br />

| F (x :: xr) = f x :: F xr;<br />

(7.1)<br />

Ud fra en funktion f, der virker p˚a elementer, dannes en ny funktion F , som skal anvende<br />

f p˚a hvert element i en liste. Hvis f har type α -> β, f˚ar F type α list -> β list, og<br />

vi har alts˚a<br />

F [a1, a2, ..., an] = [fa1, fa2, ..., fan]<br />

Man kan opfatte F som en funktion af f, og sammenhængen findes foruddefineret i<br />

Moscow ML under navnet map; der gælder med andre ord F = map f. Resultatet af<br />

kaldet map f er s˚aledes en funktion, der skal have en liste som argument.<br />

Med andre ord er map en funktion af to variable: den funktion, hvis virkning skal “løftes<br />

fra elementer til lister”, og den liste, for hvilken dette skal gøres. Da disse to argumenter<br />

ofte ikke vil foreligge samtidigt, har man valgt at lade map være en sekventialiseret funktion,<br />

der tager sit funktionsargument først.<br />

Man ville sagtens selv kunne definere funktionen:<br />

fun map f [] = []<br />

| map f (x :: xr) = f x :: map f xr<br />

eller lidt mere effektivt (med udnyttelse af, at argumentet f ikke ændres)<br />

fun map f = let fun g [] = []<br />

| g (x :: xr) = f x :: g xr<br />

in g end<br />

106


men det er som nævnt overflødigt: map er tilgængelig som standardfunktion p˚a yderste<br />

niveau.<br />

Funktionen store i afsnit 7.4.1 og funktionen soejler i afsnit 7.4.3 fulgte skabelonen<br />

(7.1). Vi kunne med andre ord i afsnit 7.4.1 have sparet os definitionen af store og<br />

brugt map stort i stedet for; hovedfunktionen kunne have været defineret:<br />

fun majuskler s = implode (map stort (explode s));<br />

val majuskler = fn : string -> string<br />

P˚a samme m˚ade kunne man i afsnit 7.4.3 have skrevet map soejle i stedet for soejler.<br />

Læg mærke til, at hvis man alligevel vil indføre navnet soejler, men definere funktionen<br />

ved hjælp af map, s˚a er det valgfrit, om man vil tage et listeargument med eller ej:<br />

fun soejler nr = map soejle nr;<br />

val soejler = fn : int list -> char list list<br />

val soejler = map soejle;<br />

val soejler = fn : int list -> char list list<br />

— de to muligheder er helt ækvivalente (matematisk set er f = g hvis og kun hvis f(x) =<br />

g(x) for alle x), men tager man ikke noget argument med, skal der bruges det reserverede<br />

ord val i stedet for fun!<br />

Definitionen af norm fra afsnit 7.4.3 følger ogs˚a omtrent skabelonen (7.1) — den eneste<br />

afvigelse er, at der st˚ar et udtryk “round (t / stoerste * real bredde)”, hvor der efter<br />

skabelonen burde være et kald af en funktion af t.<br />

Man kunne med andre ord opn˚a overensstemmelse med skabelonen ved at definere en<br />

funktion, der afbildede t over i round (t / stoerste * real bredde). Denne funktion<br />

skulle imidlertid kun bruges til dette ene form˚al, og at skulle finde p˚a et navn til den og<br />

bruge selvstændige definitionslinjer p˚a den kan synes et stort apparat at sætte i sving (for<br />

blot at f˚a map’s skabelon til at passe).<br />

For at lette den slags situationer indeholder ML som omtalt i afsnit 6.3 muligheden for,<br />

at man kan arbejde med en funktion uden at give den navn.<br />

I den tidligere kontekst kunne norm have været defineret gennem:<br />

val norm = map (fn t => round (t / stoerste * real bredde))<br />

Sammenfattende kunne hele definitionen<br />

fun normer talliste<br />

= let val stoerste = maximum talliste<br />

fun norm [] = []<br />

| norm (t :: tr) = round (t / stoerste * real bredde)<br />

:: norm tr<br />

in norm talliste end<br />

s˚aledes erstattes af<br />

fun normer talliste<br />

= let val stoerste = maximum talliste<br />

in map (fn t => round (t / stoerste * real bredde))<br />

talliste<br />

end<br />

107


7.6 Sammenfatning<br />

Typerne af tegn og tekster hedder henholdsvis char og string. Tekstkonstanter skrives<br />

med tegnene mellem anførelsestegn; tegnkonstanter som tekster af længde 1, hvor der yderligere<br />

er sat et nummertegn foran. Forskellige særlige koder (med spejlvendt skr˚astreg som<br />

undvigelsestegn) gør det muligt at anføre alle 8-bit ISO-tegn.<br />

Man kan ikke indlægge et linjeskift-tegn i en tekstkonstant ved direkte at skifte linje,<br />

mens konstanten skrives ned; skal et linjeskift-tegn indg˚a i en tekstkonstant, m˚a det skrives<br />

\n. Normalt skal alle dele af en tekstkonstant skrives p˚a samme linje; ved at man lader en<br />

linje slutte med spejlvendt skr˚astreg og næste linje begynde med spejlvendt skr˚astreg, kan<br />

lange tekstkonstanter dog deles over flere programlinjer.<br />

Typeudtryk kan navngives med en typeerklæring.<br />

Hvis f er en funktion, der fører elementer af type τ over i værdier af type τ ′ , vil map f<br />

“løfte” funktionen op til at føre en liste af type τ list over i en lige s˚a lang liste af type<br />

τ ′ list (ved at anvende f p˚a hvert listeelement for sig).<br />

7.7 Opgaver<br />

Opgave 7.1 Skriv en funktion, som for to ikke negative hele tal w og n danner et decimalt<br />

talord for n med foranstillede nuller, s˚a der i alt bruges w positioner.<br />

Opgave 7.2 Skriv en funktion, der udskriver et heltal som et talord, hvor cifrene er grupperet<br />

tre og tre p˚a sædvanlig m˚ade (for eksempel 16.427.244). Udbyg funktionen til en<br />

sekventialiseret funktion af to variable, der som første argument tager det tegn, der skal<br />

bruges som skilletegn (for eksempel punktum, komma eller blanktegn), og som andet argument<br />

heltallet.<br />

Opgave 7.3 Skriv en funktion, der højrestiller en tekst i et felt og en, der midtstiller en<br />

tekst i et felt af given længde.<br />

1<br />

1 1<br />

1 2 1<br />

1 3 3 1<br />

1 4 6 4 1<br />

1 5 10 10 5 1<br />

1 6 15 20 15 6 1<br />

1 7 21 35 35 21 7 1<br />

1 8 28 56 70 56 28 8 1<br />

1 9 36 84 126 126 84 36 9 1<br />

1 10 45 120 210 252 210 120 45 10 1<br />

Opgave 7.4 En af opgaverne i kapitel 5 omtalte Pascals trekant. Skriv en funktion, der<br />

for en værdi n giver en centreret opstilling af de første n + 1 linjer af Pascals trekant. For<br />

n = 10 for eksempel “vulkankeglen” vist ovenfor.<br />

108


Opgave 7.5 Mange ting kan g˚a galt i funktionen histogram fra afsnit 7.4.3 (man kan for<br />

eksempel komme til at dividere med 0). Skriv en robust version.<br />

Opgave 7.6 Skriv en funktion, der præsenterer en talliste som søjlediagram med vertikale<br />

søjler.<br />

Opgave 7.7 Funktionen skaler, for hvilken skaler ([x1,. . .,xn],y) = [ x1 xn ,. . ., ], kan<br />

y y<br />

i SML defineres<br />

fun skaler ([], ) = []<br />

| skaler (x :: xr,y) = x / y :: skaler (xr,y)<br />

Definer en hjælpefunktion s p˚a en s˚adan m˚ade, at skaler derefter ville kunne f˚as via<br />

fun skaler (xr,y) = map (s y) xr<br />

Definer til sidst funktionen normer fra afsnit 7.4.3 ved hjælp af skaler. [Vink: Slut af med<br />

map round.]<br />

109


110


Kapitel 8<br />

Naïv sortering<br />

For at opn˚a lidt erfaring i at h˚andtere lister vil vi i dette kapitel arbejde med et klassisk problem<br />

inden for databehandling: sortering. At sortere en liste vil sige at omordne (permutere)<br />

den, s˚a elementerne kommer i voksende rækkefølge. (Hvis gentagelser skal være tilladt, m˚a<br />

man nøjes med at kræve ikke-aftagende rækkefølge.)<br />

Det kriterium, der sorteres efter, kan i princippet være en hvilken som helst ordningsrelation,<br />

og resultatet er entydigt fastlagt, hvis ordningen er total (i den forstand, at der for<br />

to vilk˚arlige elementer x og y enten gælder x < y, x = y eller x > y). Ofte vil elementerne<br />

være sammensatte værdier, og ordningen vil være fastlagt af en eller flere heltallige koder,<br />

der indg˚ar i disse værdier (s˚asom personnummer, kontonummer, dato, osv.), men andre eksempler<br />

er sortering af flydende talværdier (real), alfabetisk sortering af tegn (char) eller<br />

leksikografisk sortering af tekster (string).<br />

I eksempelprogrammerne vil vi g˚a ud fra, man skal sortere en liste af heltal, s˚a de bliver<br />

placeret efter ikke-aftagende størrelse.<br />

8.1 Naboombytning<br />

En simpel ide til ordning af en liste er at lede efter to naboelementer x og y, hvor x st˚ar lige<br />

foran y, men x > y, og bytte dem om, s˚a de kommer til at st˚a rigtigt.<br />

Funktionen bytvh undersøger alle s˚adanne par af naboer i en liste og ombytter dem, hvis<br />

de st˚ar forkert, idet den løber listen en gang igennem:<br />

fun bytvh (x :: y :: xr)<br />

= if x > y then y :: bytvh (x :: xr)<br />

else x :: bytvh (y :: xr)<br />

| bytvh xr = xr<br />

Mønsteret x :: y :: xr passer p˚a alle lister med mindst to elementer, s˚a funktionsdefinitionens<br />

sidste linje fanger lister af længde nul eller et (hvis elementer jo altid st˚ar rigtigt).<br />

Da funktionen sammenligner de to forreste elementer og eventuelt ombytter dem, før den<br />

rekursivt bearbejder den nye haleliste, kan man sige, at gennemløbet foretages fra venstre<br />

mod højre.<br />

Man kan ogs˚a skrive en ombytningsfunktion, som løber listen igennem fra højre mod<br />

venstre:<br />

111


fun bythv (x :: (xr as :: ))<br />

= let val y :: yr = bythv xr<br />

in if x > y then y :: x :: yr<br />

else x :: y :: yr<br />

end<br />

| bythv xr = xr<br />

Her har vi med synonymet :: sørget for, at parameteren xr ikke er tom, s˚a at<br />

x :: (xr as :: ) igen er en liste med mindst to elementer.<br />

For at sortere en liste er et enkelt naboombyttende gennemløb ikke tilstrækkeligt, men<br />

gentager man den slags gennemløb, vil listen før eller siden blive sorteret. Der er flere m˚ader<br />

at afgøre p˚a, hvor mange gennemløb der skal til:<br />

1. For en liste af længde n vil n − 1 gennemløb altid være tilstrækkeligt. (I værste fald<br />

flytter et gennemløb kun et forkert placeret element en enkelt plads, men intet element<br />

skal flyttes længere end fra den ene ende af listen til den anden.)<br />

2. I bytvh xr vil det største element i xr med sikkerhed være blevet placeret helt til<br />

højre (og i bythv xr vil det mindste element i xr være placeret helt til venstre). Man<br />

kunne derfor nøjes med at lade de fortsatte gennemløb virke p˚a den resterende liste<br />

uden det bageste (forreste) element.<br />

3. Hvis der i et gennemløb med bytvh (eller bythv) ingen ombytninger har været foretaget,<br />

st˚ar alle elementer rigtigt, og man behøver ikke at løbe flere gange igennem.<br />

Hver af de tre egenskaber kunne bruges som grundlag for programmering af en sorteringsfunktion,<br />

men det er instruktivt at se, hvordan vi kan basere os p˚a egenskab 3. Der er<br />

i s˚a fald brug for at udbygge bytvh (eller bythv) til funktioner, der ikke kun returnerer en<br />

liste, men ogs˚a en indikation af, hvorvidt der skete ombytninger under gennemløbet.<br />

Det kan gøres p˚a følgende m˚ade:<br />

fun bytvh2 (x :: y :: xr)<br />

= if x > y then (y :: #1 (bytvh2 (x :: xr)),true)<br />

else let val (yr,b) = bytvh2 (y :: xr)<br />

in (x :: yr,b) end<br />

| bytvh2 xr = (xr,false)<br />

fun bythv2 (x :: (xr as :: ))<br />

= let val (y :: yr,b) = bythv2 xr<br />

in if x > y then (y :: x :: yr,true)<br />

else (x :: y :: yr,b)<br />

end<br />

| bythv2 xr = (xr,false)<br />

Rent programmeringsteknisk er det værd at bemærke konstruktionerne i definitionen af<br />

bytvh2 (x :: y :: xr): Kald af funktionen returnerer et par; er der kun brug for første<br />

komponent af parret, kan vi udvælge den med #1; skal vi derimod bruge parret med en<br />

modificeret førstekomponent, pakkes det først ud til mønsteret (yr,b), hvorefter man kan<br />

returnere (x :: yr,b). (At konstruere denne sidstnævnte værdi<br />

(x :: #1 (bytvh2 (y :: xr)),#2 (bytvh2 (y :: xr))) ville indebære to kald af samme<br />

funktion med samme parametre og være urimelig ineffektivt.)<br />

112


8.2 Boblesortering<br />

Sorteringsmetoder, der baserer sig p˚a naboombytninger, kaldes boblesortering (eng.:bubble<br />

sort). Med bytvh2 til r˚adighed er det let at programmere s˚adan en funktion:<br />

fun boblesort xr = let val (yr,b) = bytvh2 xr<br />

in if b then boblesort yr else yr end<br />

(Da not b indebærer, at xr og yr er ens, kunne man lige s˚a godt have skrevet<br />

“else xr end” her til sidst.)<br />

I dette program kunne man have brugt bythv2 i steden for bytvh2, og i s˚a fald kunne<br />

man endda nøjes med at lade det rekursive kald virke p˚a den nye hale. (At programmere<br />

den beskrevne variant er stillet som en opgave.)<br />

Man kan vise, at boblesortering ikke er nogen god metode — det bedste ved den er<br />

faktisk navnet. Bedre sorteringsmetoder skal ombytte elementer, der ikke nødvendigvis er<br />

naboer, men kan st˚a længere fra hinanden. Vi slutter kapitlet af med omtale af to s˚adanne<br />

metoder, som dog heller ikke er optimale.<br />

8.3 Indsættelsessortering<br />

Ideen bag denne metode er at tage en listes elementer i betragtning et ad gangen og efter tur<br />

indsætte dem p˚a rette plads i dellisten af allerede sorterede elementer. Der bliver derfor brug<br />

for en hjælpefunktion, som kan indsætte et element i en liste af allerede ordnede elementer:<br />

(* I kald af formen indsaet (x,xr) skal xr allerede være sorteret *)<br />

fun indsaet (x,[]) = [x]<br />

| indsaet (x,xr as y :: yr)<br />

= if x > y then y :: indsaet (x,yr) else x :: xr<br />

Indsættelsessortering bygger p˚a denne hjælpefunktion:<br />

fun indssort [] = []<br />

| indssort (x :: xr) = indsaet (x,indssort xr)<br />

Undervejs i indsættelsessortering er den oprindelige liste delt op i en sorteret og en<br />

usorteret del, og elementer flyttes et ad gangen fra den usorterede til den sorterede del. Ved<br />

bare at tage det forreste af de usorterede elementer lægges størstedelen af arbejdet (det,<br />

indsaet foretager) i korrekt indplacering i den sorterede del.<br />

8.4 Udtagelsessortering<br />

Den i en vis forstand “modsatte” ide er at gøre indplaceringen i den sorterede del enkel p˚a<br />

bekostning af, at valget af “det rigtige” af de usorterede elementer kompliceres.<br />

Dette er netop ideen bag udtagelsessortering, som hele tiden stiller det mindste af de<br />

tilbageværende elementer forrest. For at programmere det bliver der brug for en funktion,<br />

der kan adskille en ikke-tom liste i sit mindste element og de øvrige elementer:<br />

113


fun splitmin [x] = (x,[])<br />

| splitmin (x :: xr)<br />

= let val (y,yr) = splitmin xr<br />

in if x > y then (y,x :: yr)<br />

else (x,xr)<br />

end<br />

Sprogsystemet vil advare os om, at definitionen af splitmin ikke dækker alle parametermønstre<br />

— hvilket er fuldstændig rigtigt, da man ikke kan angive det mindste element i<br />

en tom liste. Vi skal sørge for, at splitmin kun kaldes med ikke-tomme argumentlister:<br />

fun udtsort [] = []<br />

| udtsort xr = let val (y,yr) = splitmin xr in y :: udtsort yr end<br />

8.5 Sammenfatning<br />

At sortere en liste vil sige at permutere dens elementer, s˚a de opfylder en passende ordningsrelation.<br />

Korte lister kan med fordel sorteres med naive metoder, der blot ombytter<br />

elementer. I kapitel 12 vil vi møde metoder, der er effektive for lange lister.<br />

Bedre end boblesortering, som ombytter naboelementer, er indsættelsessortering og udtagelsessortering.<br />

8.6 Opgaver<br />

Opgave 8.1 Programmer en sortering, der virker efter følgende princip: Funktionen bythv<br />

kaldes igen og igen, men hver gang p˚a en liste, der kun er den nye hale af den forrige liste,<br />

idet det udnyttes, at bythv har bragt det mindste element p˚a sin rette plads.<br />

Skriv ogs˚a den variant af funktionen, som slutter af “i utide”, hvis der i et gennemløb<br />

ingen ombytninger er sket.<br />

Opgave 8.2 Programmer en sortering, der skiftevis løber listen igennem med bytvh og<br />

med bythv, idet den efter hvert gennemløb nøjes med at fortsætte med en liste, der er et<br />

element kortere, og alts˚a udnytter, at enten det nye forreste eller det nye bageste element nu<br />

st˚ar rigtigt. Denne variant af boblesortering er blevet kaldt “rystesortering” (eng.:cocktail<br />

shaker sort).<br />

Opgave 8.3 Programmer den variant af udtagelsessortering, hvor det hele tiden er det<br />

største element, der sættes bagest.<br />

114


Kapitel 9<br />

Et større eksempel: kalender<br />

Den tid, det tager jorden at rotere en gang omkring sig selv og igen vende samme punkt<br />

mod solen, g˚ar ikke et helt antal gange op i den tid, jorden er om at gennemløbe sin bane<br />

om solen; forholdet mellem de to tal er 365,24219.<br />

Ønsker man derfor, at ˚arstiderne skal falde p˚a de samme datoer hvert ˚ar, m˚a man ind<br />

mellem ˚ar p˚a 365 døgn skyde enkelte ˚ar med 366 døgn. Sammenhængen mellem et ˚ars datoer<br />

og ugedage kan derved falde ud p˚a 14 forskellige m˚ader, og da disse forskellige kalendere<br />

veksler i et uoverskueligt mønster, er der megen rimelighed i at konstruere et program, der<br />

kan generere kalenderen for et givet ˚arstal.<br />

Under operativsystemet Unix findes faktisk allerede et s˚adant program; det hedder cal<br />

og har (direkte p˚a operativsystem-niveau, alts˚a uden for mosml) et af de to kaldformer<br />

cal m˚aned ˚ar eller cal ˚ar. (Husk alle cifrene i ˚arstallet!)<br />

Vi vil sætte os for at konstruere en tilsvarende funktion i ML, men med m˚aneders og<br />

ugedages navne p˚a dansk og med mandag som ugens første dag (i overensstemmelse med<br />

den gældende standard). Med for eksempel 1998 som inddata ønsker vi p˚a skærmen at se<br />

noget i retning af<br />

januar 1998 februar 1998 marts 1998<br />

ma ti on to fr lø sø ma ti on to fr lø sø ma ti on to fr lø sø<br />

1 2 3 4 1 1<br />

5 6 7 8 9 10 11 2 3 4 5 6 7 8 2 3 4 5 6 7 8<br />

12 13 14 15 16 17 18 9 10 11 12 13 14 15 9 10 11 12 13 14 15<br />

19 20 21 22 23 24 25 16 17 18 19 20 21 22 16 17 18 19 20 21 22<br />

26 27 28 29 30 31 23 24 25 26 27 28 23 24 25 26 27 28 29<br />

30 31<br />

april 1998 maj 1998 juni 1998<br />

ma ti on to fr lø sø ma ti on to fr lø sø ma ti on to fr lø sø<br />

1 2 3 4 5 1 2 3 1 2 3 4 5 6 7<br />

6 7 8 9 10 11 12 4 5 6 7 8 9 10 8 9 10 11 12 13 14<br />

13 14 15 16 17 18 19 11 12 13 14 15 16 17 15 16 17 18 19 20 21<br />

og s˚a videre.<br />

Overordnet vil vi konstruere en funktion kalender, der til 1998 lader svare skærmbilledet<br />

som en lang tekst " januar 1998 februar 1998 . . ."; den ønskede opgave<br />

115


løses s˚a af et kald af formen print (kalender 1998);<br />

Det bliver nødvendigt at dele konstruktionen op i flere faser. Den tekst, vi skal ende<br />

med, er bygget op af 12 rektangulære blokke, der hver igen er bygget op af rektangulære<br />

delblokke. Vist skematisk er strukturen<br />

og s˚a videre<br />

Det vil derfor være nyttigt med nogle funktioner, der kan danne den slags rektangulære<br />

blokke af tegn og bygge dem sammen ved siden af og under hinanden.<br />

For at danne den billedblok, som viser en bestemt m˚aned, er der brug for fire grundoplysninger:<br />

• m˚anedens navn,<br />

• ˚arstallet (til at sætte lige bagefter m˚anedsnavnet),<br />

• hvilken ugedag den første i m˚aneden ligger og<br />

• antallet af dage i m˚aneden.<br />

Lad os kalde s˚adan et sæt af oplysninger for m˚anedsdata.<br />

Vi har nu analyseret os frem til tre delopgaver:<br />

• Ud fra ˚arstallet dannes sættet af m˚anedsdata for hver af de tolv m˚aneder.<br />

• Der skal være en funktion, som konstruerer billedblokken for en m˚aned ud fra m˚anedsdata.<br />

• Der skal være funktioner til at konstruere og sammensætte rektangulære billedblokke<br />

Af disse ingredienser kan funktionen kalender stykkes sammen. De efterfølgende afsnit<br />

vil løse delopgaverne og til sidst konstruere kalender.<br />

116


9.1 Rektangulære billeder<br />

Lad os indføre typen billede som en liste af linjer og lade en linje være en tekst.<br />

type linje = string;<br />

type linje = string<br />

type billede = linje list;<br />

type billede = string list<br />

De funktioner, vi nu skal bygge op, vil blive indrettet efter, at intet af tegnene i en linje vil<br />

være #"\n" (eller et tilsvarende tegn), og at alle linjer i et billede bliver lige lange. Desuden<br />

skal et billede mindst indeholde én linje. (Det er blandt andet s˚adanne begrænsninger, der<br />

gør det berettiget s˚adan at indføre nye navne for best˚aende typer – jævnfør opgave 9.2.)<br />

Et billede har en højde og en bredde<br />

fun hoejde (bi : billede) = length bi;<br />

val hoejde = fn : string list -> int<br />

fun bredde (bi : billede) = size (hd bi);<br />

val bredde = fn : string list -> int<br />

Vi vil kun arbejde med rektangulære billeder; alle linjer i et billede skal derfor være lige<br />

lange, og billedets bredde kan findes som længden af en hvilken som helst af disse linjer —<br />

funktionen bredde bruger første linje. Da vi forlangte, et billede ikke m˚atte være en tom<br />

liste af linjer, er dets bredde altid veldefineret.<br />

To billeder kan stilles oven over hinanden (hvis de er lige brede) eller ved siden af hinanden<br />

(hvis de er lige høje). Det første er let (man skal bare lade den ene liste af linjer efterfølge af<br />

den anden), men det andet kræver, at de to førstelinjer bliver sat i forlængelse af hinanden,<br />

at de to andenlinjer bliver sat i forlængelse af hinanden, og s˚a videre.<br />

fun over (bi1,bi2) = bi1 @ bi2 : billede;<br />

val over = fn : string list * string list -> string list<br />

fun vsiden ([],[]) = [] : billede<br />

| vsiden (li1 :: bi1,li2 :: bi2) = li1 ^ li2 :: vsiden (bi1,bi2);<br />

! Toplevel input:<br />

! ....vsiden ([],[]) = [] : billede<br />

! | vsiden (li1 :: bi1,li2 :: bi2) = li1 ^ li2 :: vsiden (bi1,bi2).<br />

! Warning: pattern matching is not exhaustive<br />

val vsiden = fn : string list * string list -> string list<br />

Advarslen minder os om, at vi ikke har taget stilling til, hvad der skal ske, hvis man forsøger<br />

at stille to ulige høje billeder ved siden af hinanden. Opgave 9.1 g˚ar ud p˚a at gøre over og<br />

vsiden robuste.<br />

Selv om et billede ikke m˚atte være tomt, har vi alligevel efter princippet om at tilstræbe<br />

enkelhed valgt at bruge et par af tomme lister som basistilfælde i definitionen af vsiden.<br />

(Basistilfældet skal s˚aledes blot bruges “internt”; funktionen vil aldrig udefra blive kaldt p˚a<br />

den m˚ade.)<br />

Der bliver ogs˚a brug for at stille flere end to billeder over eller ved siden af hinanden.<br />

Det kan naturligt opfattes som operationer p˚a lister:<br />

117


fun stabl [bi] = bi<br />

| stabl (bi :: bir) = over (bi,stabl bir);<br />

! Toplevel input:<br />

! ....stabl [bi] = bi<br />

! | stabl (bi :: bir) = over (bi,stabl bir).<br />

! Warning: pattern matching is not exhaustive<br />

val stabl = fn : string list list -> string list<br />

fun sidestil [bi] = bi<br />

| sidestil (bi :: bir) = vsiden (bi,sidestil bir);<br />

! Toplevel input:<br />

! ....sidestil [bi] = bi<br />

! | sidestil (bi :: bir) = vsiden (bi,sidestil bir).<br />

! Warning: pattern matching is not exhaustive<br />

val sidestil = fn : string list list -> string list<br />

Der vil senere blive brug for en generalisering af sidestil, som for en liste af lister af billeder<br />

kan sidestille hver af de indg˚aende lister. Selv om behovet ikke har vist sig endnu, er det<br />

naturligt at behandle problemet her. Funktionen skal alts˚a for en liste [bir1, bir2, ...] af<br />

billedlister danne listen [sidestil bir1, sidestil bir2, ...] af billeder. At programmere<br />

funktionen er lettere end at forklare, hvad den skal gøre:<br />

fun sidestilAlle [] = []<br />

| sidestilAlle (bir :: birr) = sidestil bir :: sidestilAlle birr;<br />

val sidestilAlle = fn : string list list list -> string list list<br />

Til sidst bliver der naturligvis brug for at omsætte et billede til en tekst, man kan se p˚a<br />

skærmen, og ogs˚a under indkøring af de forskellige hjælpefunktioner er det nyttigt at have<br />

s˚adan en funktion til r˚adighed. Lad os kalde den billedetiltekst; den skal simpelt hen<br />

lægge et linjeskift ind efter hver af billedets linjer:<br />

fun billedetiltekst [] =<br />

| billedetiltekst (li :: bi) = li ^ "\n"^ billedetiltekst bi;<br />

val billedetiltekst = fn : string list -> string<br />

Billeder opbygges en linje ad gangen. Lad teksttilbillede være navnet p˚a den operation,<br />

som konstruerer et grundbillede ud fra en enkelt tekstlinje:<br />

fun teksttilbillede s = [s] : billede;<br />

val teksttilbillede = fn : string -> string list<br />

For at kunne stille delbilleder op med lidt afstand imellem er det nyttigt at kunne danne<br />

et blankt billede og at kunne stille et givet billede op øverst til venstre — i det “nordvestlige<br />

hjørne” s˚a at sige — i et felt af given størrelse. Disse opgaver isoleres som selvstændige<br />

funktioner, idet vi f˚ar brug for en hjælpefunktion, der bygger en liste, hvor et forelagt<br />

element er gentaget et ønsket antal gange.<br />

118


fun gentag (n,x) = if n = 0 then [] else x :: gentag (n - 1,x)<br />

val gentag = fn : int * ’a -> ’a list<br />

fun mellemrum n = implode (gentag (n,#));<br />

val mellemrum = fn : int -> string<br />

fun blankbillede (h,b)<br />

= stabl (gentag (h,teksttilbillede (mellemrum b)));<br />

val blankbillede = fn : int * int -> string list<br />

fun nvstil (h,b,bi)<br />

= let val hbi = hoejde bi<br />

val bbi = bredde bi<br />

in over (vsiden (bi,blankbillede (hbi,b - bbi)),<br />

blankbillede (h - hbi,b))<br />

end;<br />

val nvstil = fn : int * int * string list -> string list<br />

9.2 Opdeling af en liste i dellister<br />

Den sidste funktion til billedh˚andtering skal ombryde en liste af billeder: i første omgang<br />

grupperet som en liste af rækker med et bestemt antal billeder i hver række, men derefter<br />

skal rækkerne stables oven p˚a hinanden til et enkelt stort billede.<br />

Lad os først betragte en simplere opgave: at opsplitte en liste i to dele. I biblioteksmodulet<br />

“List” ligger funktionerne<br />

take, drop: α list * int -> α list<br />

der henholdsvis udvælger og fjerner et opgivet antal elementer af en liste:<br />

List.take ([a1,. . .,ak−1,ak,ak+1,. . .,an],k) = [a1,. . .,ak−1,ak]<br />

List.drop ([a1,. . .,ak−1,ak,ak+1,. . .,an],k) = [ak+1,. . .,an]<br />

Hver af disse funktioner kræver naturligvis et gennemløb af argumentlisten. Lad os prøve<br />

at konstruere en funktion, som danner parret af de to dele af en liste i kun et enkelt gennemløb.<br />

Den ønskede funktion, som vi kan kalde splitAt, skal “splitte en liste efter et<br />

opgivet elementantal”, s˚adan at<br />

splitAt ([a1,. . .,ak−1,ak,ak+1,. . .,an],k) = ([a1,. . .,ak−1,ak],[ak+1,. . .,an])<br />

Vi har alts˚a splitAt (ℓ,k) = (List.take (ℓ,k),List.drop (ℓ,k)), men for at spare<br />

et gennemløb (og for at øve os i funktionskonstruktion) vil vi gerne konstruere splitAt<br />

direkte, uden at bruge take eller drop.<br />

Normalt skal naturligvis 0 ≤ k ≤ length(ℓ), n˚ar man kalder splitAt (ℓ,k), men for<br />

andre værdier af k virker det naturligt at sørge for at opretholde, at listen kan gendannes<br />

af sine to dele:<br />

Hvis splitAt (ℓ,k) = (xr,yr), da vil ℓ = xr @ yr<br />

Vi beslutter derfor, at for k < 0 skal splitAt (ℓ,k) = ([],ℓ), og for k > length (ℓ) skal<br />

splitAt (ℓ,k) = (ℓ,[]).<br />

119


For at funktionen skal kunne løse sin opgave i et enkelt gennemløb, skal værdien af<br />

splitAt (x::xr,. . .) kunne dannes ud fra splitAt (xr,. . .), og efter et øjebliks overvejelse<br />

indser man, at det kan ske ved at splitte en plads tidligere og derefter tilføje x forrest i venstre<br />

komponent af parret. (En analytisk brug af val, se afsnit 4.4.1.) Som program:<br />

fun splitAt ([], ) = ([],[])<br />

| splitAt (wr as x :: xr,n)<br />

= if n > 0 then let val (yr,zr) = splitAt (xr,n - 1)<br />

in (x :: yr,zr) end<br />

else ([],wr);<br />

Med splitAt til r˚adighed lader grupper sig let konstruere:<br />

fun grupper (xr,n) = if length xr > n<br />

then let val (yr,zr) = splitAt (xr,n)<br />

in yr :: grupper (zr,n) end<br />

else [xr];<br />

val grupper = fn : ’a list * int -> ’a list list<br />

fun ombryd (bir,n) = stabl (sidestilAlle (grupper (bir,n)));<br />

val ombryd = fn : string list list * int -> string list<br />

9.3 En enkelt m˚aneds billede<br />

Oven over listen af (forkortede) ugedagsnavne skal m˚anedens navn og ˚arstallet st˚a, og nedenunder<br />

datoskemaet. Til omsætning af ˚arstal og de enkelte datoer til tekst bruges tiltekst<br />

fra afsnit 7.4.2.<br />

Tilbage st˚ar konstruktionen af selve datoskemaet. Der bliver mindst brug for 4 og højst<br />

for 6 rækker, s˚a vi vil altid afsætte plads til 6, det vil sige bruge 42 felter. Hvert felt fylder<br />

tre positioner og skal enten være blankt eller indeholde et tal mellem 1 og m˚anedens længde,<br />

men man skal vide, hvilken ugedag m˚aneden begynder.<br />

Lad os kode de syv dage mandag, tirsdag, . . . , søndag ved tallene 1, 2, . . . , 7. Hvis koden<br />

for m˚anedens første dag er fd, g˚ar der fd − 1 blanke felter foran den første i m˚aneden. Man<br />

kan derfor opfatte tabellen som en ombrydning af de 42 tal fra 2 − fd til 43 − fd, idet tal,<br />

der ikke er relevante for den p˚agældende m˚aned, er maskeret som blanke felter (se figuren).<br />

Selve listen af tal fra 2 − fd til 43 − fd dannes med en hjælpefunktion interval for hvilken<br />

interval (m,n) = [m,m + 1,...,n − 1,n] hvis m ≤ n<br />

1<br />

2 3 4 5 6 7 8<br />

9 10 11 12 13 14 15<br />

16 17 18 19 20 21 22<br />

23 24 25 26 27 28 29<br />

30 31<br />

beregnes ud fra<br />

-5 -4 -3 -2 -1 0 1<br />

2 3 4 5 6 7 8<br />

9 10 11 12 13 14 15<br />

16 17 18 19 20 21 22<br />

23 24 25 26 27 28 29<br />

30 31 32 33 34 35 36<br />

fun interval (m,n) = if m > n then [] else m :: interval (m + 1,n)<br />

val interval = fn : int * int -> int list<br />

120


val ugedage = "ma ti on to fr lø sø";<br />

val ugedage = "ma ti on to fr lø sø": string<br />

fun tildatobi (mdlgd,n)<br />

= teksttilbillede (if n < 1 orelse n > mdlgd then " "<br />

else (if n < 10 then " "else )<br />

^ tiltekst n);<br />

val tildatobi = fn : int * int -> string list<br />

fun tildatobiAlle (mdlgd,[]) = []<br />

| tildatobiAlle (mdlgd,n :: nr)<br />

= tildatobi (mdlgd,n) :: tildatobiAlle (mdlgd,nr);<br />

val tildatbiAlle = fn : int * int list -> string list list<br />

fun datotabel (fd,mdlgd)<br />

= ombryd (tildatobiAlle (mdlgd,interval (2 - fd,43 - fd)),7);<br />

val datotabel = fn : int * int -> string list<br />

fun mdbillede (maaned,aar,fd,mdlgd)<br />

= nvstil (10,22,<br />

stabl [nvstil (2,21,<br />

teksttilbillede ( ^ maaned ^ ^ tiltekst aar)),<br />

teksttilbillede ugedage,<br />

datotabel (fd,mdlgd)]);<br />

val mdbillede = fn : string * int * int * int -> string list<br />

Som tidligere nævnt skal mdbillede bruges p˚a en liste af tolv m˚anedsdata. Derfor definerer<br />

vi<br />

fun mdbilledeAlle [] = []<br />

| mdbilledeAlle (q :: qr) = mdbillede q :: mdbilledeAlle qr;<br />

val mdbilledeAlle = fn : (string * int * int * int) list -> string list list<br />

9.4 Ugedag<br />

Vi valgte at kode mandag som 1, tirsdag som 2, og s˚a videre. I nogle af beregningerne bliver<br />

der brug for at finde koden for en ugedag, der ligger et vist antal dage d senere end ugedagen<br />

med en given kode k. Med nedenst˚aende definition kan den findes som ugedag (d + k). Vi<br />

benytter ogs˚a lejligheden til at generalisere til en liste af datoer:<br />

fun ugedag n = (n - 1) mod 7 + 1;<br />

val ugedag = fn : int -> int<br />

fun ugedagAlle [] = []<br />

| ugedagAlle (n :: nr) = ugedag n :: ugedagAlle nr;<br />

val ugedagAlle = fn : int list -> int list<br />

9.5 M˚anedsdata<br />

Den indledende analyse afklarede, at de enkelte m˚aneders billedblokke kunne dannes ud fra<br />

et kvadrupel med fire grundoplysninger: m˚anedens navn, ˚arstallet, ugedagen for den første<br />

i m˚aneden og antallet af dage i m˚aneden.<br />

121


Konstruktionerne i dette afsnit munder ud i en funktion<br />

mddata : int -> (string * int * int * int) list, s˚adan at hvis n er et ˚arstal, vil<br />

mddata n beregne de ønskede oplysninger i form af en liste med tolv kvadrupler (et for hver<br />

af de tolv m˚aneder).<br />

9.5.1 Skud˚ar<br />

P˚a Catos og Ciceros tid var der ingen regel for, hvorledes skud˚ar og almindelige ˚ar skulle<br />

følge hinanden; præsterne var kalenderforvaltere. I ˚ar 47 før vor tidsregning var kalender˚aret<br />

kommet 80 dage forud for solen. Julius Cæsar besluttede, at ˚ar 46 f.v.t. skulle indeholde de<br />

nødvendige ekstra dage, og at man fra ˚ar 45 f.v.t. skulle benytte reglen om, at hvert fjerde ˚ar<br />

var skud˚ar. Efter ham kaldes denne kalender for den “julianske”. Forskellen mellem 365,25<br />

og de i indledningen nævnte 365,24219 gjorde, at solen langsomt kom foran kalenderen. I<br />

det sekstende ˚arhundrede indtraf for˚arsjævndøgn 10 dage for tidligt, og pave Gregor XIII<br />

besluttede (med assistance fra astronomen Lilius), at man skulle udelade 10 dage af 1582<br />

og derefter udelade 3 skuddage i hver periode p˚a 400 ˚ar, idet ˚arstal delelige med 100 kun<br />

skulle være skud˚ar, hvis de ogs˚a kunne deles med 400. I Danmark indførtes den “gregorianske”<br />

kalender i ˚aret 1700, hvor man gik direkte fra den 18. februar til den 1. marts og<br />

s˚aledes udelod 11 dage (idet det jo ellers skulle have været et skud˚ar). Med den gregorianske<br />

korrektion bliver ˚aret gennemsnitligt p˚a 365,2425 døgn.<br />

For at beregne første januars ugedag et givet ˚ar er det bekvemt at g˚a ud fra ˚ar 1 (selv<br />

om man ikke brugte den gregorianske kalender p˚a det tidspunkt).<br />

I forhold til et s˚adant fiktivt ˚ar 1 vil hvert almindeligt ˚ar skubbe den første januar 1 dag<br />

(fordi 365 mod 7 = 1) og hvert skud˚ar skubbe den to dage. Det viser sig, at man skal regne<br />

den første januar ˚ar 1 som en mandag. Forud for et vist ˚ar aar vil antallet af ˚ar siden ˚ar 1<br />

være aar - 1, og af disse aar - 1 ˚ar vil (regnet efter vores gregorianske kalender)<br />

(aar - 1) div 4 - (aar - 1) div 100 + (aar - 1) div 400<br />

have været skud˚ar.<br />

Herudfra defineres funktionen jan1 til beregning af ugedagen for ˚arets første dag:<br />

fun skudaar aar = if aar mod 100 = 0 then aar mod 400 = 0<br />

else aar mod 4 = 0;<br />

val skudaar = fn : int -> bool<br />

fun jan1 aar<br />

= let val mandag = 1<br />

val antalaar = aar - 1<br />

val skud = antalaar div 4 - antalaar div 100<br />

+ antalaar div 400<br />

in ugedag (mandag + antalaar + skud) end;<br />

val jan1 = fn : int -> int<br />

9.5.2 De enkelte m˚aneder<br />

M˚anedernes navne kan vi bare skrive ned, og m˚anedernes længder kan beregnes, n˚ar man<br />

ved, hvorvidt der er tale om et skud˚ar:<br />

122


val mdnavne = ["januar","februar","marts","april","maj","juni","juli",<br />

"august","september","oktober","november","december"];<br />

val mdnavne =<br />

["januar", "februar", "marts", "april", "maj", "juni", "juli", "august",<br />

"september", "oktober", "november", "december"]<br />

: string list<br />

fun mdlgder aar = let val feb = if skudaar aar then 29 else 28<br />

in [31,feb,31,30,31,30,31,31,30,31,30,31] end;<br />

val mdlgder = fn : int -> int list<br />

˚Arstallet er forelagt, og tilbage af de fire m˚anedsdataelementer st˚ar herefter kun at<br />

beregne ugedagen for den første i hver m˚aned. Hvis den første januar falder p˚a ugedagen k,<br />

vil den første i de øvrige m˚aneder falde p˚a ugedag (d + k), hvor d (p˚a ˚ar, der ikke er skud˚ar)<br />

findes ved at sl˚a m˚aneden op i listen [0, 31, 31 + 28, 31 + 28 + 31, 31 + 28 + 31 + 30, . . .]. Den<br />

m˚ade, hvorp˚a denne liste kan beregnes ud fra listen [31, 28, 31, 30, . . .] af m˚anedslængder,<br />

følger et generelt princip, og i stedet for at løse problemet ad hoc vælger vi at programmere<br />

dette generelle princip:<br />

Initialsummer<br />

Lad os betragte den opgave at overføre en liste [a1, a2, a3, . . . , an] med n tal i til en liste med<br />

n + 1 delsummer: [0, a1, a1 + a2, a1 + a2 + a3, . . . , a1 + a2 + a3 + . . . + an].<br />

I konstruktion af en funktion, der tager en liste som argument, har vi hidtil ofte kunnet<br />

bruge den skabelon, hvor man dels fastlægger funktionens værdi med den tomme liste som<br />

argument, dels dens værdi for en liste med et hovede og en hale ud fra funktionsværdien af<br />

halen.<br />

I vores første forsøg initialsummer1 p˚a at løse opgaven følges denne skabelon. Har man<br />

allerede beregnet<br />

initialsummer1 [a2, a3, . . ., an] = [0, a2, a2 + a3, . . ., a2 + a3 + . . . + an], s˚a findes<br />

initialsummer [a1, a2, a3, . . ., an] ˚abenbart ved, at a1 adderes til alle elementer i<br />

listen, hvorefter et nyt 0 sættes allerforrest.<br />

Vi f˚ar alts˚a brug for en hjælpefunktion, der adderer en værdi til hvert element i en liste,<br />

og kan derefter programmere initialsummer1:<br />

fun flytAlle (m,[]) = []<br />

| flytAlle (m,n :: nr) = m + n :: flytAlle (m,nr);<br />

val flytAlle = fn : int * int list -> int list<br />

fun initialsummer1 [] = [0]<br />

| initialsummer1 (n :: nr) = 0 :: flytAlle (n,initialsummer1 nr);<br />

val initialsummer1 = fn : int list -> int list<br />

Selv om det konstruerede program er kompakt og elegant, afslører nærmere overvejelse<br />

en urimelig defekt: det er faktisk temmelig ineffektivt! Det er klart, at flytAlle (m,nr)<br />

m˚a foretage en addition for hvert element i listen nr. Kalder man derfor initialsummer1<br />

med en liste med n elementer som argument, vil flytAlle-kaldet p˚a højre side umiddelbart<br />

kræve n additioner, men hertil kommer de additioner, som for˚arsages af det indre rekursive<br />

kald af initialsummer1. Alt i alt fører det til n + (n − 1) + . . . + 3 + 2 + 1 additioner.<br />

123


P˚a sin vis er dette ikke s˚a mærkeligt, for det er (p˚anær de n additioner til 0) netop<br />

antallet af plus-tegn i resultatet [0, a1, a1 + a2, a1 + a2 + a3, . . . , a1 + a2 + a3 + . . . + an];<br />

problemet er bare, at det m˚a kunne gøres smartere: hver sum i listen burde kunne dannes<br />

ud fra den foreg˚aende ved blot en enkelt addition mere, s˚a man burde kunne klare sig med<br />

n additioner i alt.<br />

Funktionsværdien af lister af en vis længde skal dannes ud fra funktionsværdien af en<br />

kortere liste, men prøv at sammenligne det ønskede resultat med funktionsværdien af [a1 +<br />

a2, a3, . . . , an], som m˚a være [0, a1 + a2, a1 + a2 + a3, . . . , a1 + a2 + a3 + . . . + an]. Nu f˚as<br />

det ønskede resultat blot ved, at man heri “stikker a1 ind mellem forreste og næstforreste<br />

plads”.<br />

Vores næste forsøg initialsummer2 bygger p˚a denne ide og bruger kun n − 1 additioner<br />

p˚a at beregne sit resultat for et argument af længde n:<br />

fun initialsummer2 [] = [0]<br />

| initialsummer2 [n] = [0,n]<br />

| initialsummer2 (n1 :: n2 :: nr)<br />

= let val 0 :: mr = initialsummer2 (n1+n2 :: nr)<br />

in 0 :: n1 :: mr end;<br />

val initialsummer2 = fn : int list -> int list<br />

Man kunne ogs˚a sige, at ineffektiviteten i initialsummer1 bundede i, at additionen (via<br />

flytAlle) af et element til hver plads i listen var adskilt fra dannelsen af initialsummerne.<br />

Den effektive funktion initialsummer’ nedenfor kombinerer de to operationer. Sammenhængen<br />

er alts˚a<br />

initialsummer’ (m,nr) = flytAlle (m,initialsummer nr)<br />

men der er en simpel effektiv definition af initialsummer’, og initialsummer fremkommer<br />

som det specialtilfælde, hvor det er 0, man adderer:<br />

fun initialsummer’ (m,[]) = [m]<br />

| initialsummer’ (m,n :: nr) = m :: initialsummer’ (m+n,nr);<br />

val initialsummer’ = fn : int * int list -> int list<br />

fun initialsummer nr = initialsummer’ (0,nr);<br />

val initialsummer = fn : int list -> int list<br />

9.5.3 Opsamling af m˚anedsdata<br />

Ugedagene for hver den første i m˚aneden beregnes som nævnt af ugedag (k + d), hvor k<br />

er ugedagen for 1. januar, og d er hentet fra listen af initialsummer af m˚anedernes længder.<br />

Det klares netop af ugedagAlle (initialsummer’(k,lgdr )), hvor lgdr er længderne af<br />

˚arets første elleve m˚aneder (s˚a der bliver tolv initialsummer).<br />

Vi f˚ar med andre ord præcis brug for funktionen initialsummer’ fra foreg˚aende underafsnit,<br />

mens initialsummer, initialsummer1 og initialsummer2 ikke skal bruges her i<br />

kalenderprogrammet:<br />

124


fun md1er aar = let val lgdr = #1 (splitAt (mdlgder aar,11))<br />

in ugedagAlle (initialsummer’ (jan1 aar,lgdr))<br />

end;<br />

val md1er = fn : int -> int list<br />

Til sidst kan listen af de 12 m˚anedsdata dannes ved “sammenlyning” af fire lister, der<br />

alle har længde 12. Virkem˚aden for funktionen zip4 kan skitseres s˚aledes:<br />

zip4 ([a1, a2, . . ., an],[b1, b2, . . ., bn],[c1, c2, . . ., cn],[d1, d2, . . ., dn])<br />

Det er stillet som en opgave at konstruere zip4.<br />

= [(a1,b1,c1,d1), (a2,b2,c2,d2), . . ., (an,bn,cn,dn)]<br />

fun mddata aar = zip4 (mdnavne,gentag (12,aar),md1er aar,mdlgder aar);<br />

val mddata = fn : int -> (string * int * int * int) list<br />

9.6 Kalenderfunktionen<br />

I de foreg˚aende afsnit er arbejdet gjort — nu skal vi bare samle sammen: Ud fra ˚arstallet<br />

dannes listen af de 12 m˚anedsdata, p˚a hver af dem anvendes mdbillede, og listen af 12<br />

billeder brydes om med 3 i hver række:<br />

fun kalender aar<br />

= billedetiltekst (ombryd (mdbilledeAlle (mddata aar),3));<br />

val kalender = fn : int -> string<br />

print (kalender 1998);<br />

januar 1998 . . .<br />

. . .<br />

9.7 Hele programmet<br />

(* En linje best˚ar af grafiske tegn, herunder blanktegn *)<br />

type linje = string (* ej linjeskift, vognretur ell. lign. *)<br />

(* Et billede er en ikke tom liste af lige lange linjer *)<br />

type billede = linje list<br />

fun hoejde (bi : billede) = length bi<br />

fun bredde (bi : billede) = size (hd bi)<br />

(* over (bi1,bi2)<br />

= billedet bi1 placeret oven over billedet bi2,<br />

idet bi1 og bi2 skal være lige brede *)<br />

fun over (bi1,bi2) = bi1 @ bi2 : billede<br />

(* vsiden (bi1,bi2)<br />

= billedet bi1 placeret til venstre for billedet bi2,<br />

125


idet bi1 og bi2 skal være lige høje *)<br />

fun vsiden ([],[]) = [] : billede<br />

| vsiden (li1 :: bi1,li2 :: bi2) = li1 ^ li2 :: vsiden (bi1,bi2)<br />

(* stabl [bi1, bi2, bi3, ...]<br />

= over (bi1, over (bi2, over (bi3, ...)))<br />

En liste af lige brede billeder placeres oven over hinanden *)<br />

fun stabl [bi] = bi<br />

| stabl (bi :: bir) = over (bi,stabl bir)<br />

(* sidestil [bi1, bi2, bi3, ...]<br />

= vsiden (bi1, vsiden (bi2, vsiden (bi3, ...)))<br />

En liste af lige høje billeder placeres ved siden af hinanden *)<br />

fun sidestil [bi] = bi<br />

| sidestil (bi :: bir) = vsiden (bi,sidestil bir)<br />

(* sidestilAlle [bir1, bir2, bir3, ...]<br />

= [sidestil bir1, sidestil bir2, sidestil bir3, ...] *)<br />

fun sidestilAlle [] = []<br />

| sidestilAlle (bir :: birr) = sidestil bir :: sidestilAlle birr<br />

(* Et billede omformes til en tekst. De underforst˚aede<br />

linjeskift indsættes, og resultatet kan vises af print *)<br />

fun billedetiltekst [] = ""<br />

| billedetiltekst (li :: bi) = li ^ "\n" ^ billedetiltekst bi<br />

(* En tekst uden linjeskift, vognretur eller lignende<br />

omformes til et en-linjes-billede *)<br />

fun teksttilbillede (s : string) = [s] : billede<br />

(* gentag (n,x) = [x, x, x, ...] med n elementer i listen *)<br />

fun gentag (n,x) = if n = 0 then [] else x :: gentag (n - 1,x)<br />

(* mellemrum n = en tekst best˚aende af n blanktegn *)<br />

fun mellemrum n = implode (gentag (n,#" "))<br />

(* blankbillede (h,b)<br />

= et blankt billede med højde h og bredde b *)<br />

fun blankbillede (h,b)<br />

= stabl (gentag (h,teksttilbillede (mellemrum b)))<br />

(* nvstil (h,b,bi)<br />

= billedet bi udvidet til højde h og bredde b,<br />

idet bi placeres i øverste venstre hjørne *)<br />

fun nvstil (h,b,bi)<br />

= let val hbi = hoejde bi<br />

val bbi = bredde bi<br />

126


in over (vsiden (bi,blankbillede (hbi,b - bbi)),<br />

blankbillede (h - hbi,b))<br />

end<br />

(* splitAt (xr,n) = (yr,zr)<br />

hvor yr er de n forreste elementer af xr, og zr er resten *)<br />

fun splitAt ([],_) = ([],[])<br />

| splitAt (wr as x :: xr,n)<br />

= if n > 0 then let val (yr,zr) = splitAt (xr,n - 1)<br />

in (x :: yr,zr) end<br />

else ([],wr)<br />

(* grupper (xr,n) = [xr1, xr2, xr3, ...]<br />

hvor xr = xr1 @ xr2 @ xr3 @ ..., og delene har længde n *)<br />

fun grupper (xr,n) = if length xr > n<br />

then let val (yr,zr) = splitAt (xr,n)<br />

in yr :: grupper (zr,n) end<br />

else [xr]<br />

(* ombryd (bir,n)<br />

Af en liste bir af billeder dannes et nyt billede, idet<br />

de indkommende billeder sidestilles n ad gangen *)<br />

fun ombryd (bir,n) = stabl (sidestilAlle (grupper (bir,n)))<br />

val ugedage = " ma ti on to fr lø sø"<br />

(* interval (m,n) = [m, m+1, m+2, ..., n-1, n] *)<br />

fun interval (m,n) = if m > n then nil else m :: interval (m + 1,n)<br />

(* tilcifre n<br />

= listen af decimale enkelttegn i det positive heltal n *)<br />

fun tilcifre n = if n = 0 then []<br />

else tilcifre (n div 10) @ [chr (ord #"0" + n mod 10)]<br />

(* tiltekst n<br />

= det naturlige tal n som talord *)<br />

fun tiltekst n = if n = 0 then "0" else implode (tilcifre n)<br />

(* tildatobi (mdlgd,n)<br />

= den tretegnstekst, datoen n skal blive til<br />

i m˚aneder af længde mdlgd *)<br />

fun tildatobi (mdlgd,n)<br />

= teksttilbillede (if n < 1 orelse n > mdlgd then " "<br />

else (if n < 10 then " " else " ") ^ tiltekst n)<br />

(* tildatobiAlle (mdlgd,[n1, n2, ...])<br />

= [tildatobi (mdlgd,n1), tildatobi (mdlgd,n2), ...] *)<br />

fun tildatobiAlle (mdlgd,[]) = []<br />

127


| tildatobiAlle (mdlgd,n :: nr)<br />

= tildatobi (mdlgd,n) :: tildatobiAlle (mdlgd,nr)<br />

(* datotabel (fd,mdlgd)<br />

= selve datodelen for en m˚aned af længde mdlgd, hvis<br />

den første i m˚aneden falder p˚a dagen med kode fd *)<br />

fun datotabel (fd,mdlgd)<br />

= ombryd (tildatobiAlle (mdlgd,interval (2 - fd,43 - fd)),7)<br />

(* mdbillede maanedsdata<br />

= billedet (med højde 10 og bredde 22) af de<br />

som argument givne maanedsdata *)<br />

fun mdbillede (maaned,aar,fd,mdlgd)<br />

= nvstil (10,22,<br />

stabl [nvstil (2,21,<br />

teksttilbillede (" " ^ maaned ^ " " ^ tiltekst aar)),<br />

teksttilbillede ugedage,<br />

datotabel (fd,mdlgd)])<br />

(* mdbilledeAlle<br />

vil for en liste af maanedsdata danne den tilsvarende<br />

liste af billeder *)<br />

fun mdbilledeAlle [] = []<br />

| mdbilledeAlle (q :: qs) = mdbillede q :: mdbilledeAlle qs<br />

(* Ugens dage kodes mandag = 1, tirsdag = 2, ..., søndag = 7 *)<br />

fun ugedag n = (n - 1) mod 7 + 1<br />

fun ugedagAlle [] = []<br />

| ugedagAlle (n :: nr) = ugedag n :: ugedagAlle nr<br />

(* skudaar ˚ar<br />

hvis og kun hvis ˚ar er skud˚ar efter den<br />

gregorianske kalender *)<br />

fun skudaar aar = aar mod 4 = 0 andalso aar mod 100 0<br />

orelse aar mod 400 = 0<br />

(* jan1<br />

giver ugedagskoden for 1. januar<br />

i det som argument anførte ˚ar *)<br />

fun jan1 aar<br />

= let val mandag = 1 (* 1. januar ˚ar 1 skal være en mandag *)<br />

val antalaar = aar - 1<br />

val skud = antalaar div 4 - antalaar div 100<br />

+ antalaar div 400<br />

in ugedag (mandag + antalaar + skud) end<br />

val mdnavne = ["januar","februar","marts","april","maj","juni","juli",<br />

"august","september","oktober","november","december"]<br />

128


fun mdlgder aar = let val feb = if skudaar aar then 29 else 28<br />

in [31,feb,31,30,31,30,31,31,30,31,30,31] end<br />

(* initialsummer’ (m,[n1, n2, n3, ...])<br />

= [m, m+n1, m+n1+n2, m+n1+n2+n3, ...] *)<br />

fun initialsummer’ (m,[]) = [m]<br />

| initialsummer’ (m,n :: nr) = m :: initialsummer’ (m+n,nr)<br />

(* md1er<br />

beregner listen af ugedagskoder for den første i hver<br />

af de 12 m˚aneder af det som argument angivne ˚ar *)<br />

fun md1er aar = let val lgdr = #1 (splitAt (mdlgder aar,11))<br />

in ugedagAlle (initialsummer’ (jan1 aar,lgdr))<br />

end<br />

(* zip4 ([a1, a2, a3, ...],<br />

[b1, b2, b3, ...],<br />

[c1, c2, c3, ...],<br />

[d1, d2, d3, ...])<br />

= [(a1,b1,c1,d1), (a2,b2,c2,d2), (a3,b3,c3,d3), ...] *)<br />

fun zip4 ... (* Opgave 9.3 *)<br />

(* mddata<br />

beregner listen af m˚anedsdata for de 12 m˚aneder<br />

af det som argument angivne ˚ar *)<br />

fun mddata aar = zip4 (mdnavne,gentag (12,aar),md1er aar,mdlgder aar)<br />

(* kalender : int -> string *)<br />

fun kalender aar<br />

= billedetiltekst (ombryd (mdbilledeAlle (mddata aar),3));<br />

9.8 Udnyttelse af listefunktionalen map<br />

Lad os udnytte, at flere af de konstruerede funktioner mere eller mindre følger skabelonen<br />

(7.1), til at præsentere programmet mere kompakt.<br />

De funktioner, som burde kunne undværes, er<br />

sidestilAlle<br />

tildatobiAlle<br />

mdbilledeAlle<br />

ugedagAlle<br />

For tre af disse funktioner er der en direkte sammenhæng<br />

val sidestilAlle = map sidestil<br />

val mdbilledeAlle = map mdbillede<br />

val ugedagAlle = map ugedag<br />

129


men tildatobi er b˚ade en funktion af m˚anedens længde og af den dato, som skal vises.<br />

Rent syntaktisk ville der ikke være noget i vejen for at konstruere map tildatobi, men da<br />

tildatobi har type int * int -> string list, vil<br />

map tildatobi : (int * int) list -> string list list blive en funktion, der som<br />

argument skal have en liste af par (hvor hvert par best˚ar af en m˚anedslængde og en dato)<br />

— og ikke, som vi har brug for det, et par best˚aende af en m˚anedslængde og en liste af<br />

datoer.<br />

Det ønskede kan opn˚as ved at definere en sekventialiseret funktion<br />

tildatobi’ mdlgd n = tildatobi (mdlgd,n)<br />

fun tildatobi’ mdlgd n<br />

= teksttilbillede (if n < 1 orelse n > mdlgd then<br />

else (if n < 10 then else ) ^ tiltekst n);<br />

val tildatobi’ = fn : int -> int -> string list<br />

fun tildatobiAlle’ mdlgd = map (tildatobi’ mdlgd)<br />

val tildatobiAlle’ = fn : int -> int list -> string list list<br />

Bemærk, at tildatobiAlle’ nu ogs˚a er en sekventialiseret funktion, s˚a det oprindelige<br />

kald<br />

tildatobiAlle (mdlgd,interval (2 - fd,43 - fd))<br />

nu skal erstattes af<br />

tildatobiAlle’ mdlgd (interval (2 - fd,43 - fd))<br />

I næste afsnit samles de beskrevne omformninger, og vi viser hele programmet i komprimeret<br />

form uden kommentarer.<br />

9.9 Hele programmet p˚a kompakt form<br />

type linje = string<br />

type billede = linje list<br />

fun hoejde (bi : billede) = length bi<br />

fun bredde (bi : billede) = size (hd bi)<br />

fun over (bi1,bi2) = bi1 @ bi2 : billede<br />

fun vsiden ([],[]) = [] : billede<br />

| vsiden (li1 :: bi1,li2 :: bi2) = li1 ^ li2 :: vsiden (bi1,bi2)<br />

fun stabl [bi] = bi<br />

| stabl (bi :: bir) = over (bi,stabl bir)<br />

fun sidestil [bi] = bi<br />

| sidestil (bi :: bir) = vsiden (bi,sidestil bir)<br />

fun billedetiltekst [] = ""<br />

| billedetiltekst (li :: bi) = li ^ "\n" ^ billedetiltekst bi<br />

fun teksttilbillede (s : string) = [s] : billede<br />

fun gentag (n,x) = if n = 0 then [] else x :: gentag (n - 1,x)<br />

fun mellemrum n = implode (gentag (n,#" "))<br />

130


fun blankbillede (h,b)<br />

= stabl (gentag (h,teksttilbillede (mellemrum b)))<br />

fun nvstil (h,b,bi)<br />

= let val hbi = hoejde bi<br />

val bbi = bredde bi<br />

in over (vsiden (bi,blankbillede (hbi,b - bbi)),<br />

blankbillede (h - hbi,b))<br />

end<br />

fun splitAt ([],_) = ([],[])<br />

| splitAt (wr as x :: xr,n)<br />

= if n > 0 then let val (yr,zr) = splitAt (xr,n - 1)<br />

in (x :: yr,zr) end<br />

else ([],wr)<br />

fun grupper (xr,n) = if length xr > n<br />

then let val (yr,zr) = splitAt (xr,n)<br />

in yr :: grupper (zr,n) end<br />

else [xr]<br />

fun ombryd (bir,n) = stabl (map sidestil (grupper (bir,n)))<br />

val ugedage = " ma ti on to fr lø sø"<br />

fun interval (m,n) = if m > n then nil else m :: interval (m + 1,n)<br />

fun tilcifre n = if n = 0 then []<br />

else tilcifre (n div 10) @ [chr (ord #"0" + n mod 10)]<br />

fun tiltekst n = if n = 0 then "0" else implode (tilcifre n)<br />

fun tildatobi’ mdlgd n<br />

= teksttilbillede (if n < 1 orelse n > mdlgd then " "<br />

else (if n < 10 then " " else " ") ^ tiltekst n)<br />

fun datotabel (fd,mdlgd)<br />

= ombryd (map (tildatobi’ mdlgd) (interval (2 - fd,43 - fd)),7)<br />

fun mdbillede (maaned,aar,fd,mdlgd)<br />

= nvstil (10,22,<br />

stabl [nvstil (2,21,<br />

teksttilbillede (" " ^ maaned ^ " " ^ tiltekst aar)),<br />

teksttilbillede ugedage,<br />

datotabel (fd,mdlgd)])<br />

fun ugedag n = (n - 1) mod 7 + 1<br />

fun skudaar aar = aar mod 4 = 0 andalso aar mod 100 0<br />

orelse aar mod 400 = 0<br />

fun jan1 aar<br />

= let val mandag = 1<br />

val antalaar = aar - 1<br />

val skud = antalaar div 4 - antalaar div 100<br />

+ antalaar div 400<br />

in ugedag (mandag + antalaar + skud) end<br />

val mdnavne = ["januar","februar","marts","april","maj","juni","juli",<br />

"august","september","oktober","november","december"]<br />

131


fun mdlgder aar = let val feb = if skudaar aar then 29 else 28<br />

in [31,feb,31,30,31,30,31,31,30,31,30,31] end<br />

fun initialsummer’ (m,[]) = [m]<br />

| initialsummer’ (m,n :: nr) = m :: initialsummer’ (m+n,nr)<br />

fun md1er aar = let val lgdr = #1 (splitAt (mdlgder aar,11))<br />

in map ugedag (initialsummer’ (jan1 aar,lgdr))<br />

end<br />

fun zip4 ... (* Opgave 9.3 *)<br />

fun mddata aar = zip4 (mdnavne,gentag (12,aar),md1er aar,mdlgder aar)<br />

fun kalender aar<br />

= billedetiltekst (ombryd (map mdbillede (mddata aar),3));<br />

9.10 Opgaver<br />

Opgave 9.1 Gør over og vsiden robuste, s˚adan at over kaster en undtagelse, hvis billederne<br />

ikke er lige brede, og vsiden kaster en undtagelse, hvis de ikke er lige høje.<br />

Opgave 9.2 Ud over, at de skulle tjene som forkortelser, var ideen med at indføre typenavnene<br />

linje og billede ogs˚a, at de skulle markere nogle særlige begrænsninger: En værdi<br />

af type string kunne kun bruges som linje, hvis alle dens tegn havde grafisk fremtræden<br />

(opfyldte Char.isPrint). En værdi af type linje list kunne kun bruges som billede,<br />

hvis den ikke var tom, og alle dens linjer var lige lange.<br />

Sørg for, at alle de hjælpefunktioner i kalenderprogrammet, som bygger værdier af type<br />

linje og af type billede, overholder disse begrænsninger (idet det samtidig kan antages,<br />

at eventuelle parametre af type linje eller billede overholder begrænsningerne). [Vink:<br />

Brug biblioteksfunktionen List.all.]<br />

Opgave 9.3 Konstruer zip4.<br />

Opgave 9.4 Skriv en funktion, hvis virkning er den samme som ombryd, blot transponeret:<br />

Med en liste af billeder og et tal n som parameter skal den stable billederne fra listen n ad<br />

gangen oven p˚a hinanden og derefter sidestille disse stable.<br />

Opgave 9.5 Skriv en udgave af kalenderfunktionen, hvor den enkelte m˚aneds datoer opstilles<br />

søjlevis:<br />

januar 1998 februar 1998 marts 1998<br />

ma 5 12 19 26 ma 2 9 16 23 ma 2 9 16 23 30<br />

ti 6 13 20 27 ti 3 10 17 24 ti 3 10 17 24 31<br />

on 7 14 21 28 on 4 11 18 25 on 4 11 18 25<br />

to 1 8 15 22 29 to 5 12 19 26 to 5 12 19 26<br />

fr 2 9 16 23 30 fr 6 13 20 27 fr 6 13 20 27<br />

lø 3 10 17 24 31 lø 7 14 21 28 lø 7 14 21 28<br />

sø 4 11 18 25 sø 1 8 15 22 sø 1 8 15 22 29<br />

132


Opgave 9.6 I henhold til Encyclopedia Britannica er skud˚arsreglen i den gregorianske<br />

kalender for nyligt revideret, s˚a ˚ar, hvis nummer er deleligt med 4000, ikke skal være skud˚ar.<br />

Dette forbedrer gennemsnits˚arets 365,2425 døgn til 365,24225 døgn, hvilket tilnærmer den<br />

eksakte værdi 365,24219 tilstrækkelig godt til ethvert praktisk form˚al. Modificer programmet,<br />

s˚a det tager hensyn til denne revision.<br />

Opgave 9.7 Hver uge har 7 dage, der strækker sig fra mandag til søndag, og uger nummereres<br />

efter deres midterste dag, det vil sige torsdag. ˚Arets n-te uge er med andre ord den<br />

uge, som strækker sig fra tre dage før til tre dage efter ˚arets n-te torsdag (uanset, om disse<br />

dage alle tilhører samme ˚arstal).<br />

Skriv et program, som for en opgiven dato beregner dens ugedag og ugenummer.<br />

133


134


Kapitel 10<br />

Kombinatorisk søgning<br />

Nogle problemers løsning kan beskrives som resultat af en serie trufne valg. Man kan da<br />

tænke p˚a problemet som en form for “spil”: I hver “stilling” (tilstand, konfiguration) er<br />

der nogle mulige “træk”, som hver fører til en ny stilling, og fra en forelagt udgangsstilling<br />

gælder det om at bestemme en sekvens af træk, der fører til en ønsket slutstilling.<br />

Problemer af denne type kan illustreres som i figur 10.1 af en graf, hvor stillingerne er<br />

knuder og trækkene er pile. (Hvis samme stilling kan n˚as ad flere forskellige sekvenser af<br />

træk, vil grafen ikke som p˚a figuren være et træ.)<br />

Her er nogle af de opgavetyper, som kan formuleres inden for denne ramme:<br />

• Kan en slutstilling n˚as ad en sekvens af træk?<br />

• Angiv en sekvens af træk, der fører til en slutstilling.<br />

• Find den bedste (efter et eller andet nærmere specificeret kriterium) slutstilling, som<br />

kan n˚as.<br />

• Angiv antallet af opn˚aelige slutstillinger.<br />

• Opregn samtlige løsninger p˚a problemet.<br />

<br />

✑<br />

✑ ✁❆◗<br />

◗◗<br />

✑✰ ✁☛ ❆❯ <br />

<br />

❅<br />

✠ ❄ ❄ ❄ ❄<br />

❅❘ <br />

❄ ❄ ❄ ❄<br />

❄ ❄<br />

Figur 10.1: Illustration af kombinatorisk søgning.<br />

10.1 Det gr˚adige princip<br />

Et af de problemer, som falder under den beskrevne ramme, er “pengevekslingsproblemet”,<br />

hvor et beløb skal betales med mønterne inden for et bestemt møntsystem.<br />

135


Lad os skrive en funktion, der med et beløb som argument beregner de danske mønter,<br />

det skal betales med, n˚ar der skal bruges s˚a f˚a mønter som muligt. For en pris p˚a for eksempel<br />

14,75 kr. skal svaret være en tikrone, to tokroner, en 50-øre og en 25-øre.<br />

Det m˚a være hensigtsmæssigt at prøve de største møntværdier først, og efterh˚anden som<br />

det bliver besluttet, hvilke mønter der skal bruges, reduceres det beløb, som mangler at blive<br />

betalt. Lad os derfor skrive en funktion, der holder styr p˚a to ting: en liste mntr af mulige<br />

møntværdier (i øre) i faldende orden og det beløb blb, som skal dannes. Hvis beløbet er 0,<br />

skal ingen mønter bruges. Hvis møntsystemets største mønt er større end beløbet, kan den<br />

ikke bruges, men ellers skal den bruges, og beløbet kan reduceres tilsvarende:<br />

fun betalG ( ,0) = []<br />

| betalG (mntr1 as mnt :: mntr,blb)<br />

= if mnt > blb then betalG (mntr,blb)<br />

else mnt :: betalG (mntr1,blb - mnt);<br />

! Toplevel input:<br />

! ....betalG ( ,0) = []<br />

! | betalG (mntr1 as mnt :: mntr,blb)<br />

! = if mnt > blb then betalG (mntr,blb)<br />

! else mnt :: betalG (mntr1,blb - mnt).<br />

! Warning: pattern matching is not exhaustive<br />

val betalG = fn : int list * int -> int list<br />

val DKmntr = [2000, 1000, 500, 200, 100, 50, 25];<br />

val DKmntr = [2000, 1000, 500, 200, 100, 50, 25] : int list<br />

betalG (DKmntr,1475);<br />

val it = [1000, 200, 200, 50, 25] : int list<br />

I det rekursive tilfælde vælger betingelsen mnt > blb mellem to delproblemer, som begge<br />

er simplere end det oprindelige problem: enten er der færre møntstørrelser at vælge imellem,<br />

eller ogs˚a er der et mindre restbeløb at betale.<br />

N˚ar en møntstørrelse kan bruges, bliver den det ogs˚a; den m˚ade at løse et problem p˚a:<br />

at vælge et træk, blot det er muligt, kaldes “det gr˚adige princip”.<br />

Det gr˚adige princip fungerer ikke altid! Lad os forestille os, man kun har femmere og<br />

tokroner til r˚adighed og skal betale 11 kr.<br />

betalG ([500,200],1100);<br />

! Uncaught exception:<br />

! Match<br />

Programmet giver op, selv om man kunne betale elleve kroner med en femmer og tre tokroner.<br />

Læg mærke til, at systemet allerede under oversættelsen med “pattern matching is<br />

not exhaustive” advarede os om muligheden for, at beregninger kunne kaste undtagelsen<br />

“Match”.<br />

136


10.2 Alle løsninger<br />

Det gik galt, fordi betalingsfunktionen ikke prøvede andre møntstørrelser end “den første<br />

den bedste”. Ønsker man at f˚a den løsning med, hvor 1100 dannes som 500+200+200+200,<br />

er en af mulighederne at programmere en funktion, der danner samtlige mulige m˚ader at<br />

betale et beløb p˚a. Det indebærer blandt andet, at selv om en bestemt møntstørrelse kan<br />

bruges, s˚a skal funktionen b˚ade prøve at bruge den og ikke at gøre det.<br />

Hvor svaret før var en betaling i form af en liste af møntværdier, skal funktionsværdien<br />

nu være en liste af den slags lister. Hvis et problem ingen løsning har, f˚as en tom liste;<br />

har det netop én løsning, bliver funktionsværdien en liste med denne ene løsningsliste som<br />

element, men i almindelighed returneres alts˚a en liste af løsningslister.<br />

N˚ar det var beregnet, at en bestemt møntstørrelse mnt skulle bruges, kunne man tidligere<br />

blot med mnt :: delresultat sætte den foran det simplere delresultat. Nu, hvor et delresultat<br />

er en liste af lister, bliver der brug for en hjælpefunktion foranAlle, som sætter mnt foran<br />

hver enkelt liste i denne liste af lister.<br />

fun foranAlle [] = []<br />

| foranAlle x (xr :: xrr) = (x :: xr) :: foranAlle x xrr;<br />

val ’a foranAlle = fn : ’a -> ’a list list -> ’a list list<br />

fun betalA ( ,0) = [[]]<br />

| betalA ([], ) = []<br />

| betalA (mntr1 as mnt :: mntr,blb)<br />

= if mnt > blb then betalA (mntr,blb)<br />

else foranAlle mnt (betalA (mntr1,blb - mnt))<br />

@ betalA (mntr,blb);<br />

val betalA = fn : int list * int -> int list list<br />

betalA ([500,200],1100);<br />

val it = [[500, 200, 200, 200]] : int list list<br />

betalA (DKmntr,100);<br />

val it = [[100], [50, 50], [50, 25, 25], [25, 25, 25, 25]] : int list list<br />

Vi kan ogs˚a prøve det oprindelige eksempel<br />

betalA (DKmntr,1475);<br />

val it =<br />

[[1000, 200, 200, 50, 25], [1000, 200, 200, 25, 25, 25],<br />

[1000, 200, 100, 100, 50, 25], [1000, 200, 100, 100, 25, 25, 25],<br />

[1000, 200, 100, 50, 50, 50, 25], [1000, 200, 100, 50, 50, 25, 25, 25],<br />

[1000, 200, 100, 50, 25, 25, 25, 25, 25],<br />

...<br />

[500, 200, 100, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 25, 25,<br />

25], ...] : int list list<br />

length it;<br />

val it = 1082 : int<br />

Efter et øjebliks tøven fyldes skærmen med tallister, og kun de første 200 løsninger 1 vises<br />

1 Hvor mange elementer af en liste der skal vises, kan man selv styre med standardvariablen printLength,<br />

se [13, afsnit 3.4].<br />

137


af Moscow ML, men resultatet opbevares inde i systemet, og vi kan f˚a at vide, at der er<br />

1082 løsninger i alt.<br />

Omskrivninger Rent programmeringsteknisk er det utilfredsstillende, at deludtrykket<br />

betalA (mntr,blb) forekommer to gange:<br />

fun betalA1 ( ,0) = [[]]<br />

| betalA1 ([], ) = []<br />

| betalA1 (mntr1 as mnt :: mntr,blb)<br />

= (if blb < mnt then []<br />

else foranAlle mnt (betalA1 (mntr1,blb - mnt)))<br />

@ betalA1 (mntr,blb);<br />

Her sikrer betingelsen blb < mnt, at subtraktionen blb - mnt ikke giver et negativt resultat,<br />

s˚a betalA1 kunne blive bedt om at betale et negativt beløb. Vi kunne imidlertid ogs˚a<br />

vælge at tillade et negativt beløbsargument, som s˚a bare skulle resultere i tom løsningsliste:<br />

fun betalA2( ,0) = [[]]<br />

| betalA2 ([], ) = []<br />

| betalA2 (mntr1 as mnt :: mntr,blb)<br />

= if blb < 0 then []<br />

else foranAlle mnt (betalA2 (mntr1,blb - mnt))<br />

@ betalA2 (mntr,blb)<br />

Opsamles de foransatte mønter i en ekstra parameter, overflødiggøres foranAlle: (Da<br />

senest valgte mønter sættes forrest i denne liste, spejlvendes løsningerne nu i forhold til den<br />

m˚ade, tidligere funktioner præsenterede dem p˚a.)<br />

fun betalA’ (bmntr, ,0) = [bmntr]<br />

| betalA’ ( ,[], ) = []<br />

| betalA’ (bmntr,mntr1 as mnt :: mntr,blb)<br />

= if blb < 0 then []<br />

else betalA’ (mnt :: bmntr,mntr1,blb - mnt)<br />

@ betalA’ (bmntr,mntr,blb)<br />

fun betalA3 (mntr,blb) = betalA’ ([],mntr,blb);<br />

val betalA’ = fn : int list * int list * int -> int list list<br />

val betalA3 = fn : int list * int -> int list list<br />

betalA3 (DKmntr,125);<br />

val it = [[25, 100], [25, 50, 50], [25, 25, 25, 50], [25, 25, 25, 25, 25]] :<br />

int list list<br />

10.3 Kaste og gribe undtagelser<br />

I tillæg til de undtagelser, som p˚a forh˚and er defineret i systemet, kan man konstruere sine<br />

egne. Før en undtagelse kan bruges, skal den erklæres med en definition af formen<br />

exception undtagelsesnavn<br />

138


og i de tilfælde, hvor den skal kastes, skriver man s˚a<br />

raise undtagelsesnavn<br />

Undtagelser kan ogs˚a have parametre, se [5, Sect. 7.6] og [12, Sect. 4.6].<br />

Ved selv at gribe (fange, h˚andtere) en undtagelse kan man forhindre den reaktion p˚a<br />

undtagelser, vi har set indtil nu, hvor de afbryder den igangværende beregning med en<br />

fejlmelding “Uncaught exception: . . . ”. I sprogkonstruktionen<br />

exp handle match<br />

hvor match har formen pat 1 => exp 1 | . . . | pat n => exp n, vil eventuelle undtagelser kastet<br />

af exp bliver sammenholdt med mønstrene i match. Hvis exp i er det første mønster, som<br />

passer, erstattes hele konstruktionen af det tilsvarende udtryk exp i til højre for dobbeltpilen.<br />

Hvis intet mønster passer, propageres undtagelsen videre ud.<br />

Som eksempel kan vi lade gr˚adige løsninger udskrive som tekst:<br />

load "Int";<br />

val it = () : unit<br />

fun vistalliste [] =<br />

| vistalliste [n] = Int.toString n<br />

| vistalliste (n :: nr)<br />

= Int.toString n ^ ", "^ vistalliste nr;<br />

val vistalliste = fn : int list -> string<br />

fun visbetaling (mntr,blb)<br />

= "I møntsystemet\n"<br />

^ vistalliste mntr ^ "\n"<br />

^ "kan "^ Int.toString (blb div 100) ^ "kr. og "<br />

^ Int.toString (blb mod 100) ^ "øre "<br />

^ ("gr˚adigt betales som\n"^ vistalliste (betalG (mntr,blb))<br />

handle Match => "ikke betales gr˚adigt") ^ "\n";<br />

val visbetaling = fn : int list * int -> string<br />

print (visbetaling (DKmntr,1425));<br />

I møntsystemet<br />

2000, 1000, 500, 200, 100, 50, 25<br />

kan 14 kr. og 25 øre gr˚adigt betales som<br />

1000, 200, 200, 25<br />

val it = () : unit<br />

print (visbetaling ([500,200],1100));<br />

I møntsystemet<br />

500, 200<br />

kan 11 kr. og 0 øre ikke betales gr˚adigt<br />

val it = () : unit<br />

10.4 Blindgydesøgning<br />

Hvor det gr˚adige princip ikke kunne bruges, s˚a vi, at man i stedet kunne generere (listen<br />

af) alle løsninger. Er man kun interesseret i en enkelt løsning, synes det imidlertid at være<br />

139


spild af ressourcer. Ganske vist vil man f˚a udvalgt den forreste løsning ved at skrive<br />

hd (betalA3 (møntsystem,beløb))<br />

men i et sprog som Standard ML er herved ikke sparet noget: P˚a grund af den strikse<br />

argumentoverføring beregnes hele listen, før dens hoved udtages.<br />

Det, der er brug for, er muligheden for at “prøve sig frem”: man skal kunne træffe et valg,<br />

men hvis det viser sig ikke at føre til en brugbar løsning, skal det senere kunne gøres om.<br />

Denne strategi, som passende kunne kaldes blindgydesøgning (eng.:backtrack programming),<br />

er let at programmere, n˚ar man kan kaste og gribe undtagelser:<br />

exception Ubetaleligt;<br />

exn Ubetaleligt = Ubetaleligt : exn<br />

fun betalB ( ,0) = []<br />

| betalB ([], ) = raise Ubetaleligt<br />

| betalB (mntr1 as mnt :: mntr,blb)<br />

= if blb < 0 then raise Ubetaleligt<br />

else mnt :: betalB (mntr1,blb - mnt)<br />

handle<br />

Ubetaleligt => betalB (mntr,blb);<br />

val betalB = fn : int list * int -> int list<br />

betalB (DKmntr,1425);<br />

val it = [1000, 200, 200, 25] : int list<br />

betalB ([5,2],11);<br />

val it = [5, 2, 2, 2] : int list<br />

betalB ([5,2],3);<br />

! Uncaught exception:<br />

! Ubetaleligt<br />

Vi prøver først at medtage møntsystemets største mønt<br />

mnt :: betalB (mntr1,blb - mnt), men hvis det ikke fører til en løsning, prøves den<br />

anden mulighed: ikke at medtage denne mønt betalB (mntr,blb). Kun hvis slet ingen<br />

kombination af valg kan bruges, forplanter undtagelsen sig til det yderste funktionskald<br />

(som hvis 3 skal betales med 5 og 2).<br />

10.5 Otte-dronning-problemet<br />

Blindgydesøgning illustreres med endnu et klassisk eksempel, det s˚akaldte “otte-dronningproblem”<br />

(se eventuelt ogs˚a [5, Sect. 7.7]). En “dronning” i skak sl˚ar andre brikker p˚a samme<br />

række, samme søjle (i skak kaldet “linje”) og p˚a de samme diagonaler. Opgaven er at placere<br />

otte dronninger p˚a et skakbræt, s˚a ingen af dem kan sl˚a nogen af de andre.<br />

Man kan forsøge at finde en enkelt løsning, at finde alle løsninger eller at finde ud af,<br />

hvilke essentielt forskellige løsninger der er, n˚ar symmetriske løsninger kun skal medregnes<br />

en gang.<br />

I 1850 blev problemet undersøgt af matematikeren Karl Friedrich Gauß, som dog ikke<br />

løste det fuldt ud.<br />

140


8<br />

7<br />

6<br />

5<br />

4<br />

3<br />

2<br />

1<br />

❊❆✁❆✁❆✁✆<br />

❊<br />

❊<br />

✆<br />

✆<br />

❊❆✁❆✁❆✁✆<br />

❊<br />

❊<br />

✆<br />

✆<br />

❊❆✁❆✁❆✁✆<br />

❊<br />

❊<br />

✆<br />

✆<br />

❊❆✁❆✁❆✁✆<br />

❊<br />

❊<br />

✆<br />

✆<br />

❊❆✁❆✁❆✁✆<br />

❊<br />

❊<br />

✆<br />

✆<br />

❊❆✁❆✁❆✁✆<br />

❊<br />

❊<br />

✆<br />

✆<br />

❊❆✁❆✁❆✁✆<br />

❊<br />

❊<br />

✆<br />

✆<br />

❊❆✁❆✁❆✁✆<br />

a b c d e f g h<br />

Figur 10.2: En løsning p˚a problemet med de otte dronninger.<br />

Med computer til r˚adighed kan problemet angribes efter principperne for kombinatorisk<br />

søgning. Lad os i første omgang blot prøve at finde en enkelt løsning (opgave 10.6 g˚ar ud p˚a<br />

at finde alle løsninger).<br />

Det er værd at bemærke, at den samme opgave kunne stilles for et bræt med n × n felter<br />

for andre værdier af n end 8, og efter princippet om at søge den størst mulige enkelhed ved<br />

passende generalisering vælger vi at konstruere programmet, s˚a der kun skal ændres ganske<br />

lidt, for at det vil kunne fungere med andre brætstørrelser (jævnfør opgave 10.7).<br />

Otte-dronning-problemet repræsenterer en fundamental klasse af kombinatoriske problemer<br />

og blev meget tidligt taget op i den datalogiske litteratur. Man finder for eksempel<br />

problemet behandlet i Edsger W. Dijkstra: Notes on Structured Programming, Technical<br />

University Eindhoven (1970) og i Niklaus Wirth: Program Development by Stepwise Refinement,<br />

Communications of the ACM 14 (April 1971) 221–227, mens en helt anden indgangsvinkel<br />

kan ses i Peter Naur: An Experiment on Program Development, BIT 12 (1972)<br />

347–365.<br />

Der kan højst være en dronning p˚a hver linje, s˚a hvis der i alt skal placeres otte, m˚a der<br />

netop st˚a en p˚a hver linje. Vi forsøger at placere dem fra en ende af, for eksempel først en<br />

dronning p˚a linje 8 (=h), s˚a p˚a linje 7 (=g), og s˚a videre ned til linje 1 (=a). Til r˚adighed<br />

under løsning af den delopgave at placere en dronning i linje x vil vi derfor have en delløsning<br />

best˚aende af dronninger i linjerne x + 1, . . . , 7, 8.<br />

141<br />

❊<br />

❊<br />

✆<br />


En dronnings placering kunne vises som (x, y), hvor x er linjenummeret og y rækkenummeret;<br />

en delløsning af den nævnte slags kunne da repræsenteres<br />

[(x + 1,yx+1),. . .,(7,y7),(8,y8)]. Denne repræsentation er valgt i [5, Sect. 7.7], men det<br />

er klart, at linjenumrene kan underforst˚as, og det vil vi udnytte her, s˚a en s˚adan delløsning<br />

blot vil blive repræsenteret gennem listen [yx+1,. . .,y7,y8].<br />

N˚ar en dronning skal placeres p˚a en linje, prøves rækkerne af i rækkefølgen 8, 7, . . . , 2, 1.<br />

Den centrale funktion i løsningen er dronning ((x,y),yr), som forsøger at udvide delløsningen<br />

yr med en dronning i (x, y). Den ønskede løsning dannes ved, at man udefra kalder<br />

p˚a formen dronning ((8,8),[]). Funktionens opbygning skal tage hensyn til følgende:<br />

• Hvis x = 0, er der allerede en dronning i linjerne 1 . . . 8, s˚adan at “del”løsningen faktisk<br />

er en fuld løsning og kan returneres som facit.<br />

• Hvis y = 0, har alle mulige rækker været forsøgt forgæves for dronningen i den aktuelle<br />

linje. Vi er havnet i en blindgyde og m˚a omgøre et af de tidligere valg.<br />

• Ellers skal det undersøges, om nogen af delløsningens dronninger sl˚ar feltet (x, y).<br />

Hvis det ikke er tilfældet, kan delløsningen udvides med en dronning p˚a dette felt, og<br />

dronning kan kaldes rekursivt med den udvidede delløsning i forsøg p˚a ogs˚a at placere<br />

en dronning i linje x − 1, i første omgang i række 8.<br />

• I tilfælde af, at det rekursive kald mislykkes, m˚a vi i stedet prøve feltet (x, y − 1).<br />

• Ogs˚a hvis en dronning p˚a (x, y) kan sl˚as af de andre, m˚a vi i stedet prøve (x, y − 1).<br />

De beskrevne punkter kan direkte identificeres i programmet:<br />

fun dronning ((0, ),yr) = yr<br />

| dronning (( ,0), ) = raise Dronning<br />

| dronning ((x,y),yr)<br />

= if sikker ((x,y),x + 1,yr)<br />

then dronning ((x - 1,8),y :: yr)<br />

handle Dronning => dronning ((x,y - 1),yr)<br />

else dronning ((x,y - 1),yr)<br />

Vi har her præsenteret den centrale struktur uden først at definere de indg˚aende størrelser.<br />

Undtagelsen Dronning skal erklæres. Dronninger p˚a (x, y) og (x1, y1) kan sl˚a hinanden,<br />

hvis de st˚ar p˚a samme linje (x = x1), samme række (y = y1) eller samme diagonal (x + y =<br />

x1 + y1 eller x − y = x1 − y1). Ved hjælp af funktionen slaar defineres sikker, hvor det<br />

ud over delløsningen yr og det aktuelle felt (x, y) er bekvemt at tage en ekstra parameter<br />

med til at holde styr p˚a delløsningens længde. (Værdien af den ekstra parameter er faktisk<br />

linjenummeret for delløsningens forreste dronning.)<br />

Det er ogs˚a bekvemt at konstruere en funktion visBraet, som kan fremstille en løsning<br />

grafisk. (Mange medier viser tegn med 10 tegn pr. tomme2 vandret, men 6 linjer pr. tomme<br />

lodret; for at f˚a felterne nogenlunde kvadratiske har vi derfor valgt, at hvert felt skal fylde<br />

to tegn vandret.) For nemheds skyld er funktionen skrevet, s˚a brættet roteres 90◦ , n˚ar det<br />

skrives ud:<br />

a<br />

b c<br />

d efg<br />

h<br />

12345678<br />

2 En amerikansk tomme (eng.:inch) er standardiseret til 25.4 mm.<br />

142


Nu skulle det samlede program være til at forst˚a:<br />

fun prikker 0 =<br />

| prikker n = ". "^ prikker (n - 1)<br />

fun visBraet [] =<br />

| visBraet (y :: yr)<br />

= prikker (y - 1) ^ "* "^ prikker (8 - y) ^ "\n"<br />

^ visBraet yr<br />

fun slaar ((x,y),(x1,y1))<br />

= (* x = x1 orelse *) y = y1<br />

orelse x + y = x1 + y1 orelse x - y = x1 - y1<br />

fun sikker ((x,y), ,[]) = true<br />

| sikker ((x,y),x1,y1 :: yr)<br />

= not (slaar ((x,y),(x1,y1)))<br />

andalso sikker ((x,y),x1 + 1,yr)<br />

exception Dronning<br />

fun dronning ((0, ),yr) = yr<br />

| dronning (( ,0), ) = raise Dronning<br />

| dronning ((x,y),yr)<br />

= if sikker ((x,y),x + 1,yr)<br />

then dronning ((x - 1,8),y :: yr)<br />

handle Dronning => dronning ((x,y - 1),yr)<br />

else dronning ((x,y - 1),yr);<br />

val prikker = fn : int -> string<br />

val visBraet = fn : int list -> string<br />

val slaar = fn : (int * int) * (int * int) -> bool<br />

val sikker = fn : (int * int) * int * int list -> bool<br />

exn Dronning = Dronning : exn<br />

val dronning = fn : (int * int) * int list -> int list<br />

print (visBraet (dronning ((8,8),[])));<br />

. . . . * . . .<br />

. . . . . . * .<br />

. * . . . . . .<br />

. . . . . * . .<br />

. . * . . . . .<br />

* . . . . . . .<br />

. . . * . . . .<br />

. . . . . . . *<br />

val it = () : unit<br />

10.6 Sammenfatning<br />

Mange opgaveløsninger best˚ar af en sekvens af valg. Hvis valgene kan ordnes, s˚a trufne valg<br />

aldrig skal omgøres, kan man blot benytte det gr˚adige princip. Gængse møntsystemer er<br />

s˚adan indrettet, at med alle møntstørrelser til r˚adighed kan betalinger bruge det gr˚adige<br />

143


princip.<br />

I almindelighed kan det være nødvendigt at “prøve sig frem”. Blindgydesøgning er den<br />

søgestrategi, hvor det systematisk er det senest trufne valg, der gøres om.<br />

Undtagelser erklæres med exception, kastes med raise og gribes med handle. Ved<br />

at bruge undtagelser er det let at programmere blindgydesøgning efter den første mulige<br />

løsning.<br />

Alle løsninger p˚a et problem kan man f˚a frem ved at arbejde med en liste af løsninger.<br />

Længden af denne liste er s˚a antallet af løsninger. (Hvis problemet er uløseligt, bliver listen<br />

tom.)<br />

10.7 Opgaver<br />

Opgave 10.1 BetalG benytter den urealistiske forudsætning, at der er et vilk˚arligt antal af<br />

hver møntværdi til r˚adighed. Modificer funktionen, s˚a den betaler fra en endelig pengepung.<br />

Opgave 10.2 Definer foranAlle ved hjælp af map og en anonym funktion.<br />

Opgave 10.3 Skriv en funktion, som ud fra et ˚arstal og dagens nummer i ˚aret beregner<br />

datoen (i form af m˚anedens nummer og dagens nummer inden for den p˚agældende m˚aned).<br />

Opgave 10.4 (Ideen til denne opgave skyldes Peter Sestoft.) Romertalord skrives med<br />

bogstaverne ”M”(1000), ”D”(500), ”C”(100), ”L”(50), ”X”(10), ”V”(5) og ”I”(1). Normalt<br />

skrives bogstaver med mindre vægt til højre for bogstaver med større vægt; hvis et bogstav<br />

skrives til venstre for et med større vægt, skal dets egen vægt subtraheres. Antag, de eneste<br />

tilladte kombinationer af den slags er ”IV”, ”IX”, ”XL”, ”XC”, ”CD”og ”CM”. Skriv et<br />

program, der omsætter tal til romertalord. Programmet kan benytte det gr˚adige princip.<br />

Opgave 10.5 I denne opgave kaldes en liste for delliste af en anden, hvis alle dens elementer<br />

er indeholdt i den anden og st˚ar i samme rækkefølge. Skriv en funktion, der ud fra en liste<br />

som argument danner listen af alle dens dellister. Den rækkefølge, dellisterne kommer i, er<br />

underordnet. Med argumentet [1,2,3] kunne funktionsværdien for eksempel være<br />

[[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]. Vink: Man kan bruge omtrent samme<br />

programskabelon som i betalA.<br />

Opgave 10.6 Find alle løsninger til otte-dronning-problemet (der er 92). Skriv ogs˚a en<br />

funktion, der kun medtager én repræsentant fra hver klasse af symmetriske løsninger (der<br />

er 12 forskellige løsninger, hvis symmetri tages i betragtning).<br />

Opgave 10.7 Generaliser otte-dronning-problemet til den opgave at placere n dronninger<br />

p˚a et n × n-bræt. For n = 2 og n = 3 er der ingen løsninger, men for n = 4 er der 2 (1<br />

modulo symmetri). Hvor mange løsninger er der for n = 5, n = 6 og n = 7?<br />

144


Kapitel 11<br />

Køretidseffektivitet<br />

11.1 Eksempel: potensopløftning<br />

Vi vil konstruere en funktion til beregning af x n . Her kan x være et hvilket som helst reelt<br />

tal, mens vi vil forudsætte, at n er et naturligt tal, s˚a man kan bygge p˚a formlen<br />

x n = x · x · · · x<br />

<br />

n<br />

(11.1)<br />

(Matematikere kan udvide definitionen af potensopløftning til eksponenter, der er negative,<br />

brudne og s˚agar vilk˚arlige reelle og komplekse tal, men det kræver, at man g˚ar ud over<br />

definitionen (11.1), som vi vil bygge p˚a her1 .)<br />

Af (11.1) indses<br />

x n = x · x n−1<br />

(11.2)<br />

der er et godt grundlag for beregning, idet x n her er dannet ved hjælp af det simplere udtryk<br />

x n−1 . Vi mangler en basis for rekursionen. Her kunne man vælge x 2 = x · x eller x 1 = x,<br />

men efter princippet om at reducere antallet af specialtilfælde vælger vi<br />

x 0 = 1 (11.3)<br />

som giver (11.2) det størst mulige gyldighedsomr˚ade (alle positive hele n) og medfører de to<br />

andre identiteter (x 1 = x og x 2 = x · x).<br />

Funktionsdefinitionen kan bygge p˚a (11.3) og (11.2):<br />

fun oploefttil (x : real, n : int)<br />

= if n = 0 then 1.0 else x * oploefttil (x, n - 1);<br />

val oploefttil = fn : real * int -> real<br />

Læg mærke til typebegrænsningerne af x og n og til, hvordan basis for multiplikationerne<br />

skal være den flydende talkonstant 1.0 (som ogs˚a kunne have været skrevet real 1).<br />

(Typebegrænsningerne kunne strengt taget undværes, idet de impliceres af brugen af 1.0<br />

og af operationerne n = 0 og n - 1.)<br />

Funktionen skal afprøves, s˚a i den fil, hvori definitionen er skrevet, tilføjer vi<br />

1 I oversættelsesenheden Math ligger biblioteksfunktionen Math.pow : real * real -> real.<br />

145


oploefttil (real 3, 0) = real 1;<br />

oploefttil (real 4, 1) = real 4;<br />

oploefttil (real 3, 2) = real 9;<br />

oploefttil (real 2, 3) = real 8;<br />

(som alle besvares af systemet med val it = true : bool ).<br />

11.1.1 En forbedring<br />

Det er oplagt, at et kald af formen oploefttil (x, n) løser sin opgave ved at udføre n<br />

multiplikationer, men s˚a mange er det ikke nødvendigt at bruge! Man kan jo kvadrere et<br />

tal med én multiplikation, s˚a for at beregne for eksempel x 4 = (x 2 ) 2 kan man nøjes med to<br />

multiplikationer, og for store eksponenter bliver gevinsten betragtelig. Man kan sige, at vi<br />

har fundet endnu en egenskab<br />

som kan udnyttes i funktionsdefinitionen:<br />

x n = (x 2 ) n<br />

2 for lige naturlige tal n (11.4)<br />

fun oploefttilhurt (x : real, n : int)<br />

= if n = 0 then 1.0<br />

else if n mod 2 = 0<br />

then oploefttilhurt (x * x, n div 2)<br />

else x * oploefttilhurt (x, n - 1);<br />

oploefttilhurt (real 3, 0) = real 1;<br />

oploefttilhurt (real 4, 1) = real 4;<br />

oploefttilhurt (real 3, 2) = real 9;<br />

oploefttilhurt (real 2, 3) = real 8;<br />

11.2 M˚aling af ressourceforbrug<br />

Ved at prøve forskellige eksempler af ved terminalen kan man godt fornemme, at oploefttil<br />

ikke er s˚a effektiv som oploefttilhurt, men det er nyttigt at kunne understøtte denne<br />

fornemmelse med konkrete tal.<br />

Mosml.time<br />

I oversættelsesenheden Mosml ligger en biblioteksfunktion<br />

Mosml.time : (α -> β) -> (α -> β)<br />

som kan bruges til m˚aling af ressourceforbrug. Det fremg˚ar af typen, at der er tale om en<br />

højereordensfunktion: for en funktion f beregner konstruktionen Mosml.time f det samme<br />

som f, men i tilgift f˚ar man, i en ekstra linje oven over funktionsværdien, en opsummering<br />

af beregningens tidsforbrug.<br />

Helt konkret betyder det, at man (efter at direktivet load "Mosml"; har gjort funktionen<br />

Mosml.time tilgængelig) kan f˚a m˚alt ressourceforbruget for et kald f x ved i stedet at<br />

skrive (Mosml.time f) x (hvor parentesen i øvrigt kan undværes, fordi funktionsanvendelse<br />

associerer fra venstre).<br />

Den ekstra linje med oplysninger har formen<br />

146


User: brugttid System: systtid GC: spildop Real: sandtid<br />

idet sandtid er den faktisk forløbne tid, brugttid er det tidsrum, centralenheden 2 har været<br />

benyttet, systtid er tiden brugt p˚a systemkald og spildop er tiden brugt under s˚akaldte<br />

spildopsamlinger (hvilket til en vis grad m˚aler lagerforbruget). Enheden for tidsangivelserne<br />

er sekunder.<br />

Man kan for eksempel finde:<br />

load "Mosml";<br />

val it = () : unit<br />

Mosml.time oploefttil (2.0, 1000);<br />

User: 0.010 System: 0.000 GC: 0.000 Real: 0.018<br />

val it = 1.07150860719E301 : real<br />

Mosml.time oploefttilhurt (2.0, 1000);<br />

User: 0.000 System: 0.000 GC: 0.000 Real: 0.001<br />

val it = 1.07150860719E301 : real<br />

11.3 Beregning af udførelsestid<br />

M˚alinger kan naturligvis kun give oplysning om forløbet for konkrete inddatasæt, hvilket ikke<br />

er tilstrækkeligt, hvis man vil udtale sig om en funktions ressourceforbrug i almindelighed.<br />

S˚adanne udtalelser m˚a bygge p˚a analyser af selve programteksten. For oploefttil (x, n)<br />

ses det, at de dele af programmet, som udføres, hvis n = 0, er<br />

if n = 0 then 1.0 else . . .<br />

De hermed forbundne beregninger m˚a formodes at tage en vis konstant tid a.<br />

Lad os indføre notationen Tf(x) som betegnelse for den tid, det tager at kalde en bestemt<br />

ML-funktion f med argumentet x. S˚a har vi lige set, at<br />

Toploefttil(x, 0) = a (11.5)<br />

For n > 0 er der fire skridt i beregningen oploefttil (x, n):<br />

1. Indledningsvis beregnes if n = 0 then ... else<br />

2. Derefter evalueres n1 = n - 1 (fordi ML har striks parameteroverføring)<br />

3. S˚a udføres det rekursive kald x1 = oploefttil (x, n1)<br />

4. Og til sidst multipliceres x * x1<br />

Her kan vi g˚a ud fra, at skridtene 1, 2 og 4 tilsammen tager en vis konstant tid b, mens<br />

tiden for skridt 3 jo er Toploefttil(x, n − 1), alts˚a:<br />

Toploefttil(x, n) = b + Toploefttil(x, n − 1) for n > 0 (11.6)<br />

Af de to ligninger (11.5) og (11.6) finder man<br />

Toploefttil(x, n) = a + b · n (11.7)<br />

2 En computers centralenhed (eng.:central processing unit eller CPU) er den aktive enhed, som foretager<br />

den egentlige ordrefortolkning.<br />

147


Analyse af oploefttilhurt<br />

Den forbedrede funktion er lidt vanskeligere at behandle. Man indser, at der m˚a være konstanter<br />

a, b0 og b1, s˚a<br />

Toploefttilhurt(x, 0)<br />

Toploefttilhurt(x, n)<br />

=<br />

=<br />

a<br />

b0 + Toploefttilhurt(x<br />

(11.8)<br />

2 , n<br />

)<br />

2<br />

for lige n > 0 (11.9)<br />

Toploefttilhurt(x, n) = b1 + Toploefttilhurt(x, n − 1) for ulige n (11.10)<br />

Da tallet umiddelbart forud for et ulige tal jo er lige, medfører de to sidste ligninger<br />

(11.9) og (11.10)<br />

Toploefttilhurt(x, n) ≤ b + Toploefttilhurt(x 2 , n div 2) for n > 0 (11.11)<br />

for en passende konstant b, og af (11.8) og (11.11) finder man<br />

Toploefttilhurt(x, n) ≤ a + b · k (11.12)<br />

hvor k er antallet af gange, n skal divideres med 2, før man f˚ar 0.<br />

For funktionen oploefttil kunne udførelsestiden direkte udtrykkes som en funktion<br />

af n, der var den ene komponent af inddataparret, men i almindelighed behøver inddata<br />

ikke at være tal: de kan ogs˚a være tegn eller tekster eller strukturer af forskellig slags. Da<br />

er man interesseret i at knytte et eller andet størrelsesm˚al til inddata og s˚a at f˚a udtrykt<br />

udførelsestiden som funktion af inddatas størrelse. Vælger man at bruge n til vurdering af<br />

størrelsen af (x, n), har vi i ligningen (11.7) direkte et udtryk for udførelsestiden.<br />

Inddata af samme størrelse kan have forskellige udførelsestider; det, man som regel er<br />

interesseret i, er da at finde udførelsestiden i det værste tilfælde. Det er heller ikke altid let<br />

(om overhovedet muligt) at finde et eksplicit udtryk for denne udførelsestid, og man vil da<br />

være tilfreds med bare at finde en begrænsning opadtil, alts˚a en eller anden funktion g, s˚a<br />

Tf(x) ≤ g(n) for alle inddata x af størrelse n.<br />

For oploefttilhurt fandt vi (11.12), der ogs˚a (for n = 0) kan skrives<br />

Toploefttilhurt(x, n) ≤ a ′ + b ′ · log 2 n<br />

idet ⌊log 2 n⌋ (det største hele tal mindre end eller lig med totalslogaritmen af n) er antallet<br />

af gange, et tal n skal heltalsdivideres med 2, før man f˚ar 1.<br />

11.4 Funktioners vækst<br />

Nøjagtige formler for, hvor lang tid et program kører, kan være vanskelige at finde — og<br />

selv om man finder dem, kan de være s˚a komplicerede, at det er vanskeligt at gennemskue,<br />

hvad de egentlig betyder.<br />

Som regel har man heller ikke brug for den eksakte formel; i praksis er situationen ofte<br />

den, at det afgørende er, hvordan køretiden vokser med voksende inddatastørrelse. Flytter<br />

man et program fra en lille maskine til en større og dyrere, der har mere lager og kan udføre<br />

flere ordrer i sekundet, vil det naturligvis køre hurtigere. Men forsyner man den lille maskine<br />

med et bedre program, er det alligvel muligt, at den for visse (passende store) inddata vil<br />

klare sig bedre end den store maskine med det d˚arligere program, som illustreret i figur 11.1.<br />

148


tid<br />

11.4.1 Store O-notation<br />

Vi havde jo fundet<br />

✻<br />

<br />

✑ (1)<br />

(2)<br />

✟<br />

✭✭<br />

(3)<br />

✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑✑<br />

✑✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟✟ ✟<br />

Figur 11.1: Køretider for potensopløftning<br />

(1) oploefttil (x, n) p˚a et langsomt system<br />

(2) oploefttil (x, n) p˚a et hurtigt system<br />

✲<br />

(3) oploefttilhurt (x, n) p˚a et langsomt system<br />

Toploefttil(x, n) = a + b · n<br />

Toploefttilhurt(x, n) ≤ a ′ + b ′ · log 2 n<br />

n<br />

(11.13)<br />

og det interessante er derfor ikke størrelsen af konstanterne a, b, a ′ og b ′ (som afhænger af<br />

den konkrete maskine og det konkrete programmeringssprogssystem), men forskellen mellem<br />

funktionerne n ↦→ n og n ↦→ log 2 n.<br />

For at beskrive denne forskel er det almindeligt at bruge den s˚akaldte “store O-notation”<br />

(oprindeligt benyttet af P. Bachmann i bogen Analytische Zahlentheorie fra 1892). O’et er<br />

en forkortelse for “orden”, og meningen er, at hvis f er en funktion af de naturlige tal,<br />

betegner O(f(n)) (læses: store O af f af n) en ikke nærmere specificeret værdi af samme<br />

størrelsesorden som f(n). Mere præcist kan man sige, at<br />

Hver forekomst af O(f(n)) betegner en størrelse xn, for hvilken der findes<br />

konstanter M og n0 ≥ 0, s˚adan at |xn| ≤ M · |f(n)| for alle n ≥ n0.<br />

De nøjagtige værdier af M og n0 angives ikke af notationen, og bruges symbolet O(f(n))<br />

flere gange, kan der høre forskellige værdier af M og n0 til de forskellige forekomster.<br />

Her er nogle eksempler p˚a brug af notationen:<br />

37 + 102n =O(n) fordi 37 + 102n ≤ 139n for n ≥ 1<br />

a + b · n =O(n)<br />

7 − 12n + 6n 2 =O(n 2 ) fordi 7 − 12n + 6n 2 ≤ 13n 2 for n ≥ 1<br />

a + b · n + c · n 2 =O(n 2 )<br />

a + b log 2 n =O(log 10 n) fordi a + b log 2 n ≤ (a + 4b) log 10 n for n ≥ 10<br />

149


Man plejer i øvrigt ikke at angive noget grundtal for logaritmer, der bruges i beskrivelse<br />

af en størrelsesorden O(. . . log . . .), eftersom logaritmer med forskellige grundtal jo er<br />

proportionale (husk: log a r = log b r<br />

log b a ).<br />

Det er i øvrigt en konsekvens af notationen, at en konstant kan betegnes som O(1) (alts˚a<br />

for eksempel: 31 = O(1)).<br />

Med den indførte notation til r˚adighed kan resultatet fra (11.13) nu kompakt udtrykkes:<br />

Toploefttil(x, n) = O(n)<br />

Toploefttilhurt(x, n) = O(log n)<br />

Man siger ogs˚a, at oploefttil asymptotisk har lineær udførelsestid, mens størrelsesordenen<br />

af udførelsestiden for oploefttilhurt asymptotisk er logaritmisk.<br />

11.4.2 Store Ω- og store Θ-notation<br />

Nu, hvor vi har indført store O-notationen for asymptotisk begrænsning opadtil, er det p˚a<br />

sin plads at omtale to beslægtede notationer.<br />

Undertiden har man brug for at udtale sig om, at et problem eller en algoritme mindst<br />

kører i s˚a og s˚a lang tid. Køretiden vil naturligvis afhænge af inddatas størrelse, men man<br />

ønsker et funktionsudtryk, der asymptotisk begrænser den nedadtil.<br />

Hver forekomst af Ω(f(n)) (læses: store omega af f af n)<br />

betegner en størrelse zn, for hvilken der findes konstanter<br />

m > 0 og n0 ≥ 0, s˚adan at m · |f(n)| ≤ |zn| for alle<br />

n ≥ n0.<br />

Som før er der tale om en upræcis notation, der abstraherer fra de konkrete værdier af m<br />

og n0, og ved forskellige forekomster af Ω(f(n)) kan værdierne af m og n0 være forskellige.<br />

Hvis en størrelse er asymptotisk begrænset af samme funktion b˚ade opadtil og nedadtil,<br />

er den asymptotisk tæt begrænset af denne funktion, og man plejer at bruge det græske<br />

bogstav store theta som betegnelse herfor: Man siger, at yn = Θ(f(n)), hvis der b˚ade gælder<br />

yn = O(f(n)) og yn = Ω(f(n)).<br />

11.5 Effektivitet<br />

Hver forekomst af Θ(f(n)) betegner en størrelse yn, for<br />

hvilken der findes konstanter M, m > 0 og n0 ≥ 0, s˚adan<br />

at m · |f(n)| ≤ |yn| ≤ M · |f(n)| for alle n ≥ n0.<br />

Med oploefttilhurt er effektiviteten forbedret p˚a bekostning af overskueligheden: antallet<br />

af tilfælde blev øget fra to til tre, idet vi inddrog relationen (11.4). Lad os overveje den<br />

tilsvarende identitet<br />

x n = (x n<br />

2 ) 2<br />

for lige naturlige tal n (11.14)<br />

hvor de to eksponenter er byttet om. Denne identitet er naturligvis lige s˚a god at bygge en<br />

effektiv potensopløftning over — hvis man vel at mærke gør det rigtigt!<br />

Her er det gjort forkert:<br />

150


fun oploefttillngs (x : real, n : int)<br />

= if n = 0 then 1.0<br />

else if n mod 2 = 0<br />

then oploefttillngs(x,n div 2)*oploefttillngs(x,n div 2)<br />

else x * oploefttillngs (x, n - 1);<br />

val oplloefttillngs = fn : real * int -> real<br />

Mosml.time oploefttillngs (2.0, 1000);<br />

User: 0.040 System: 0.000 GC: 0.000 Real: 0.044<br />

val it = 1.07150860719E301 : real<br />

Det var ˚abenbart en forværring snarere end en forbedring!<br />

Advarsel Det kan stærkt anbefales, at man indtaster disse funktioner og m˚aler køretiden<br />

for kald af oploefttil, oploefttilhurt og oploefttillngs med forskellige argumentværdier,<br />

men p˚a flerbrugersystemer er det vigtigt ikke at overdrive: Man kan komme til at sætte<br />

s˚a omfattende beregninger i gang, at systemets øvrige brugere generes af det.<br />

Grunden til den d˚arlige udførelsestid for oploefttillngs er naturligvis, at selv om dybden<br />

af kaldtræet en nøjagtig den samme som i kaldtræet for oploefttilhurt (antallet af gange,<br />

n skal heltalshalveres for at blive 0 og alts˚a af størrelsesorden log n), s˚a har hver knude med<br />

lige n to nye knuder under sig (se figur 11.2) (og ikke som i oploefttilhurt kun én), s˚a<br />

optill (x, 7)<br />

optill (x, 6)<br />

✘✘❳<br />

✘✘✘<br />

❳❳❳<br />

✘✘✘<br />

❳❳❳<br />

✘<br />

❳❳❳<br />

optill (x, 3) optill (x, 3)<br />

optill (x, 2) optill (x, 2)<br />

✘✘✘❳❳<br />

✘✘<br />

❳❳❳<br />

✘✘✘❳❳<br />

✘✘<br />

❳❳❳<br />

optill (x, 1) optill (x, 1) optill (x, 1) optill (x, 1)<br />

optill (x, 0) optill (x, 0) optill (x, 0) optill (x, 0)<br />

Figur 11.2: Kaldtræet for oploefttillngs (x, 7) (forkortet til optill (x, 7)).<br />

det samlede antal knuder i kaldtræet bliver af størrelsesorden n.<br />

Det er tydeligt for os, at de to indre kald oploefttillngs (x,n div 2) er ens, men det<br />

er ML-afviklingssystemet ikke programmeret til at finde ud af: Skriver man to funktionskald,<br />

s˚a udføres der to funktionskald!<br />

Det rigtige er at huske værdien af det indre funktionskald i en hjælpevariabel:<br />

fun oploefttilhurt2 (x : real, n : int)<br />

= if n = 0 then 1.0<br />

151


else if n mod 2 = 0<br />

then let val xn2 = oploefttilhurt2 (x, n div 2)<br />

in xn2 * xn2 end<br />

else x * oploefttilhurt2 (x, n - 1);<br />

val oplloefttilhurt2 = fn : real * int -> real<br />

Mosml.time oploefttilhurt2 (2.0, 1000);<br />

User: 0.000 System: 0.000 GC: 0.000 Real: 0.001<br />

val it = 1.07150860719E301 : real<br />

En anden m˚ade at opn˚a effektiviteten p˚a ville være at kvadrere ved hjælp af funktionen<br />

kvadrat fra afsnit 2.3.1; i udtrykket kvadrat (oploefttilhurt3 (x, n div 2)) vil<br />

funktionsargumentet (for kaldet af kvadrat) oploefttilhurt3 (x, n div 2) kun blive<br />

beregnet én gang (fordi ML er strikst).<br />

fun oploefttilhurt3 (x : real, n : int)<br />

= if n = 0 then 1.0<br />

else if n mod 2 = 0<br />

then kvadrat (oploefttilhurt3 (x, n div 2))<br />

else x * oploefttilhurt3 (x, n - 1);<br />

val oplloefttilhurt3 = fn : real * int -> real<br />

11.6 Listespejling<br />

Tabel [5, D.17] viser, at der er en biblioteksfunktion rev til at spejlvende (eng.:reverse) en<br />

liste, men det er lærerigt selv at prøve at konstruere s˚adan en – lad os kalde den spejl.<br />

En liste er enten tom, og s˚a er den spejlvendte liste det ogs˚a, eller ogs˚a har den formen<br />

x :: xr, i hvilket tilfælde den spejlvendte liste begynder med det spejlvendte af xr og slutter<br />

med x. S˚adan set har vi nu to egenskaber, ud fra hvilke spejl kunne konstrueres<br />

spejl [] = []<br />

spejl (x :: xr) = spejl xr @ [x]<br />

(11.15)<br />

men for at vurdere køretiden for spejl herudfra m˚a man vide, hvor lang tid listesammenstilling<br />

med operatoren @ tager. Den letteste m˚ade at forst˚a kompleksiteten af @ p˚a er at<br />

forestille sig, man ogs˚a selv skulle konstruere den:<br />

11.6.1 Analyse af listesammenstillen (operatoren @)<br />

Den forreste af de to lister, der skal stilles sammen, er enten tom eller er konstrueret af et<br />

hoved sat foran en hale. Vi finder egenskaberne<br />

[] @ yr = yr<br />

(x :: xr) @ yr = x :: (xr @ yr)<br />

der kan lægges til grund for definitionen af den tilsvarende funktion, som vi vælger at kalde<br />

“append”:<br />

152


fun append ([], yr) = yr<br />

| append (x :: xr, yr) = x :: append (xr, yr);<br />

val append = fn : ’a list * ’a list -> ’a list<br />

Programmet viser, at udførelsestiden for append kan findes p˚a omtrent samme m˚ade,<br />

som vi brugte ovenfor til at finde udførelsestiden for oploefttil: Der m˚a være konstanter<br />

a og b, s˚adan at<br />

Tappend([], yr) = a<br />

Tappend(x :: xr, yr) = b + Tappend(xr, yr)<br />

Af disse ligninger finder man Tappend([x1,x2,. . .,xn], yr) = a + b · n. Køretiden er alts˚a<br />

helt uafhængig af anden liste, men asymptotisk lineært begrænset af længden af den første<br />

liste:<br />

Udførelsestiden for sammenstillen af to lister xr og yr<br />

er O(length xr)<br />

11.6.2 Analyse af funktionen spejl<br />

Med udgangspunkt i (11.15) konstruerer vi nu spejl:<br />

fun spejl [] = [] | spejl (x :: xr) = append (spejl xr, [x]);<br />

val spejl = fn : ’a list -> ’a list<br />

Der m˚a være en konstant a, s˚a<br />

Tspejl([]) = a (11.16)<br />

Skridtene under spejlvending af en ikke tom liste x :: xr ses at være (de to første skridt<br />

skyldes, at argumenterne til append jo beregnes før kaldet):<br />

1. En spejlvending af xr<br />

2. Konstruktion af singletonlisten [x]<br />

3. Kaldet af append<br />

Tiden for skridt 1 er Tspejl(xr); tiden for skridt 2 kan antages at være en konstant b, og<br />

tiden for skridt 3 er ifølge analysen ovenfor begrænset af en konstant c gange længden af<br />

den spejlvendte liste.<br />

Nu har den spejlvendte liste og listen selv jo samme længde, s˚a samlet kan det skrives<br />

Tspejl(x :: xr) ≤ Tspejl(xr) + b + c · length xr (11.17)<br />

Af (11.16) og (11.17) finder man for en liste af længde n<br />

n(n − 1)<br />

Tspejl([x1, x2, . . ., xn]) ≤ a+bn+c(1+2+3+. . .+n−1) = a+bn+c<br />

2<br />

= O(n 2 )<br />

Udførelsestiden for spejlvendingsfunktionen i dette afsnit er asymptotisk begrænset<br />

af kvadratet p˚a listens længde.<br />

153


Summen af en differensrække<br />

I udregningen ovenfor fik vi brug for at beregne summen af tallene 1, 2, 3, . . . , n − 1, hvor<br />

hvert tal netop er 1 større end det foreg˚aende. En talrække, hvor forskellen mellem hvert tal<br />

og det foreg˚aende tal har en konstant værdi d, kaldes en differensrække med differens d, og<br />

for summen af en s˚adan række findes en færdig formel, vi vil udlede i dette afsnit.<br />

Kald det første af tallene a og antallet af dem for n; talrækken best˚ar da af<br />

a1 = a, a2 = a + d, a3 = a + 2d, . . . , an−1 = a + (n − 2)d, an = a + (n − 1)d<br />

Indføres betegnelsen S for tallenes sum (den værdi, vi er ude p˚a at finde en formel for),<br />

finder man ved omordning af leddene i S + S:<br />

eller<br />

2S =<br />

a +a + d +a + 2d<br />

+a + (n − 1)d +a + (n − 2)d +a + (n − 3)d<br />

= 2a + (n − 1)d +2a + (n − 1)d +2a + (n − 1)d<br />

= n · (2a + (n − 1)d) = n · (a1 + an)<br />

S = n<br />

2 · (a1 + an)<br />

. . .<br />

. . .<br />

. . .<br />

Summen af tallene i en differensrække er halvdelen af antallet<br />

gange det første plus det sidste tal.<br />

+a + (n − 1)d<br />

+a<br />

+2a + (n − 1)d<br />

(11.18)<br />

Summen af de n tal fra 1 til n er derfor n(1+n),<br />

og det var den formel, som blev benyttet<br />

2<br />

ovenfor.<br />

Anvisning Prøv at definere de viste funktioner, og prøv at m˚ale deres køretider med<br />

Mosml.time. For at eksperimentere med spejlvending af lister af forskellig længde kunne<br />

man definere spejl (gentag (n, 1)) eller spejl (interval (1, n)) som funktion af n,<br />

hvor gentag og interval er funktionerne fra kapitel 9.<br />

Eksperimenter bekræfter køretidens kvadratiske afhængighed af n. Sammen med ressourceforbruget<br />

f˚ar man ogs˚a den spejlvendte liste (udtrykkets værdi) ud, hvilket i netop<br />

denne forbindelse m˚aske er lidt forstyrrende 3 . Man kan da betjene sig af følgende trick (der<br />

afgørende bygger p˚a den strikse parameteroverføring): kapsl udtrykket ind i en funktion,<br />

som smider sit argument væk!<br />

Man kunne for eksempel indføre<br />

fun nul x = 0;<br />

val nul = fn : ’a -> int<br />

og s˚a tage tid p˚a nul (spejl (gentag (n, 1))) som funktion af n.<br />

3 Hvis strukturer bliver tilstrækkeligt lange eller dybe, udskriver Moscow ML dem faktisk ikke fuldtud!<br />

To systemvariable printLength, printDepth : int ref (se [13, afsnit 3.4]), der fra start af er sat til<br />

henholdsvis 200 og 20, men hvis værdi man selv kan ændre (hvordan variable ændres, beskrives i [5, afsnit<br />

18.2.4]), bestemmer, hvor meget man f˚ar at se.<br />

154


11.7 Programtransformation<br />

Fra dagligdagen er det kendt, hvordan man kan spejlvende en stak af emner ved at blade<br />

den en gang igennem (se figur 11.3).<br />

✬ ✩<br />

4 ❄<br />

5<br />

6 3<br />

7 2<br />

8 1<br />

Figur 11.3: Spejlvending af en stak af emner ved en enkelt gennembladning.<br />

Derfor er spejlvending i kvadratisk tid ikke tilfredsstillende — det m˚a kunne gøres i<br />

lineær tid. I dette afsnit vil vi konstruere s˚adan en funktion.<br />

For potensopløftning s˚a vi, hvordan de hurtigere udgaver var mere komplicerede og vanskeligere<br />

at konstruere end oploefttil, der direkte fulgte specifikationen, og det gælder<br />

generelt: Hensynet til at f˚a et rimelig effektivt program betyder ofte, at man m˚a omforme<br />

det specifikationsmæssige udgangspunkt, og trækker derved i modsat retning af hensynet til<br />

enkelhed og overskuelighed.<br />

Den ideelle m˚ade at konstruere et program p˚a ville være først at skaffe sig et korrekt<br />

program ud fra den simplest mulige specifikation og derefter at gøre dette program effektivt<br />

via transformationer, der vidstes at bevare korrektheden.<br />

Del og hersk<br />

Princippet bag konstruktionen af de hurtige potensopløftningsfunktioner kan siges at være<br />

det, nogle har kaldt “del og hersk”, og som kan udtrykkes s˚aledes: Hvis et problem giver<br />

anledning til delproblemer, er det en fordel at gøre det største delproblem mindst muligt.<br />

I det aktuelle tilfælde: Det var bedre at dele x n i problemer af maksimalt halv størrelse<br />

(x n<br />

2 , samt en kvadrering) end at dele i et meget lille problem (multiplikation med x) og et,<br />

som kun var ganske lidt mindre end det oprindelige problem (x n−1 ).<br />

Hvis det ikke havde været s˚a tidskrævende at opsplitte en liste midti og at sammenhægte<br />

lister, kunne man ogs˚a overveje “del og hersk” i forbindelse med listespejling — et program<br />

ville kunne bygge p˚a egenskaben<br />

Fortsættelser<br />

spejl (xr @ yr) = spejl yr @ spejl xr<br />

Vi vil forbedre spejlvendingsfunktionen ved at betragte en fortsættelse (eng.:continuation).<br />

Ideen er, at hvis forbedrende omskrivninger forhindres af den kontekst, det rekursive kald<br />

indg˚ar i, s˚a kan man undertiden komme igennem ved en generalisering, hvor konteksten<br />

inddrages som ekstra funktionsparameter.<br />

155


Det rekursive tilfælde er<br />

spejl (x :: xr) = append (spejl xr, [x])<br />

hvor det rekursive kald indg˚ar som første argument til append, eller sagt p˚a en anden m˚ade:<br />

det rekursive kald fortsættes med efterhægtning af listen [x].<br />

Lad os derfor i stedet for listespejling betragte en mere generel opgave: den at spejlvende<br />

en liste og bagefter hægte en anden liste p˚a. Specifikation 4 :<br />

spejlforan (xr, yr) = spejl xr @ yr<br />

To krav m˚a naturligvis være opfyldt, hvis man skal have fornøjelse af at g˚a over til den<br />

mere generelle opgave:<br />

1. Den oprindelige funktion skal kunne beregnes som et specialtilfælde af den nye funktion.<br />

2. I specifikationen for den nye funktion indg˚ar den oprindelige funktion, men hensigten<br />

er, at den skal programmeres mere effektivt og uden reference til den gamle funktion.<br />

Punkt 1. er opfyldt, fordi spejl xr fremkommer som det specialtilfælde af<br />

spejlforan (xr, yr), hvor yr er tom.<br />

For at tilgodese 2. foretages nogle omskrivninger, som bygger p˚a definitionerne af spejl<br />

og append. For overskuelighedens skyld <strong>noter</strong>es append med operatoren @:<br />

spejlforan ([], yr) = spejl [] @ yr = [] @ yr = yr<br />

spejlforan (x :: xr, yr) = spejl (x :: xr) @ yr<br />

= append (spejl xr, [x]) @ yr = (spejl xr @ [x]) @ yr<br />

= spejl xr @ ([x] @ yr) = spejl xr @ (x :: yr)<br />

= spejlforan (xr, x :: yr)<br />

Her lykkedes det at udtrykke spejlforan uden brug af spejl, s˚a vi nu kan præsentere<br />

en ny definition:<br />

fun spejlforan ([], yr) = yr<br />

| spejlforan (x :: xr, yr) = spejlforan (xr, x :: yr);<br />

val spejlforan = fn : ’a list * ’a list -> ’a list<br />

fun spejlhurt xr = spejlforan (xr, []);<br />

val spejlhurt = fn : ’a list -> ’a list<br />

I en vis forstand kan man sige, at spejlforan realiserer metoden skitseret i figur 11.3,<br />

og da det rekursive kald benytter halen af argumentparrets førstekomponent, f˚as<br />

Tspejlhurt(xr) = O(length xr)<br />

Udførelsestiden for den hurtige spejlvendingsfunktion er asymptotisk lineært<br />

begrænset af listens længde.<br />

4 En effektiv implementering af spejlforan er faktisk tilgængelig i standardbiblioteket under navnet<br />

List.revAppend.<br />

156


11.8 Sammenfatning<br />

Tidskompleksiteten for en algoritme er den funktion af n, som angiver det største antal<br />

operationer, algoritmen vil foretage for inddata af længde n.<br />

Ofte angives tidskompleksitetens størrelsesorden. En størrelse xn siges at være O(f(n)),<br />

hvis der findes en konstant M, s˚a der fra et vist trin at regne (med andre ord for n ≥ n0)<br />

gælder |xn| ≤ M · |f(n)|.<br />

Man har tilsvarende forkortelser Ω(f(n)) og Θ(f(n)) for begrænsning nedadtil og tosidet<br />

begrænsning.<br />

Et funktionskalds faktiske tidsforbrug (i sekunder) kan m˚ales med biblioteksfunktionen<br />

Mosml.time.<br />

Køretiden for @ er proportional med længden af venstre operand.<br />

Visse algoritmer (for eksempel potensopløftning) kan forbedres med metoden “del og<br />

hersk”, som g˚ar ud p˚a, at de komplicerede tilfælde skal søges løst af to eller flere nogenlunde<br />

lige store delproblemer.<br />

Listespejling kunne omskrives til at køre i lineær tid ved at generalisere til en funktion,<br />

som tog fortsættelsen af det indre rekursive kald med i betragtning.<br />

11.9 Opgaver<br />

Opgave 11.1 Antallet af opstillinger af n forskellige elementer valgt (uden tilbagelægning)<br />

fra en mængde med x elementer (antallet af n-permutationer af x elementer) er den faldende<br />

potensfunktion x (n) = x · (x − 1) · · · (x − n + 1) .<br />

<br />

n<br />

a. Konstruer og afprøv den faldende potensfunktion i ML.<br />

b. Brug den faldende potensfunktion til at give endnu en definition af ( n k) fra opgave 5.2.<br />

Opgave 11.2 Matematikere kan vise, at talrækken<br />

<br />

1 + 1<br />

1<br />

1 <br />

,<br />

1 + 1<br />

2<br />

2 <br />

,<br />

1 + 1<br />

3<br />

3 <br />

, . . . ,<br />

1 + 1<br />

n<br />

n<br />

, . . .<br />

er voksende og kommer vilk˚arlig tæt p˚a e, grundtallet for den naturlige logaritme. Skriv<br />

passende hjælpefunktioner og et ML-udtryk, der bestemmer e med omkring tre decimalers<br />

nøjagtighed i den forstand, at det finder en s˚adan værdi (1 + 1<br />

10n )10n , at<br />

<br />

1 + 1<br />

10n <br />

− 1 +<br />

10n<br />

1<br />

n<br />

n<br />

< 0.0005<br />

[Vink: Skriv først en funktion til beregning af (1 + 1<br />

n )n .]<br />

Prøv at udføre potensopløftningerne med de forskellige funktioner fra teksten, og m˚al<br />

tidsforbruget. Følgen ((n + 1<br />

n )n )n=1,2,3,... konvergerer meget langsomt, og den beskrevne metode<br />

er ikke særlig god at beregne e efter. P˚a grund af afrundingsfejlene ved flydende regning<br />

vil de forskellige potensopløftningsfunktioner give forskellige resultater, og kun med en af de<br />

hurtige versioner er det muligt at finde flere end tre decimaler af e.<br />

157


Opgave 11.3 Hvad er der galt med denne definition?<br />

fun oploefttilhurt1 (x : real, n : int)<br />

= if n mod 2 = 0 then oploefttilhurt1 (x * x, n div 2)<br />

else if n = 0 then 1.0 else x * oploefttilhurt1 (x, n - 1);<br />

Opgave 11.4 Antag, vi vælger at m˚ale størrelsen af inddata (x, n) ved længden af n,<br />

forst˚aet som antallet af cifre, der skal bruges for at skrive tallet n ned. Hvad bliver da<br />

størrelsesordenerne af udførelsestiderne for oploefttil og oploefttilhurt i værste tilfælde?<br />

Opgave 11.5 I en af opgaverne i kapitel 8 skulle man programmere en version af udtagelsessortering,<br />

der satte det største listeelement bagest. Et s˚adant program vil naturligt<br />

komme til at indeholde en konstruktion af formen udtsort yr @ [y], som p˚a grund af<br />

den lange venstreoperand for @ ikke er særligt effektiv. Indfør en generaliseret funktion, der<br />

beregner udtsort yr @ xr, og programmer derved metoden uden brug af @.<br />

Opgave 11.6 Konstruer funktionen minout : int list -> int, som ud fra en liste af<br />

indbyrdes forskellige naturlige tal i lineær tid (som funktion af listens længde) beregner det<br />

mindste naturlige tal, der ikke er med i listen. De naturlige tal er de ikke-negative hele<br />

tal {0, 1, 2, . . .}. [Vink: Brug “del og hersk”, og udnyt oplysningen om, at den givne listes<br />

elementer er forskellige.]<br />

158


Kapitel 12<br />

Effektiv sortering<br />

12.1 Ineffektiv sortering<br />

Vi vælger at m˚ale en sorteringsmetodes effektivitet p˚a det antal sammenligninger mellem<br />

elementer, den kræver (i værste tilfælde) for at sortere en liste med n elementer 1 .<br />

De sorteringsmetoder (boblesortering, indsættelsessortering og udtagelsessortering), som<br />

blev omtalt i kapitel 8, foretog O(n) sammenligninger under hvert enkelt listegennemløb<br />

og (i værste tilfælde) O(n 2 ) sammenligninger i alt (husk formlen (11.18) for summen af en<br />

differensrække).<br />

I dette kapitel præsenteres nogle bedre metoder.<br />

12.2 En nedre grænse for sorteringstid<br />

Den form for sorteringsmetoder, vi her betragter, skal for hver af de n! mulige inddata,<br />

som kan dannes af n forskellige tal, opbygge uddata forskelligt. Det kan kun lade sig gøre,<br />

hvis metoderne foretager tilstrækkeligt mange sammenligninger til, at der kan dannes n!<br />

forskellige gennemløb af algoritmen. Da der er tale om binære sammenligninger, m˚a det<br />

gennemløb, som stiller flest spørgsm˚al, mindst stille log 2(n!) af dem. (Med 1 spørgsm˚al kan<br />

man f˚a 2 forskellige forløb, med 2 spørgsm˚al 4 forskellige forløb, . . . , med k spørgsm˚al 2 k<br />

forskellige forløb.)<br />

Her kan vi med fordel inddrage Stirlings formel (James Stirling 1764):<br />

<br />

n n √<br />

2πn < n! <<br />

e<br />

<br />

n n √ 1<br />

2πn(1 +<br />

e<br />

10n )<br />

Da logaritmen af venstresiden er O(n log n), indses det, at enhver sorteringsmetode, som<br />

bygger p˚a sammenligning mellem elementerne, mindst m˚a have tidskompleksitet O(n log n).<br />

Dette ræsonnement udtaler sig naturligvis ikke om, hvorvidt s˚a effektive sorteringsmetoder<br />

faktisk findes — kun, at det umuligt kan gøres bedre.<br />

1 Efter teorien for algoritmers kompleksitet burde man tage udgangspunkt i inddatas længde, m˚alt i bit.<br />

Efter denne teori skulle man alts˚a ogs˚a tage i betragtning, hvor meget plads det krævede at nedskrive de<br />

tal, som skulle sorteres.<br />

159


12.3 Forbedring<br />

Nøglen til bedre sorteringsmetoder er princippet “del og hersk” fra afsnit 11.7.<br />

Som opsummeret i figur 12.1 kan man sige, at b˚ade indsættelsessortering og udtagelsessortering<br />

først opdelte den givne liste, derefter kaldte metoden rekursivt og til sidst bearbejdede<br />

resultatet med en samlings-operation. Begge disse metoder h˚andterede et element<br />

ad gangen: I indsættelsessortering var opdelingsoperationen den simplest mulige, idet listen<br />

blot blev delt i hoved og hale; til gengæld krævede samlingsoperationen en korrekt indsættelse.<br />

For udtagelsessortering var det samlingen, som var simpel; til gengæld skulle opdeling<br />

finde det mindste element.<br />

Et element udskilles:<br />

indssort<br />

OPDEL<br />

hd tl<br />

indssort<br />

❘ ✠<br />

indsæt<br />

SAML<br />

udtsort<br />

OPDEL<br />

min rest<br />

udtsort<br />

❘ ✠<br />

::<br />

SAML<br />

indsættelsessortering udtagelsessortering<br />

Tvedeling:<br />

fletsort<br />

OPDEL<br />

venstre højre<br />

fletsort fletsort<br />

❘ ✠<br />

flet<br />

SAML<br />

12.4 Flettesortering<br />

kviksort<br />

OPDEL<br />

sm˚a store<br />

kviksort kviksort<br />

❘ ✠<br />

@<br />

SAML<br />

flettesortering kviksortering<br />

Figur 12.1: Fire sorteringsmetoder<br />

I stedet for som i det rekursive kald af indsættelsessortering blot at sortere resten af listen<br />

p˚anær et enkelt element er ideen i flettesortering (eng.:mergesort) at opdele den forelagte<br />

160


liste i to s˚a vidt muligt lige lange dele og sortere hver af dem. For at f˚a dannet en korrekt<br />

ordnet liste som resultat er det nødvendigt at sammenflette (eng.:merge) de to sorterede<br />

dele.<br />

Her er en hjælpefunktion til sammenfletning af to ordnede lister af reelle værdier:<br />

fun merge ([],yr) = yr<br />

| merge (xr,[]) = xr<br />

| merge (xxr as (x : real) :: xr,yyr as y :: yr)<br />

= if x


12.5 Kviksortering<br />

I flettesortering var opdelingsoperationen gjort simpel, men som vist i figur 12.1 kunne man<br />

ogs˚a forestille sig samle-operationen simpel. Hvis samling blot skal g˚a ud p˚a at hægte to<br />

lister sammen, m˚a opdeling skille listen i to dele, hvor ethvert element i den forreste del er<br />

mindre end ethvert element i den bageste.<br />

En værdi, man kan bruge til at skille med, kaldes en pivot (svingtap, omdrejningspunkt).<br />

Her er en hjælpefunktion, som for pivot p og oprindelig liste xr danner dellisterne best˚aende<br />

af henholdsvis elementerne ≤ p og elementerne > p fra den oprindelige liste xr:<br />

fun partition ( ,[]) = ([],[])<br />

| partition (pivot : real,x :: xr)<br />

= let val (xv,xh) = partition (pivot,xr)<br />

in if x


12.6 Effektiv sortering<br />

P˚a grund af de nødvendige administrative beregninger kan en af de naive metoder (indsættelses-<br />

eller udtagelsessortering) for korte tallister være hurtigere end de her nævnte metoder<br />

(flette- og kviksortering). I praksis kan det mest effektive derfor være hybride metoder, der<br />

kun bruger flette- eller kviksortering, indtil dellisterne er blevet passende korte, hvorefter<br />

der skiftes over til en naiv sorteringsmetode.<br />

Vi har her blot brugt sorteringsproblemet som udgangspunkt til illustration af algoritmekonstruktion<br />

og -analyse, men den faglige litteratur om sortering er særdeles omfattende. Der<br />

er andre effektive algoritmer end de her nævnte (for eksempel hobsortering (eng.:heapsort)),<br />

og hvis de værdier, som skal sorteres, varierer inden for et begrænset omr˚ade, er der helt<br />

andre metoder at tage i brug.<br />

12.7 Tilfældige tal<br />

Efter de forudg˚aende præsentationer f˚ar man lyst til at teste programmerne p˚a et passende<br />

stort datamateriale, men hvordan skaffer man sig det?<br />

Det ville være rart som udgangspunkt at kunne danne en liste p˚a for eksempel 10000<br />

eller 100000 tilfældige tal. Beregning og tilfældighed er imidlertid modsatte størrelser; ingeniørerne<br />

har netop bestræbt sig p˚a, at intet i computeren skal fungere tilfældigt! Undertiden<br />

klarer man sig ved at bruge det ur, som er indbygget i de fleste computere. Klokkeslættet<br />

i milli- eller mikrosekunder vil have en passende tilfældig værdi.<br />

12.7.1 Pseudo-tilfældige tal<br />

En anden m˚ade at illudere tilfældighed p˚a er det trick, der kaldes pseudo-tilfældige tal, og<br />

som g˚ar ud p˚a at danne en talrække, der “ser tilfældig ud” uden reelt at være det. Da de<br />

“tilfældige” tal kommer i en ganske bestemt p˚a forh˚and fastlagt rækkefølge, opn˚as samtidig<br />

det paradoksale, at eksperimenter, der beror p˚a tilfældige tal, kan gentages, hvilket kan være<br />

en stor fordel ved fejlfinding.<br />

Lineær kongruens er en hyppigt benyttet metode, der fastlægges af tre værdier: multiplikatoren<br />

a, konstanten c og modulus m. Ud fra et kim (eng.:seed) r0 dannes da talrækken<br />

r0, r1, r2, . . . ved hjælp af formlen<br />

rn+1 = (a · rn + c) mod m (12.2)<br />

Ikke alle værdier af a, c, m og r0 giver naturligvis tilfældige tal (tænk p˚a konsekvensen<br />

af a = 0 eller a = 1), og der bliver heller ikke uendeligt mange forskellige tal, for alle tallene<br />

vil ligge mellem 0 og m − 1, men for passende valg af a, c og m ser tallene tilfældige ud.<br />

En mulighed er a = 16807, c = 0 og m = 2147483647 = 2 31 − 1. Netop disse værdier<br />

benyttes i biblioteksmodulet Random, og ved at bruge dette færdigprogrammerede modul<br />

kan man alts˚a selv slippe for at administrere (12.2). (Da m overstiger det største heltal<br />

2 30 − 1 = 1073741823 i MosML, ville programmeringen ikke være s˚a ligetil.)<br />

Efter load "Random" vil en ny type være til r˚adighed, typen Random.generator af<br />

“tilfældigtalsgeneratorer”. At frembringe tilfældige tal er herefter en to-trins-proces: først<br />

163


skaffer man sig en tilfældigtalsgenerator, og derefter beder man den om de tilfældige tal.<br />

Tabel 12.1 viser de værdier, som er til r˚adighed i Random.<br />

navn type virkning<br />

newgenseed : real -> generator en ny tilfældigtalsgenerator med det givne<br />

kim<br />

newgen : unit -> generator en ny tilfældigtalsgenerator med kim fra systemuret<br />

random : generator -> real et tilfældigt tal i intervallet mellem 0.0<br />

randomlist : int * generator<br />

-> real list<br />

range : int * int<br />

-> generator -> int<br />

rangelist : int * int<br />

-> int * generator<br />

-> int list<br />

(medregnet) og 1.0 (ikke medregnet)<br />

en liste af n (værdien af førsteparameteren)<br />

tilfældige tal i intervallet mellem 0.0 (med-<br />

regnet) og 1.0 (ikke medregnet)<br />

range (fra,til) gen giver et tilfældigt heltal<br />

i intervallet mellem fra (medregnet) og<br />

til (ikke medregnet)<br />

range (fra,til) (n, gen) giver en liste af<br />

n tilfældige heltal i intervallet mellem fra<br />

(medregnet) og til (ikke medregnet)<br />

Tabel 12.1: De seks værdier i biblioteksmodulet Random. Husk at bruge lange navne<br />

(Random.generator, Random.newgen, osv.)<br />

Som man kan se, er der to funktioner til fremskaffelse af tilfældigtalsgeneratorer: en, hvor<br />

man selv angiver et kim, og en, hvor systemet danner det. Derudover er der funktioner til<br />

frembringelse af et enkelt og af en liste af reelle tal (i intervallet [0|1)) og til frembringelse<br />

af et enkelt og af en liste af hele tal (i et ønsket interval).<br />

Vi slutter af med at m˚ale kørtiden for flette- og kviksortering af et tilfældigt testmateriale:<br />

load "Random";<br />

val it = () : unit<br />

val talliste = Random.randomlist (10000,Random.newgen ());<br />

val talliste =<br />

[0.505257184387, 0.546112051954, 0.957907188198, 0.961680127756,<br />

. . .<br />

load "Mosml";<br />

val it = () : unit<br />

Mosml.time mergesort talliste;<br />

User: 1.648 System: 3.265 GC: 0.889 Real: 5.817<br />

val it =<br />

[7.53141939991E~5, 0.000278659630697, 0.000357734970915, 0.000388164539071,<br />

. . .<br />

Mosml.time quicksort talliste;<br />

User: 1.485 System: 0.297 GC: 1.492 Real: 3.283<br />

val it =<br />

[7.53141939991E~5, 0.000278659630697, 0.000357734970915, 0.000388164539071,<br />

. . .<br />

164


12.8 Sammenfatning<br />

De naive sorteringsmetoder har udførelsestid O(n 2 ). Ingen sorteringsmetode, der baserer sig<br />

p˚a elementsammenligninger, kan have lavere tidskompleksitet end O(n log n). Udførelsestiden<br />

for flettesortering er faktisk O(n log n). Kviksortering, som i almindelighed vil være den<br />

hurtigste metode, har kun en udførelsestid p˚a O(n log n) i gennemsnit, men O(n 2 ) i værste<br />

tilfælde.<br />

En lineær kongruensmetode til dannelse af pseudo-tilfældige tal er til r˚adighed i modulet<br />

Random.<br />

12.9 Opgaver<br />

Opgave 12.1 Da køretiden for listesammenstillen (afsnit 11.6.1) ikke er konstant, men<br />

lineært begrænset af længden af venstre operand, er forekomsten af @ i deludtrykket<br />

“quicksort xv @ x :: quicksort xh” uheldig. Generaliser quicksort til en funktion<br />

quickersort, hvor quickersort (xr,yr) inddrager fortsættelsen “. . . @ yr” og beregner<br />

quicksort xr @ yr. (Denne generalisering er helt analog til dannelsen af spejlforan ud<br />

fra spejl i afsnit 11.7.) Dan derved en mere effektiv sorteringsfunktion. Ved omskrivning til<br />

iterativ form ([5, Chapt. 17]) kan der opn˚as yderligere forbedringer, se eventuelt [12, Sect.<br />

3.20].<br />

Opgave 12.2 Bevis, at der for alle hele tal n gælder n div 2 + (n + 1) div 2 = n.<br />

Som flettesortering er fremstillet ovenfor, vil b˚ade length og splitAt i hvert rekursivt<br />

skridt gennemløbe argumentlisten. En m˚ade, hvorp˚a man kan integrere disse administrative<br />

operationer i selve sorteringen (og helt spare splitAt) er at definere en hjælpefunktion<br />

sort, s˚adan at sort (n,xr) = (yr,zr), hvor yr er en sorteret liste med de n forreste<br />

elementer fra xr, og zr er de øvrige elementer fra xr. Basistilfældene er n = 0 og n = 1,<br />

og for n ≥ 2 udnyttes identiteten bevist i begyndelsen af opgaven. Det ønskede resultat<br />

f˚as som førstekomponenten af parret sort (length xr,xr) (hvis andenkomponent vil være<br />

den tomme liste). Indtast programmet:<br />

local fun sort (0,xr) = ([],xr)<br />

| sort (1,x :: xr) = ([x],xr)<br />

| sort (n,xr)<br />

= let val (yr1,xr1) = sort (n div 2,xr)<br />

val (yr2,xr2) = sort ((n + 1) div 2,xr1)<br />

in (merge (yr1,yr2),xr2) end<br />

in fun mergesort’ xr = #1 (sort (length xr,xr)) end<br />

og m˚al efter, at der herved dannes en effektiv sorteringsfunktion.<br />

Opgave 12.3 Programmer en funktion find, s˚adan at find (xr,i) returnerer det i-temindste<br />

element i listen xr. Det kan gøres (C.A.R. Hoare 1962) væsentlig hurtigere end først<br />

at sortere listen og derefter vælge det i-te element. [Vink: brug partition.]<br />

165


166


Litteratur<br />

[1] Dansk Standardiseringsr˚ad: EDB-ordbog (redigeret af Bjørn Eriksen, Hans<br />

Jørgen Helms og Mogens D. Rømer), Dansk Standard DS 2049-1970, Gjellerup<br />

1971, ISBN 87 13 01220 7<br />

[2] Jørgen Hilden, Gustav Leunbach, Peter Naur: Forslag til tilføjelser til Edbordbog<br />

Dansk Standard DS 2049-1970, <strong>Københavns</strong> Universitet 1971<br />

[3] It-Terminologi-Udvalgets ordbog, http://www.dsn.dk/it-dansk/<br />

[4] Chr. Gram, J. Hald, H.B. Hansen, P. Naur, A. Wessel, Datamatik, Regnecentralen<br />

og Studentlitteratur 1968–70 (11 ud af 19 planlagte hæfter)<br />

[5] Michael Reichhardt Hansen, Hans Rischel: Introduction to Programming using<br />

SML, Addison-Wesley 1999, ISBN 0-201-39820-6<br />

[6] Greg Michaelson, Elementary Standard ML, UCL Press 1995, ISBN 1-85728-<br />

398-8<br />

[7] Robin Milner, Mads Tofte, and Robert Harper, The Definition of Standard ML,<br />

The MIT Press 1990, ISBN 0-262-13255-9 (paperback: ISBN 0-262-63132-6)<br />

[8] Robin Milner, Mads Tofte, Commentary on Standard ML, The MIT Press 1991,<br />

ISBN 0-262-13271-0 (paperback: ISBN 0-262-63137-7)<br />

[9] Robin Milner, Mads Tofte, Robert Harper, and David MacQueen, The Definition<br />

of Standard ML (Revised), The MIT Press 1997, ISBN 0-262-63181-4<br />

[10] Peter Naur, Concise Survey of Computer Methods, Studentlitteratur og Petrocelli/Charter<br />

1974<br />

[11] Peter Naur, Computing: A Human Activity, ACM Press 1992, ISBN 0-201-58069-<br />

1.<br />

[12] Lawrence C. Paulson, ML for the Working Programmer, second edition, Cambridge<br />

University Press 1996, ISBN 0 521 57050 6 (paperback: ISBN 0 521 56543<br />

X)<br />

[13] Sergei Romanenko, Claudio Russo and Peter Sestoft, Moscow ML Owner’s Manual,<br />

se hjemmesiden http://www.dina.kvl.dk/~sestoft/mosml.html<br />

167


[14] Sergei Romanenko, Claudio Russo and Peter Sestoft, Moscow ML Language<br />

Overview, se hjemmesiden http://www.dina.kvl.dk/~sestoft/mosml.html<br />

[15] Sergei Romanenko, Claudio Russo and Peter Sestoft, Moscow ML Library Documentation,<br />

se hjemmesiden http://www.dina.kvl.dk/~sestoft/mosml.html<br />

[16] The Unicode Consortium, The Unicode Standard, Version 2.0, Addison-Wesley<br />

Developers Press 1996, ISBN 0-201-48345-9 (CD-ROM indlagt)<br />

168


Bilag A<br />

Styretegn<br />

Styretegnenes tilsigtede virkning er forklaret i Dansk Standard DS/ISO 646 og ISO 8859-1.<br />

Her gives en kort oversættelse til dansk. Rækkefølgen er som i tabellerne 1.1 og 1.2.<br />

NUL Null Udfyldning af mediet eller af tid uden betydning for informationsindholdet.<br />

SOH Start of heading Første tegn i en meddelelses overskrift.<br />

STX Start of text Transmissionsstyretegn, der afslutter en overskrift og g˚ar forud for en<br />

tekst.<br />

ETX End of text Transmissionsstyretegn, der afslutter en tekst.<br />

EOT End of transmission Transmissionsstyretegn til angivelse af afslutningen p˚a en<br />

eller flere tekster.<br />

ENQ Enquiry Transmissionsstyretegn til anmodning om svar fra en fremmed modtager.<br />

ACK Acknowledge Bekræftende transmissionsstyretegn fra modtager til afsender.<br />

BEL Bell P˚akaldelse af opmærksomhed.<br />

BS Backspace Opstillingstegn, der flytter positionen en tegnposition baglæns i samme<br />

linje.<br />

HT Horizontal tabulation Opstillingstegn, der flytter positionen til næste forudbestemte<br />

tegnposition i samme linje.<br />

LF Line feed Opstillingstegn, der flytter positionen til samme tegnposition i næste linje.<br />

VT Vertical tabulation Opstillingstegn, der flytter positionen til samme tegnposition i<br />

næste forudbestemte linje.<br />

169


FF Form feed Opstillingstegn, der flytter positionen til samme tegnposition i en forudbestemt<br />

linje p˚a næste blanket eller side.<br />

CR Carriage return Opstillingstegn, der flytter positionen til første tegnposition i samme<br />

linje.<br />

SO Shift-out Benyttes sammen med SI til at udvide repertoiret af grafiske tegn, idet det<br />

fungerer som l˚asende skiftetegn, der ændrer betydning for koder mellem 33 og 126 indtil<br />

næste forekomst af SI.<br />

SI Shift-in Benyttes sammen med SO til at udvide repertoiret af grafiske tegn, idet det<br />

fungerer som l˚asende skiftetegn for tilbagevenden til standardbetydningen af koder mellem<br />

33 og 126.<br />

DLE Data link escape Transmissionsstyretegn, der giver et begrænset antal efterfølgende<br />

tegn (som skal være grafiske eller transmissionsstyretegn) ændret betydning. Med andre<br />

ord et parametriseret tranmissionsstyretegn, der kan give supplerende transmissionsstyring.<br />

DC1 Device control 1 Enhedsstyretegn fortrinsvis som signal til at tænde en enhed eller<br />

sætte den i en grundtilstand.<br />

DC2 Device control 2 Enhedsstyretegn fortrinsvis som signal til at tænde en enhed eller<br />

sætte den i en specialtilstand.<br />

DC3 Device control 3 Enhedsstyretegn fortrinsvis som signal til at slukke en enhed eller<br />

sætte den i vente-tilstand.<br />

DC4 Device control 4 Enhedsstyretegn fortrinsvis som signal til at slukke eller afbryde<br />

en enhed.<br />

NAK Negative acknowledge Afvisende transmissionsstyretegn fra modtager til afsender.<br />

SYN Synchronous idle Transmissionsstyretegn til opn˚aelse (eller bevaring) af synkron<br />

transmission.<br />

ETB End of transmission block Transmissionsstyretegn til afslutning af en datablok<br />

(til brug i de tilfælde, hvor data er opdelt i blokke).<br />

CAN Cancel Alene eller eventuelt i forbindelse med efterfølgende tegn signal til, at forudg˚aende<br />

data har været fejlagtige.<br />

EM End of medium Afslutning p˚a et medium rent fysisk eller p˚a den benyttede eller<br />

ønskede del af det.<br />

170


SUB Substitute character Erstatning for et ugyldigt eller fejlagtigt tegn.<br />

ESC Escape Benyttes til yderligere styrefunktioner, idet det ændrer betydningen af et<br />

begrænset antal efterfølgende koder.<br />

FS File separator (IS4 Information separator 4) Filskilletegn.<br />

GS Group separator (IS3 Information separator 3) Gruppeskilletegn.<br />

RS Record separator (IS2 Information separator 2) Postskilletegn.<br />

US Unit separator (IS1 Information separator 1) Enhedsskilletegn.<br />

SP Space Blanktegn; opstillingstegn, der flytter positionen en tegnpositionen frem i samme<br />

linje. (Kan ogs˚a opfattes som et grafisk tegn, der ikke skriver noget.)<br />

DEL Delete Slettetegn; beregnet til overhulning, men kan ogs˚a anvendes til udfyldning<br />

af mediet eller af tid uden betydning for informationsindholdet.<br />

NBSP No-break space Blanktegn, hvor der ikke m˚a være linjeskift.<br />

SHY Soft hyphen Bindestreg til brug i orddeling ved linjeskift. (Egentlig et grafisk tegn<br />

og ikke et styretegn.)<br />

171


172


Bilag B<br />

Engelsk-dansk ordliste<br />

Apparater og metoder inden for informationsteknik er i overvejende grad kommet til os fra<br />

Storbritannien og USA, og hele it-omr˚adet er derfor stærkt p˚avirket af engelsk terminologi.<br />

N˚ar engelske ord optages i dansk (som for eksempel “sweater” eller “weekend”), skal de<br />

imidlertid følge danske udtaleregler og bøjningsmønstre, hvis det ikke skal virke kunstigt og<br />

lyde grimt. For ukritiske sprogbrugere best˚ar desuden risikoen for den kortslutning, at en<br />

engelsk term “oversættes” til en dansk, der lyder liges˚adan, snarere en til en, der betyder<br />

det samme.<br />

Eksempler p˚a “kortslutningsoversættelser”: Engelsk eventually betyder noget i retning<br />

af “til syvende og sidst” og ikke “eventuelt”; control skal som regel oversættes til “styre”<br />

snarere end “kontrollere”; technology betyder snarere “teknik” end “teknologi”; set skal i<br />

matematisk sammenhæng oversættes til “mængde” og ikke til “sæt” (som hedder tuple p˚a<br />

engelsk), og s˚a fremdeles.<br />

I 1960’erne og 1970’erne udfoldedes store bestræbelser p˚a udvikling af en dansk edbterminologi<br />

(se for eksempel [1] og [2]), men det var svært at holde trit med den hastige<br />

fremkomst af nye fænomener inden for it-omr˚adet, og ordbøgerne blev ikke udbredt til de<br />

mange selvbestaltede miljøer, som opstod uden for undervisningssektoren. Overordnet m˚a<br />

man derfor nu nok indse, at bestræbelserne har været en fiasko. Det er for eksempel ikke<br />

lykkedes at f˚a den langt mere mundrette og præcise betegnelse “datamat” til at erstatte<br />

computer 1 .<br />

Dansk Sprognævn, som blandt andet regulerer den officielle retskrivning, ser det ikke<br />

som sin opgave at foreskrive en bestemt sprogbrug, men observerer blot sprogets faktiske<br />

udvikling. Et underudvalgs iagttagelser kan ses p˚a netadressen [3].<br />

P˚a kurset “Funktionsprogrammering” benyttes den engelsksprogede lærebog [5] sammen<br />

med undervisningsmateriale og forelæsninger p˚a dansk. Form˚alet med den foreliggende ordliste<br />

er at fastlægge en fælles dansk terminologi blandt kursets lærere og instruktorer, ligesom<br />

de studerende forventes at bruge denne terminologi i deres opgavebesvarelser.<br />

Nedenfor følger to ordlister, som dels er ordnet efter ordenes forekomst i lærebogen, dels<br />

alfabetisk efter den engelske term. Ordlisterne medtager stort set kun ord, der har en særlig<br />

betydning i matematisk eller datateknisk sammenhæng; andre ord m˚a søges i en almindelig<br />

1 En række danske it-kapaciteter betingede deres medvirken til udarbejdelse af “Den store danske Encyklopædi”<br />

af, at hovedopslagsordet skulle være “datamaskine” eller “datamat”, men redaktøren foretrak<br />

“computer”.<br />

173


engelsk-dansk ordbog.<br />

B.1 Ordliste for de enkelte afsnit i lærebogen<br />

Ordlisten dækker kun kapitel 1–8, hvori størsteparten af de faglige termer forekommer.<br />

Indgangene har formen p: engelsk term = oversættelse(r), hvor p er sidetallet i [5, første<br />

udgave, første trykning]. Almindeligvis anføres kun første forekomst af hver term i en given<br />

betydning.<br />

Hvor der kan være tvivl om ordklassen, bruges forkortelserne vb. for verbum (udsagnsord),<br />

sb. for substantiv (navneord) og adj. for adjektiv (tillægsord).<br />

B.1.1 Chapter 1: Getting started<br />

1: program (vb.) = programmere, 1: program (sb.) = program, 1: programming language<br />

= programmeringssprog, 1: value = værdi, 1: expression = udtryk, 1: declaration =<br />

erklæring, 1: recursive = rekursiv, 1: type = type, 1: bind (vb.) = binde, 1: environment<br />

= omgivelser, 1: evaluation = evaluering, udregning, 1: implementation = implementering,<br />

2: execute = udføre, 2: compilation = oversættelse, 2: tuple = sæt, tupel, 2: conversion =<br />

omsætning, 2: input (sb.) = ind(gangs)data, 2: output (sb.) = ud(gangs)data.<br />

Values, types, identifiers and declarations<br />

2: identifier = identifikator, navn, 2: interface = grænseflade, 2: arithmetic (adj.) = aritmetisk,<br />

regne-, 2: arithmetic expression = regneudtryk, aritmetisk udtryk, 2: key = tast,<br />

2: the return key = linjeskiftstasten, 2: character = tegn, 2: string = tekst, 2: heading<br />

string = indledende tekst, 2: prompt (sb.) = prompt, klarsymbol, rykker, 2: prompt (vb.)<br />

= prompte, tilskynde, 3: integer (sb.) = heltal, 3: integer (adj.) = heltallig, heltals-, 3: set<br />

(sb.) = mængde, 3: truth value = sandhedsværdi.<br />

Simple function declarations<br />

4: subset = delmængde, 4: argument = argument, 4: argument pattern = argumentmønster,<br />

4: formal parameter = formel parameter, 4: apply to = anvende p˚a, kalde med.<br />

Comments<br />

4: comment (sb.) = kommentar.<br />

Recursion<br />

5: factorial function = fakultetsfunktion, 6: clause = klausul, erklæringstilfælde, 6: substitute<br />

(vb.) = substituere, indsætte, 7: base case = basistilfælde, grundtilfælde, 7: match<br />

(vb.) = matche, 7: skip (vb.) = skippe, overspringe, 8: memory(=storage) = lager, 9: italic<br />

(font) = kursiv.<br />

174


The power function<br />

10: power function = potensopløftning.<br />

About types and type checking<br />

11: infer = aflede, inferere, 11: error message = fejlmelding, fejlmeddelelse.<br />

Bindings and environments<br />

12: an environment = omgivelser.<br />

Exercises<br />

13: Fibonacci number = Fibonaccital.<br />

B.1.2 Chapter 2: Basic values and operators<br />

15: number (sb.) = tal, antal, 15: character = tegn, 15: string = tekst, 15: truth value =<br />

sandhedsværdi.<br />

Integers and reals<br />

16: computer = datamat, datamaskine, computer, 16: instruction = ordre, 16: machine<br />

instruction = maskinordre, 16: prefix (vb.) = foranstille, 16: monadic = monadisk, unær,<br />

16: dyadic = dyadisk, binær.<br />

Expressions, precedence, association<br />

17: infix (adj.) = infiks-, mellemstillet, 17: infix notation = infiksnotation, mellemstillet<br />

notation, 17: precedence = prioritet, 17: (operator) precedence = operatorprioritet, 17:<br />

(operator) association = forbindelse, associeringsretning, 18: associate to the left/the right<br />

= associere fra venstre/højre.<br />

Euclid’s algorithm<br />

18: greatest common divisor = største fælles divisor, største fælles m˚al.<br />

Evaluations with environments<br />

21: subexpression = deludtryk.<br />

Characters and strings<br />

22: special character = specialtegn, 22: punctuation (sb.) = tegnsætning, 22: punctuation<br />

(adj.) = skille-, tegnsætnings-, 22: control (vb.) = styre, 22: control (adj.) = styre-, 22:<br />

encode = indkode, kode, 22: quote (sb.) = anførelsestegn, 22: space character = blanktegn,<br />

175


22: space = mellemrum, 22: new line (character) = linjeskift(stegn), 22: backslash (character)<br />

= spejlvendt skr˚astreg, omvendt skr˚astreg, 22: escape sequence = udskiftningssekvens,<br />

udskiftningsfølge, 24: lexicographical = leksikografisk, 24: conversion = omsætning.<br />

Truth values<br />

25: predicate = prædikat, 25: negation = benægtelse, negering, 25: propositional logic =<br />

udsagnslogik, udsagnskalkule, 25: lower case letter = lille bogstav.<br />

Overloaded operators<br />

27: overload = overlæsse, 27: default (vb.) = falde tilbage.<br />

Type inference<br />

27: type inference = typeafledning, typeinferens.<br />

Exercises<br />

28: binomial coefficient = binomialkoefficient, 30: prime number = primtal.<br />

B.1.3 Chapter 3: Tuples and records<br />

31: tuple = sæt, tupel, 31: record (sb.) = post.<br />

Tuples<br />

34: selector = selektor, projektion.<br />

Tuple patterns<br />

34: pattern matching = mønstertilpasning, 35: wild card (sb.) = joker, 35: wild card (adj.)<br />

= joker-, 35: wild card pattern = jokermønster.<br />

Infix functions on pairs<br />

35: infix status = infiksstatus, mellemstillingsstatus, 35: directive = direktiv, 36: scope =<br />

virkefelt, 36: Cartesian = kartesisk.<br />

Records<br />

39: record (sb.) = post, 39: label (sb.) = etiket(te), feltnavn, 39: field = felt.<br />

Record patterns<br />

41: wild card mark = jokermærke.<br />

176


Type declarations<br />

42: quadratic equation = andengradsligning, 43: exception = undtagelse, 43: raise an<br />

exception = kaste en undtagelse, hejse en undtagelse, 44: equality type = lighedstype.<br />

Exercises<br />

47: time of day = klokkeslæt, 48: inverse wrt 2 addition = modsatte, additivt inverse,<br />

48: inverse wrt 2 multiplication = reciprokke, multiplikativt inverse, 48: mirror (vb.) =<br />

spejlvende, spejle.<br />

B.1.4 Chapter 4: Problem solving I<br />

Solution 1<br />

52: irreducible fraction = uforkortelig brøk, 52: cancel = bortforkorte.<br />

B.1.5 Chapter 5: Lists<br />

60: sequence = sekvens, følge, 60: polymorphic = polymorf, 60: equality type = lighedstype.<br />

Building lists<br />

61: type constructor = typekonstruktør, 62: postfix (adj.) = postfiks-, efterstillet, 62: postfix<br />

notation = postfiksnotation, efterstillet notation, 62: subtree = deltræ, undertræ.<br />

Append and reverse; polymorphic types<br />

69: append (vb.) = tilføje, 69: called ‘append’ = benævnt ‘tilføj’, 70: reverse (vb.) =<br />

spejlvende, 70: called ‘reverse’ = benævnt ‘spejlvend’, 70: polymorphic = polymorf, 70:<br />

monomorphic = monomorf, 70: polymorphism = polymorfi, 71: efficient = effektiv.<br />

Membership; equality types<br />

76: equality type variable = lighedstypevariabel, variabel af lighedstype.<br />

Exercises<br />

80: prime number = primtal, 80: palindrome = palindrom.<br />

B.1.6 Chapter 6: Problem solving II<br />

B.1.7 Chapter 7: Tagged values and partial functions<br />

90: tag (vb.) = afmærke, 90: tag (sb.) = mærke, 90: square = kvadrat, 90: triangle =<br />

trekant.<br />

2 with respect to<br />

177


Datatype declarations<br />

91: value constructor = værdikonstruktør.<br />

The case-expression<br />

93: match (sb.) = tilpasning, match, 94: nest (vb.) = indlejre.<br />

Enumeration types<br />

94: enumerate = opregne, 94: enumeration type = opregningstype.<br />

Partial functions: the option datatype<br />

95: proper subset = ægte delmængde.<br />

Exception handling<br />

98: exception constructor = undtagelseskonstruktør, 99: catch an exception = gribe en undtagelse,<br />

fange en undtagelse, 99: exception handler = undtagelsesvogter 3 , handle-udtryk,<br />

99: match (sb.) = tilpasning, match.<br />

The Eight Queens problem<br />

100: backtracking = blindgydesøgning, 100: backtrack (vb.) = søge (samme vej) tilbage.<br />

Exercises<br />

102: mark = karakter, 102: pass (vb.) = best˚a, 102: day care centre = daginstitution,<br />

102: day nursery = vuggestue, 102: nursery school = børnehave, 102: recreation centre =<br />

fritidshjem, 103: union = foreningsmængde, 103: intersection = fællesmængde.<br />

B.1.8 Chapter 8: Finite trees<br />

104: Chinese boxes = kinesiske æsker, 104: ancestor = forfader, ane, 104: node = knude,<br />

104: circuit = kredsløb.<br />

Chinese boxes<br />

110: layered pattern = mønstersynonym.<br />

Trees of ancestors; traversal of a tree<br />

118: traverse = gennemløbe, 118: pre-order traversal = præordensgennemløb, gennemløb i<br />

præorden.<br />

3 ikke alment accepteret fordanskningsforslag<br />

178


Mutual recursion<br />

120: mutual = gensidig.<br />

Parameterized datatypes<br />

123: instance = udgave, specialtilfælde, instans, 123: leaf = blad, 123: node = knude.<br />

Exercises<br />

132: propositional logic = udsagnslogik, udsagnskalkule, 133: calculator = regnemaskine,<br />

133: instruction = ordre.<br />

B.2 Engelsk-dansk ordliste<br />

Listen omfatter fagtermerne fra de første otte kapitler i [5] for s˚a vidt ang˚ar ordet i den<br />

angivne kontekst (de anførte sidenumre gælder første udgave, første trykning). Listen<br />

kan p˚a ingen m˚ade erstatte en ordbog: Alternative betydninger angives ikke, og ordlisten<br />

medtager overvejende ord af særlig datateknisk eller matematisk interesse.<br />

ancestor 104 forfader, ane<br />

append (vb.) 69 tilføje<br />

called ‘append’ 69 benævnt ‘tilføj’<br />

apply to 4 anvende p˚a, kalde med<br />

argument 4 argument<br />

argument pattern 4 argumentmønster<br />

arithmetic (adj.) 2 aritmetisk, regnearithmetic<br />

expression 2 regneudtryk, aritmetisk udtryk<br />

associate to the left/the right 18 associere fra venstre/højre<br />

(operator) association 17 forbindelse, associeringsretning<br />

backslash (character) 22 spejlvendt skr˚astreg, omvendt skr˚astreg<br />

backtrack (vb.) 100 søge (samme vej) tilbage<br />

backtracking 100 blindgydesøgning<br />

base case 7 basistilfælde, grundtilfælde<br />

bind (vb.) 1 binde<br />

binomial coefficient 28 binomialkoefficient<br />

calculator 133 regnemaskine<br />

cancel 52 bortforkorte<br />

Cartesian 36 kartesisk<br />

catch an exception 99 gribe en undtagelse, fange en undtagelse<br />

character 2, 15 tegn<br />

Chinese boxes 104 kinesiske æsker<br />

circuit 104 kredsløb<br />

clause 6 klausul, erklæringstilfælde<br />

179


computer 16 datamat, datamaskine, computer<br />

comment (sb.) 4 kommentar<br />

compilation 2 oversættelse<br />

control (vb.) 22 styre<br />

control (adj.) 22 styreconversion<br />

2, 24 omsætning<br />

day care centre 102 daginstitution<br />

day nursery 102 vuggestue<br />

declaration 1 erklæring<br />

default (vb.) 27 falde tilbage<br />

directive 35 direktiv<br />

dyadic 16 dyadisk, binær<br />

efficient 71 effektiv<br />

encode 22 indkode, kode<br />

enumerate 94 opregne<br />

enumeration type 94 opregningstype<br />

environment 1 omgivelser<br />

an environment 12 omgivelser<br />

equality type 44, 60 lighedstype<br />

equality type variable 76 lighedstypevariabel, variabel af lighedstype<br />

error message 11 fejlmelding, fejlmeddelelse<br />

escape sequence 22 udskiftningssekvens, udskiftningsfølge<br />

evaluation 1 evaluering, udregning<br />

exception 43 undtagelse<br />

exception constructor 98 undtagelseskonstruktør<br />

exception handler 99 undtagelsesvogter, handle-udtryk<br />

execute 2 udføre<br />

expression 1 udtryk<br />

factorial function 5 fakultetsfunktion<br />

Fibonacci number 13 Fibonaccital<br />

field 39 felt<br />

formal parameter 4 formel parameter<br />

greatest common divisor 18 største fælles divisor, største fælles m˚al<br />

heading string 2 indledende tekst<br />

identifier 2 identifikator, navn<br />

implementation 1 implementering<br />

infer 11 aflede, inferere<br />

infix (adj.) 17 infiks-, mellemstillet<br />

infix notation 17 infiksnotation, mellemstillet notation<br />

infix status 35 infiksstatus, mellemstillingsstatus<br />

input (sb.) 2 ind(gangs)data<br />

instance 123 udgave, specialtilfælde, instans<br />

instruction 16, 133 ordre<br />

integer (sb.) 3 heltal<br />

integer (adj.) 3 heltallig, heltals-<br />

180


interface 2 grænseflade<br />

intersection 103 fællesmængde<br />

inverse wrt addition 48 modsatte, additivt inverse<br />

inverse wrt multiplication 48 reciprokke, multiplikativt inverse<br />

irreducible fraction 52 uforkortelig brøk<br />

italic (font) 9 kursiv<br />

key 2 tast<br />

label (sb.) 39 etiket(te), feltnavn<br />

leaf 123 blad<br />

lexicographical 24 leksikografisk<br />

lower case letter 25 lille bogstav<br />

machine instruction 16 maskinordre<br />

mark 102 karakter<br />

match (vb.) 7 matche<br />

match (sb.) 93, 99 tilpasning, match<br />

memory(=storage) 8 lager<br />

mirror (vb.) 48 spejlvende, spejle<br />

monadic 16 monadisk, unær<br />

monomorphic 70 monomorf<br />

mutual 120 gensidig<br />

negation 25 benægtelse, negering<br />

nest (vb.) 94 indlejre<br />

new line (character) 22 linjeskift(stegn)<br />

node 104, 123 knude<br />

number (sb.) 15 tal, antal<br />

nursery school 102 børnehave<br />

output (sb.) 2 ud(gangs)data<br />

overload 27 overlæsse<br />

palindrome 80 palindrom<br />

pass (vb.) 102 best˚a<br />

pattern matching 34 mønstertilpasning<br />

layered pattern 110 mønstersynonym<br />

polymorphic 60, 70 polymorf<br />

polymorphism 70 polymorfi<br />

postfix (adj.) 62 postfiks-, efterstillet<br />

postfix notation 62 postfiksnotation, efterstillet notation<br />

power function 10 potensopløftning<br />

precedence 17 prioritet<br />

(operator) precedence 17 operatorprioritet<br />

predicate 25 prædikat<br />

prefix (vb.) 16 foranstille<br />

pre-order traversal 118 præordensgennemløb, gennemløb i præorden<br />

prime number 30, 80 primtal<br />

program (vb.) 1 programmere<br />

program (sb.) 1 program<br />

programming language 1 programmeringssprog<br />

181


prompt (sb.) 2 prompt, klarsymbol, rykker<br />

prompt (vb.) 2 prompte, tilskynde<br />

proper subset 95 ægte delmængde<br />

propositional logic 25, 132 udsagnslogik, udsagnskalkule<br />

punctuation (sb.) 22 tegnsætning<br />

punctuation (adj.) 22 skille-, tegnsætningsquadratic<br />

equation 42 andengradsligning<br />

quote (sb.) 22 anførelsestegn<br />

raise an exception 43 kaste en undtagelse, hejse en undtagelse<br />

record (sb.) 31, 39 post<br />

recreation centre 102 fritidshjem<br />

recursive 1 rekursiv<br />

the return key 2 linjeskiftstasten<br />

reverse (vb.) 70 spejlvende<br />

called ‘reverse’ 70 benævnt ‘spejlvend’<br />

scope 36 virkefelt<br />

selector 34 selektor, projektion<br />

sequence 60 sekvens, følge<br />

set (sb.) 3 mængde<br />

skip (vb.) 7 skippe, overspringe<br />

space 22 mellemrum<br />

space character 22 blanktegn<br />

special character 22 specialtegn<br />

square 90 kvadrat<br />

string 2, 15 tekst<br />

subexpression 21 deludtryk<br />

subset 4 delmængde<br />

substitute (vb.) 6 substituere, indsætte<br />

subtree 62 deltræ, undertræ<br />

tag (vb.) 90 afmærke<br />

tag (sb.) 90 mærke<br />

time of day 47 klokkeslæt<br />

traverse 118 gennemløbe<br />

triangle 90 trekant<br />

truth value 3, 15 sandhedsværdi<br />

tuple 2, 31 sæt, tupel<br />

type 1 type<br />

type constructor 61 typekonstruktør<br />

type inference 27 typeafledning, typeinferens<br />

union 103 foreningsmængde<br />

value 1 værdi<br />

value constructor 91 værdikonstruktør<br />

wild card (sb.) 35 joker<br />

wild card (adj.) 35 jokerwild<br />

card pattern 35 jokermønster<br />

wild card mark 41 jokermærke<br />

182


Bilag C<br />

Indeks<br />

absolut værdi 27 DS 2089 19<br />

afprøvning 82 efterladende 51<br />

afviklingsprogram 14 Euklid 48<br />

alfabet 11,16 evaluering 15<br />

analog repræsentation 11 fakultetsfunktionen 71,77,85<br />

analoge data 11 flettesortering 160<br />

analogregnemaskine 11 fortolke 14<br />

applikativ programmering 13 funktionsorienteret programmering 13<br />

applikativt programmeringssprog 15 funktionsorienteret programmeringssprog 15<br />

ASCII 17 Gauß, Karl Friedrich 140<br />

associering 29 grafem 16<br />

Babbage, Charles (1792–1871) 13 grundsymbol 55<br />

binære talsystem 11 Hanois t˚arn 85<br />

bit 11 Hoare, Charles Antony Richard 162<br />

blanktegn 17 højere programmeringssprog 14<br />

blindgydesøgning 140 imperativ programmering 13<br />

boblesortering 113 imperativt programmeringssprog 15<br />

Boole,George (1815–1864) 28 implementere 14<br />

bund 72 inddata 10<br />

byte 19 indgangsdata 10<br />

Church, Alonzo (1903–95) 13 indsættelsessortering 113<br />

Curry, Haskell Brooks (1900–82) 93 ISO 646 17,169<br />

data 9 ISO 8859 19,169<br />

databehandling 10 ivrig 51<br />

datamaskine 14 klar-symbol 25<br />

datamat 14 kode 16<br />

datamatik 14 kombination 89<br />

dataproces 10 kommentar 81<br />

deklarativ programmering 15 kontekst 14<br />

differensrække 154 kviksortering 162<br />

digital repræsentation 11 lambdaudtryk 96<br />

digitale data 11 lineær kongruens 163<br />

183


logikprogrammering 15 skiftenøgle 18<br />

logisk værdi 28 skiftetegn 18<br />

Lovelace, Augusta Ada (1815–1851) 13 skrifttegn 16<br />

maskinsprog 14 sortering 111<br />

McCarthy, John 13 Stirlings formel 159<br />

ML 25 striks 51<br />

monadisk minus 27 syntaksfejl 82<br />

monadisk plus 26 sæt 33<br />

von Neumann, John (1903–1957) 14 tal 11<br />

numerisk værdi 27 talord 11<br />

objektorienteret programmering 15 tegn 11<br />

ord 11 tegnsæt 16<br />

oversætte 14 tekstbehandlingsprogram 79<br />

Pascal, Blaise (1623–1662) 12,89 tilstandsorienteret programmering 13<br />

permutation 71,77 tilstandsorienteret programmeringssprog 15<br />

polymorfi 64 top-niveau-erklæring 26<br />

position 11 totalssystemet 11<br />

prioritet 29 typebegrænsning 54<br />

program 12 typevariabel 55<br />

programbevis 82 uddata 10<br />

programmere 12,13 udgangsdata 10<br />

programmeringsstil 80 udtagelsessortering 113<br />

pseudo-tilfældige tal 163 uegentlig værdi 72<br />

redigeringsprogram 79 uendelig løkke 72<br />

rekursiv 77 unit 62<br />

reserveret ord 55 universel 12,25<br />

rystesortering 114 variabelbaseret programmering 13<br />

sandhedsværdi 27 vægt 16<br />

Schönfinkel, Moses 93 værdiorienteret programmering 13<br />

semantisk fejl 82 værdiorienteret programmeringssprog 15<br />

simulering 10 værdipolymorfi 69<br />

184

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!