FP-2: Supplerende noter i funktionsprogrammering - Københavns ...
FP-2: Supplerende noter i funktionsprogrammering - Københavns ...
FP-2: Supplerende noter i funktionsprogrammering - Københavns ...
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 < og >. Det betyder igen, at og-tegnet (&)<br />
ikke kan bruges, men kodes &. 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 ". 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 />
& &<br />
< <<br />
> ><br />
" (i direktivparametre) "<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 § — men for bogstaverne er der ogs˚a<br />
mnemotekniske koder som vist i nedenst˚aende tabel.<br />
HTML HTML HTML HTML HTML HTML<br />
À À È È Ì Ì Ò Ò Ù Ù Ç Ç<br />
Á Á É É Í Í Ó Ó Ú Ú ´ Y Ý<br />
Â Â Ê Ê Î Î Ô Ô Û Û D- Ð<br />
Ã Ã Õ Õ Ñ Ñ<br />
Ä Ä Ë Ë Ï Ï Ö Ö Ü Ü<br />
˚A Å Æ Æ Ø Ø bp Þ<br />
à à è è ì ì ò ò ù ù ç ç<br />
á á é é í í ó ó ú ú ´y ý<br />
â â ê ê î î ô ô û û d- ð<br />
ã ã õ õ ñ ñ<br />
ä ä ë ë ï ï ö ö ü ü ¨y ÿ<br />
˚a å æ æ ß ß ø ø bp þ<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