programmeertalen en -paradigma`s

advertisement
PROGRAMMEERTALEN
EN -PARADIGMA’S
Jeroen Fokker
Informatica-instituut
Universiteit Utrecht
Willem van der Vegt
Chr. Hogeschool Windesheim
Zwolle
1
2
INHOUDSOPGAVEInleiding
Geschiedenis van programmeertalen
Programmeer-paradigma’s
IMPERATIEF PROGRAMMEREN
Variabelen en toekenning
Imperatief programmeren
Waarden
Geheugen
Opdrachten
Imperatieve computertaal
Notatie voor variabelen
Notatie voor waarden
Notatie voor toekenning
Berekeningen
Type van variabelen
Programma
Procedures
Programmastructuur
Subroutines vs. procedures : voorbeelden
Subroutines vs. procedures
Declaraties van variabelen
Lokale variabelen
Parameters
Parameters: variaties
Programmastructuur
Keuze en herhaling
Keuze (1)
Keuze (2)
Herhaling (1)
Herhaling (2)
Structured programming
Herhaling (3)
Functies
Procedures vs. functies
Procedures vs. functies: voorbeelden
Aanroepen van functies
Recursieve functies
Datatypes
Arrays
Types
Records/structures
Abstracte datatypes
Voorbeeld ADT
Object-oriëntatie
Voorbeeld OO-programmeren
Subklassen
Overerving (inheritance)
Pointers
Dynamische datastructuren
Gebruik van pointers (Pascal)
Gebruik van pointers (C++)
Gebruik van pointers (Java)
Toepassing van pointers
DECLARATIEF PROGRAMMEREN
Programmeer-paradigma’s
Declaratief programmeren
Functioneel programmeren
Functiedefinitie
Keuze
Herhaling
Lijsten
Polymorfie
Hogere-ordefuncties
Algoritmen: insertion sort
Algoritmen: quicksort
Logisch programmeren
Feiten en afleidingsregels
Uitvoeren van een programma
Voorbeeld
APPENDIX
Reflectievragen
Praktijkopdrachten
3
Deze (korte) cursus biedt een overzicht van een aantal concepten van hedendaagse
programmeertalen. Om enige orde te scheppen in de vele talen worden deze
ingedeeld in een zestal zogenoemde paradigma’s.
Deze cursus is geen programmeer-cursus: daarvoor is er de module “Visueel
programmeren met Java”, waarin het gebruik van de taal Java in detail wordt
behandeld. In de voorliggende cursus worden de voorbeelden deels geput uit Java,
maar ook uit C, C++, Pascal, Basic, machinetaal, Haskell en Prolog.
In zo’n kort bestek als deze kunnen de genoemde talen natuurlijk niet volledig
behandeld worden, maar dat is ook helemaal niet nodig. Aan de hand van de
beschreven concepten wordt het hopelijk veel eenvoudiger allerlei eigenschappen
van deze talen te kunnen plaatsen (ten opzichte van Java) en op hun waarde te
kunnen schatten. Dat maakt het ook mogelijk om tegenover leerlingen meer
beslagen ten ijs te komen. Het is bovendien geod mogelijk dat deze cursus ook het
begrip van de taal Java zelf verhoogt.
Dit boekje is opgebouwd rond een vijftigtal overhead-transparanten, die tegelijk
een samenvatting van de inhoud vormen. Acherin het boekje is bij vrijwel elk
onderwerp een “reflectievraag” opgenomen; het (proberen te) beantwoorden
daarvan kan leiden tot beter begrip van de stof, en kan bovendien dienen als
ideeënbron voor discussies in de klas-situatie. Daarna volgt een 12-tal
praktijkopdrachten. Ook deze dienen in eerste instantie ter eigen lering, maar
kunnen voor een deel ook worden uitgewerkt en aangepast tot projectjes voor
leerlingen.
Bij het boekje hoort een CD met vrij kopieerbare versies van compilers voor
diverse programmeertalen. Ze zijn niet allemaal van productiekwaliteit (maar
sommige wel!), maar ze zijn in ieder geval geschikt voor enig experimenteren. Op
de CD staan bovendien de Powerpoint-file van de transparanten, en de Word-file
van dit boekje. Ook dit materiaal mag vrij gebruikt, gereproduceerd en aangepast
worden voor gebruik in het Nederlandse voortgezet onderwijs.
Wij wensen u veel aha-ervaringen toe bij het bestuderen van deze cursus!
Jeroen Fokker
Informatica-instituut
Universiteit Utrecht
[email protected]
4
Willem van der Vegt
Chr. Hogeschool Windesheim
Zwolle
[email protected]
Programmeertalen werden al in de 1930’s, bij wijze van gedachten-experiment,
ontworpen. Maar de eerste echte talen onstonden natuurlijk pas tegelijk met de
computer, rond 1946. Aanvankelijk had elk type computer zijn eigen taal: de
machinetaal voor die specifieke computer. Omdat het programmeren met behulp
van code-getallen voor instructies erg lastig is, werden er al snel assembler-talen
gebruikt, waarin de instructies met afkortingen werden weergegeven.
Assembler-talen zijn echter nog machine- of processor-specifiek. De eerste
machine-onafhankelijke taal was Fortran, speciaal voor numerieke toepassingen
met veel formules (de naam is dan ook een afkorting van “Formula Translator”).
Dit was het begin van een Babylonische toren met bijbehorende spraakverwarring.
Het hangt er een beetje van af wat je allemaal meetelt, en of je “dialecten” apart
telt, maar een recente lijst van alle gepubliceerde programmeertalen bevat zo’n
3000 ingangen.
Veel talen lijken echter op elkaar: concepten uit de ene taal zie je overgenomen
worden in andere talen. In deze cursus bekijken we vooral de ontwikkeling van
een aantal concepten die in moderne programmeertalen voorkomen, en als vanzelf
krijgen we zo een overzicht van de belangrijkste historische ontwikkelingen.
Er is een duidelijke lijn van Fortran naar een recente taal zoals Java. Sommige van
de tussenliggende stappen (Algol en Simula) kunnen als “dode” taal worden
beschouwd, maar zijn historisch wel interessant. Sommige talen (Fortran, C)
worden nog steeds gebruikt, al of niet uit conservatisme.
Andere talen (Basic, Pascal) die sinds hun ontstaan zelf een ontwikkeling hebben
doorgemaakt in steeds nieuwe dialecten. Recente versies van Pascal lijken meer op
C++ dan op het oorspronkelijke Pascal. Waar we het in deze cursus over Basic en
Pascal hebben slaat dat vooral op de concepten die in de oorspronkelijke versies
aanwezig waren. De nieuwe concepten in recente dialecten zijn ontleend aan
andere talen, en komen zo vanzelf ter sprake.
De taal Algol was de eerste taal die expliciet bedoeld was voor algemeen gebruik.
De naam staat voor “Algorithmic Language”, een taal waarin algoritmes
(berekeningsvoorschriften) geformuleerd kon worden. Er zijn twee versies:
Algol60 en Algol68. De laatste is als een dinosaurus aan zijn eigen complexiteit
ten onder gegaan; de les die de wereld daarvan heeft geleerd is dat taalontwerp
vooral op simpelheid gericht moest worden.
De taal Basic (“Beginners All-purpose Symbolic Instruction Code”) was bedoeld
als eenvoudige taal voor beginners, en is vooral populair geworden door gebruik
op huiscomputers vanaf 1978. Pascal (geen afkorting, maar een eerbetoon aan
Blaise Pascal) was ook bedoeld als speciale onderwijstaal, waarin anders dan in
Basic gestructureerd programmeren voorop stond. Als je mensen dingen leert gaan
ze het gebruiken ook, en daardoor is Pascal de vervanger van Algol geworden als
taal voor algemeen gebruik.
Voor het schrijven van operating systems wass Pascal echter echt niet geschikt.
Voor de ontwikkeling van Unix werd daarom de taal C (opvolger van A en B)
bedacht. Het succes van Unix maakte dat C ook in het algemeen gebruikt ging
worden voor andere toepassingen waarvoor Pascal net tekort schoot.
De komst van window-systemen in de jaren 80 maakte een speciale stijl van
programmeren noodzakelijk, en daarmee nieuwe programmeertalen zoals C++
(“iets meer dan C”). Met de komst van Internet werden vooral veiligheid en
machine-onafhankelijkheid extra belangrijk, kenmerken die voorop stonden in
Java (geen afkorting, maar genoemd naar de coffeshop van de ontwerpers).
Naast de “main stream” van programmeertalen voor algemeen gebruik zijn er ook
programmeertalen voor een specifiek doel. Een belangrijk toepassingsgebied is
bijvoorbeeld het beheer van databases. Daartoe zijn aparte programmeertalen
ontwikkeld, te beginnen bij Cobol (“COmmon Business Oriented Language”) in
de zestiger jaren. SQL (“Structured Query Language”) is een recenter voorbeeld in
deze categorie.
Verder zijn er nog programmeertalen waarin geëxperimenteerd wordt met
bepaalde onorthodoxe concepten. Bouwend op een traditie die met Lisp begonnen
is, is ook hier sprake van een evolutie, compleet met invloedrijke maar inmiddels
dode talen (bijvoorbeeld Miranda), en nieuwe ontwikkelingen (bijvoorbeeld
Haskell) die echter vast nog niet het eindpunt zijn.
Behalve historisch of naar toepassingsgebied kun je programmeertalen ook
ordenen naar de concepten die er een rol in spelen. Groepjes programmeertalen
met gemeenschappelijke concepten staan bekend als een programmeer-paradigma.
Het woord “paradigma” is afkomstig uit de wetenschapsfilosofie, en staat voor het
kader van theorievorming van een bepaalde wetenschap in een bepaalde periode.
In het schema hierboven zijn een aantal paradigma’s weergegeven, met enkele
talen in elk paradigma (de donker-gekleurde talen zijn in onbruik geraakt, maar
wel van grote invloed geweest op de ontwikkeling).
Het meest voorkomende paradigma is het imperatieve. Je kunt spreken van
“imperative programmeertalen” en van “imperatief programmeren”. Je ziet dit
paradigma wel eens gecontrasteerd met het “procedurele” en het “objectgeoriënteerde” paradigma. Dat is eigenlijk minder juist, want in die laatste twee
zijn de imperatieve principes niet overboord gegooid, maar verfijnd. In het schema
is dit aangegeven; ook zie je dat het object-georiënteerde paradigma weer een
verfijning is van het procedurele.
Wel heel anders dan het imperatieve paradigma is het declaratieve. Daarvan zijn er
twee belangrijke deel-paradigma’s: het functionele en het logische.
In deze cursus bespreken we achtereenvolgens het imperatieve paradigma en de
twee verfijningen daarvan, en aan het eind ook kort de twee declaratieve
paradigma’s.
5
Het imperatieve paradigma sluit direct aan bij het eenvoudigste model van de
architectuur van de computer. Het is waarschijnlijk daarom, dat het het oudste en
meest gebruikte programmeer-paradigma is.
In dit model bestaat een computer uit een processor die opdrachten kan uitvoeren.
Doel daarvan is om de waarden die in het geheugen zijn opgeslagen te veranderen.
Natuurlijk bestaan concrete computersystemen uit meer componenten dan alleen
geheugen en processor: diskdrives, monitor, muis, enzovoorts. Maar als je de zaak
maar abstract genoeg bekijkt, is al die randapparatuur in zekere zin als geheugen te
beschouwen. Soms is dat ‘write only’ geheugen (monitor), soms is het ‘read only’
geheugen (muis); soms is het langzaam (disk), soms behoorlijk snel (RAM), soms
erg snel (registers op de processor-chip). Sommige randapparatuur ziet er voor de
processor daadwerkelijk uit als geheugen (memory mapped I/O), maar ook als dat
niet het geval is kunnen we, om het model niet te ingewikkeld te maken, doen
alsof.
Het woord “imperatief” betekent letterlijk “gebiedend” of “bevelend”. Het
refereert aan het feit dat in dit paradigma de uit te voeren opdrachten aan de
processor worden voorgeschreven.
6
Het geheugen bestaat in principe uit losse bits, de spreekwoordelijke “enen en
nullen”. In de meeste gevallen worden deze bits echter gemanipuleerd in groepjes
van bijvoorbeeld 8 bits (een byte) of een veelvoud daarvan.
In 8 bits kun je 28=256 verschillende waarden opslaan. Wat de interpretatie van die
waarden is, hangt af van de manier waarop die gebruikt worden. Je kunt de 256
waarden gebruiken om een getal tussen 0 en 255 aan te duiden, maar ook om
bijvoorbeeld een letterteken de coderen (met 256 waarden kun je alle hoofdletters,
kleine letters, cijfers en leestekens een vaste code geven, en dan houd je zelfs nog
over voor speciale symbolen en accenten). Als je behalve positieve getallen ook
negatieve getallen wilt kunnen opslaan dan is dat ook mogelijk. Bij 256
verschillende codes kun je dan bijvoorbeeld de getallen van 0 tot en met 127, en
van –128 tot en met –1 representeren. Voor het opslaan van een waarheidswaarde
(ja/nee) is in principe 1 bit genoeg, maar met 8 bits kan het natuurlijk ook. Enige
voorwaarde is dat er een afspraak is hoe de bitpatronen geïnterpreteerd moeten
worden.
Daarvoor zijn natuurlijk allerlei standaarden in gebruik: in het voorbeeld
hierboven is voor symbolen de ISO-codering gebruikt, en voor negatieve getallen
de zgn. 2’s-complement notatie.
Aan het pure bitpatroon (bijvoorbeeld 01000001) kun je niet zien welke
interpretatie bedoeld is: uit de context moet duidelijk zijn welk type waarden
opgeslagen is, en bovendien volgens welke coderingswijze dat is gebeurd.
Een groepje bits in het geheugen waarin een waarde kan worden opgeslagen heet
een variabele: de waarde kan immers in de loop van de tijd variëren. Je kunt de
opgeslagen waarde veranderen, en later nog eens bekijken welke waarde er is
opgeslagen. Je moet daarbij kunnen aangeven over welke variabele je het hebt;
daarom hebben variabelen een naam.
Een variabele heeft dus een (vaststaande) naam, en een (in de tijd variërende)
waarde. De naam van een variabele wordt over het algemeen niet in het geheugen
opgeslagen; de waarde natuurlijk wel. Men zegt wel dat de naam een stukje van
het geheugen adresseert (aanduidt).
In de wiskunde worden ook van oudsher variabelen gebruikt. Er is een subtiel
verschil met de variabelen uit de informatica. Wiskunde-variabelen duiden immers
een vaste, maar mogelijk nog onbekende waarde aan (“de oplossing van 2x=10”),
of bestrijken juist een heel gebied (“voor alle x tussen 0 en 10 geldt…”).
Informatica-variabelen daarentegen hebben altijd een bepaalde waarde, die echter
wel in de tijd kan veranderen.
Het uitvoeren van een opdracht moet blijvende sporen achterlaten – anders had je
het uitvoeren de opdracht net zo goed achterwege kunnen laten. De enige manier
om sporen achter te laten is om het geheugen te veranderen (of door iets op het
beeldscherm of printer te zetten, maar printer en beeldscherm waren in wezen een
bijzonder soort geheugen, dus dat valt ook onder de noemer “geheugen
veranderen”). Opdrachten zijn dus voorschriften om het geheugen te veranderen.
Een programma is in het imperatieve paradigma in feite slechts een lange reeks
opdrachten. De opdrachten worden één voor één uitgevoerd door de processor. Die
is daartoe voorzien van een program counter (ook wel instruction pointer
genoemd, omdat het ding niet zozeer programma’s telt, als wel de huidige
instructie aanwijst).
Het meest gebruikelijke Engelse woord voor “opdracht” is statement. Dat is
taalkundig eigenlijk niet juist, want de letterlijke betekenis van statement is niet
zozeer “opdracht” als wel “uitspraak, verklaring”. Het woord instruction wordt
alleen gebruikt op machinetaal-nivo. Het woord command is nooit echt
doorgebroken. Gelukkig maar, want behalve dat het erg militair klinkt ligt ook
begripsverwarring met comment (=toelichting) op de loer.
7
Een imperatief programma is een reeks opdrachten. Een imperatieve
programmeertaal is dus een notatiewijze om deze opdrachten te formuleren.
Omdat de opdrachten bedoeld zijn om het geheugen te veranderen, moet er in
ieder geval een notatie zijn voor de opdracht die één variabele in het geheugen
verandert.
In die opdracht zal op een of andere manier de te veranderen variabele moeten
worden aangeduid, en bovendien de nieuwe waarde die de variabele door het
uitvoeren van de opdracht moet gaan krijgen. Er zullen in de programmeertaal dus
notaties zijn om variabelen en waarden aan te duiden.
8
Een hele simpele manier om de variabelen in het geheugen aan te duiden is om ze
gewoon te nummeren: byte nummer 1, byte nummer 2, en zo verder tot byte
nummer 32 miljoen (of hoeveel geheugen er ook maar aanwezig is). In de vroege
assembler-talen was dit inderdaad de manier om variabelen aan te duiden.
Uitzondering vormden een klein aantal in de processor-chip ingebouwde
variabelen (registers); die hadden eerst namen als A en B, later A t/m E, en toen het
er steeds meer werden namen als A0 t/m A16.
Echt handig zijn dat soort genummerde variabelen niet. Vergissing tussen
variabele 15478 en variabele 15487 ligt immers voor de hand, en bovendien is de
betekenis van dit soort “namen” niet duidelijk.
In de wiskunde was het al veel langer gebruikelijk om variabelen met een letter
aan te duiden. Vaste conventies dienden als geheugensteuntje: x is (bijna) altijd een
onbekende, n is een aantal, α is een hoek, en ε een heel klein getal. Omdat in
programma’s vaak erg veel variabelen nodig zijn, is in computertalen het gebruik
ontstaan om meer-letterige variabelen te gebruiken, waarin vaak ook cijfers en
(afhankelijk van de taal) symbolen als dollar, onderstreping en accent gebruikt
mogen worden.
Van lieverlee onstond de gewoonte om variabelen een toepasselijke naam te
geven, dus bijvoorbeeld niet x9a of tb, maar totaalbedrag. Je ziet dan ook het
aantal letters dat in een variabele gebruikt mag worden steeds toenemen.
Het aanduiden van waarden door middel van bitpatronen (zoals 01000001) zou
erg omslachtig zijn. Daarom bieden alle programmeertalen notaties om waarden in
de meest voorkomende interpretaties te noteren. Duidelijkste voorbeeld hiervan
zijn getallen, die in de gewone decimale notatie in plaats van de binaire mogen
worden genoteerd, al dan niet voorzien van min-teken. Getallen met “cijfers achter
de komma” worden naar angelsaksisch gebruik meestal met een “decimale punt”
genoteerd, en voor hele grote getalen kan vaak de E-notatie worden gebruikt:
3.0E8 betekent 3.0 maal 108, oftewel 300000000.
Enige uitzondering vormen assembler-talen: omdat getallen hier al gebruikt
worden om variabelen aan te duiden, moeten “echte” getallen op een speciale
manier worden genoteerd, vaak met het teken “#”.
Lettertekens en andere symbolen kunnen in veel talen direct gebruikt worden,
zodat het onnodig is om coderings-tabellen uit het hoofd te kennen. Om
verwarring met variabelen te voorkomen moeten symbolen altijd tussen
aanhalingstekens worden gezet – ze moeten immers letterlijk worden genomen.
De taal Pascal gebruikt enkele aanhalingstekens voor zowel losse letters als meerletterige teksten; in C en Java kan onderscheid worden gemaakt tussen de losse
letter ‘a’ en de 1-letterige tekst “a”.
Sommige talen kennen een aanduiding voor waarheidswaarden (False en True);
die namen zijn dan uigesloten voor gebruik als variabele. Andere talen gebruiken 0
en 1 voor waarheidswaarden, op het gevaar af van verwarring met getallen.
In elke imperatieve taal is er een notatie beschikbaar om een variabele een nieuwe
waarde te geven. Dit wordt een “toekenningsopdracht” (assignment statement)
genoemd, omdat er een nieuwe waarde aan de variabele wordt toegekend.
Onderdeel van de toekenningsopdracht is in ieder geval een aanduiding van de
variabele die moet veranderen, alsmede diens nieuwe waarde. In sommige talen
noem je eerst de waarde en dan de variabele, in andere talen is dat andersom.
De manier waarop de twee onderdelen aan elkaar worden gelijmd verschilt per
taal. In Cobol had men (tevergeefs) het ideaal om een programma op omgangstaal
te doen lijken; recentere talen worden steeds symbolischer.
In Pascal werd om didactische redenen het symbool “:=” ingevoerd, zodat het
onderscheid tussen de opdracht “aap:=12” (maak aap, ongeacht de huidige waarde,
gelijk aan 12!) en de test “aap=12” (is aap op dit moment gelijk aan 12?) goed
duidelijk is. Het symbool “:=” dient daarbij te worden uitgesproken als “wordt”;
immers, de variabele is nog niet noodzakelijk gelijk aan de waarde, maar wordt dat
na uitvoeren van de opdracht.
In C en Java is het symbool voor toekenning “=”, dat voor de duidelijkheid toch
het beste uitgesproken kan worden als “wordt”. In deze talen wordt de gelijkheidstest genoteerd met een dubbel “==” teken, wat voor de Pascal-kenners dan weer
verwarrend is.
Het blijft natuurlijk maar een notatiekwestie, en als je daar doorheen kijkt is de
toekenningsopdracht in alle imperatieve talen in wezen hetzelfde.
9
Als er alleen maar constante waarden in een toekenningsopdracht gebruikt zouden
mogen worden, dan zou een computer zijn naam geen eer aandoen. Een computer
kan behalve waarden transporteren natuurlijk ook rekenen.
In assemblertalen zijn er instructies beschikbaar waarmee alle rekenkundige
berekeningen (optellen, vermenigvuldigen) op een variabele en een waarde
kunnen worden uitgevoerd. Cobol volgt hetzelfde principe.
Het formuleren van ingewikkelde berekeningen door middel van losse deelopdrachten, is een slechte gewoonte die wiskundigen al in de renaissance hebben
afgeleerd (en de Arabieren onder hen al in de achtste eeuw). De belangrijkste
vernieuwing in Fortran was dan ook dat aan de rechter- (waarde-)kant van een
toekennings-opdracht behalve een waarde ook een formule mag staan. De
betekenis van de naam Fortran is dan ook “formula translator”.
Bij het uitvoeren van een opdracht waar zo’n expressie in voorkomt wordt eerst de
waarde van die expressie berekend, gebruik makend van de huidige waarde van de
variabelen die in de expressie voorkomen, waarna deze waarde wordt toegekend
aan de variabele links in de toekenningsopdracht.
In expressies mogen de bekende wiskundige operatoren (+, -, * en /) worden
gebruikt, alsmede een aantal standaardfuncties (zoals sqrt). Het enige verschil met
de wiskunde is dat vermenigvuldiging expliciet moet worden geschreven (met een
sterretje); dat is de prijs van het toestaan van meer-letterige variabele-namen.
10
Niet alle soorten berekeningen zijn zinvol op alle typen waarden. Je kunt wel
getallen vermenigvuldigen, maar geen waarheidswaarden; je kunt een tekst wel
naar hoofdletters vertalen, maar een getal niet. De uitkomst van een worteltrekking
is over het algemeen een gebroken getal en niet een geheel getal.
Als je dit soort dingen per ongeluk toch probeert, zullen de in het geheugen
opgeslagen bit-patronen op de verkeerde manier worden geïnterpreteerd. Veel
talen proberen dit soort vergissingen tegen te gaan. Alleen in assemblertalen mag
alles, en daarin is het dan ook moeilijk om foutloze programma’s te schrijven.
In vrijwel alle andere talen wordt een vorm van type-controle uitgevoerd, die
nagaat of operaties wel zinvol zijn. Daartoe is het nodig dat het bekend is wat het
bedoelde type van variabelen is.
In Fortran was de typering eenvoudig: namen van variabelen die met een i t/m n
beginnen zijn gehele getallen, de rest is floating-point (uit die tijd stamt de
hardnekkige gewoonte om tellertjes i en j te noemen). In Basic was de naamgeving
vrij, maar van gehele getallen moest de naam op een %-teken eindigen. Tekstvariabelen eindigden op een $-teken, de rest was floating-point (men herkent
mensen die met Basic zijn opgevoed aan het uitspreken van ‘$’ als ‘string’!).
In recentere talen is er een trend naar steeds meer verschillende types. Dit soort
naamgeving-schema’s zijn daardoor niet meer mogelijk. In plaats daarvan is de
naamgeving vrij, maar moeten alle variabelen expliciet gedeclareerd worden. Dit
heeft als extra voordeel dat tikfouten in variabele-namen aan het licht komen.
In de modernere programmeertalen zul je in een programma behalve opdrachten
dus ook declaraties aantreffen. In Pascal moet dat gebeuren voorafgaand aan de
opdrachten, in C en Java kunnen de declaraties en de opdrachten elkaar
afwisselen. Dat laatste is uit didactisch oogpunt eigenlijk jammer, want het
verschil tussen een declaratie en een opdracht wordt er minder duidelijk van.
Het verschil tussen een declaratie en een opdracht is echter wel belangrijk. De
opdrachten worden tijdens het uitvoeren van het programma uitgevoerd. Eventuele
declaraties die ertussen staan kosten daarbij geen tijd. De declaraties waren in een
eerdere fase van belang, namelijk tijdens het compileren van het programma: ze
zijn een aanwijzing voor de compiler over hoe deze de variabelen die in
opdrachten voorkomen moet interpreteren, en dus hoe de opdrachten naar
machinetaal vertaald moeten worden.
In vrijwel alle programma’s komen deel-taken voor die meer dan eens moeten
worden uitgevoerd. Vanaf de allervroegste programmeertalen zijn er daarom
mechanismes om een deel van het programma meer dan eens te gebruiken.
In de diverse programmeertalen verschilt de terminologie voor zo’n deelprogramma, en er zijn ook wel verschillen, maar gemeenschappelijk is dat het
deel-programma meermalen kan worden gebruikt, door middel van een speciale
opdracht. Dat staat in alle talen bekend als het aanroepen van de subroutine/
procedure/functie/methode (Engels: call, of hoogdravender: invoke).
11
In Assembler-talen en in Basic zijn de opdrachten in principe genummerd. Er is
een speciale opdracht (GOSUB) waarmee de subroutine met een bepaald
regelnummer kan worden uitgevoerd. Uitvoeren van de speciale opdracht
RETURN heeft tot gevolg dat het programmaverloop wordt voortgezet bij de
opdracht volgend op de GOSUB-opdracht.
Merk op dat aan de structuur van programma niet te zien is dat er op regel 100 een
subroutine begint; dit is alleen impliciet te zien aan het feit dat er “GOSUB 100”
opdrachten in het programma staan. Het is ook denkbaar dat je halverwege in een
subroutine springt, of dat subroutines in elkaar overvloeien.
In Pascal hebben (in navolging van het inmiddels in onbruik geraakte Algol) de
procedures een naam, en is het in de programmatekst, los van de aanroepen, direct
duidelijk dat het om een procedure gaat. Na de procedures volgt het eigenlijke
programma (het zogenaamde hoofdprogramma), waarin de procedures kunnen
worden aangeroepen. Het is nu niet meer mogelijk om een procedure “half” aan te
roepen, of om de RETURN-opdracht te vergeten.
In C worden de procedures functies genoemd, maar de structuur is hetzelfde als in
Pascal. Nieuw is dat men heeft ingezien dat het hoofdprogramma in weinig
verschilt van de procedures (het is een groepje opdrachten dat bij elkaar hoort). In
C wordt daarom het hoofdprogramma ook als functie geschreven, die de naam
main moet hebben. Starten van het programma kan worden beschouwd als aanroep
van de main-functie. In Java is deze traditie ook aangehouden.
12
Een procedure bestaat uit een aantal opdrachten die bij elkaar horen. Anders dan
bij subroutines is aan de programmatekst, los van eventuele aanroepen, te zien dat
een groepje opdrachten één procedure vormt. De opdrachten zijn daartoe
gebundeld met de woorden begin en end.
Deze woorden zijn zelf echter geen opdracht – ze kunnen immers niet worden
uitgevoerd. Het zijn veleer een soort leestekens: haakjes-openen en -sluiten,
waarmee de opdrachten die samen de procedure vormen worden afgebakend. In C
en Java is dat nog duidelijker, omdat hier daadwerkelijk haakjes (of liever gezegd
accolades) worden gebruikt om de opdrachten af te bakenen.
We zijn hier getuige van een zeer belangrijke stap in de evolutie van
programmeertalen. In Assembler, Fortran en Basic werd de programmastructuur
gedicteerd door (genummerde) tekstregels; in Algol, Pascal, C en Java wordt de
programmastructuur expliciet bepaald door haakjes of haakjes-achtige keywords.
De regelovergangen verliezen daarmee hun betekenis, waardoor de door
esthetische overwegingen ingegeven programma-layout zijn intrede doet.
Met de aanwezigheid van duidelijk afgebakende procedures is het mogelijk om
declaraties van variabelen te plaatsen in zo’n procedure. In Pascal gebeurt dat
tussen de prcedure-kop en het woord begin, in C gebeurt het net na het {-haakje,
maar dat is slechts een notationeel verschil. Hoofdzaak is dat de gedeclareerde
variabele alleen geldig is binnen die ene procedure.
Zo’n variabele die alleen plaatselijk geldig is heet een lokale variabele, en de
declaratie ervan is een lokale declaratie.. Variabelen die buiten een procedure zijn
gedeclareerd, en dus in alle procedures mogen worden gebruikt, heten in contrast
daarmee globale variabelen.
Het kan gebeuren dat in verschillende procedures een variabele wordt
gedeclareerd met dezelfde naam. Die hebben niets met elkaar te maken; de
procedures kunnen immers niet van elkaar “weten” welke lokale variabelen er in
gebruik zijn. Het zijn dus wel degelijk verschillende variabelen, en het feit dat ze
dezelfde naam hebben is daarbij niet storend, omdat zo’n naam toch alleen maar
lokaal binnen de respectievelijke procedures geldig is.
Bij elke aanroep van de procedure worden de lokale variabelen opnieuw gemaakt.
Het is dus niet zo dat lokale variabelen de procedure overleven, en bij een
volgende aanroep nog onveranderd beschikbaar zijn.
Globale variabelen mogen in alle procedures (en in het hoofdprogramma) worden
gebruikt; lokale variabelen alleen maar in de procedure waar ze zijn gedeclareerd.
Je kunt je afvragen wat er het voordeel van is om een variabele lokaal te
declareren; dat legt immers alleen maar beperkingen op aan het gebruik ervan.
Een belangrijk voordeel is, dat lokale variabelen niet per ongeluk gewijzigd
kunnen worden bij aanroep van een andere procedure. Na aanroep van een
procedure hebben je eigen lokale variabelen gegarandeerd nog dezelfde waarde;
voor globale variabelen hoeft dat niet zo te zijn, die kunnen immers door de
aangeroepen procedure veranderd zijn.
Je zou dit effect ook kunnen bereiken door in alle procedures variabelen met
verschillende namen te gebruiken. Dan moet je wel goed weten welke andere
procedures er in hetzelfde programma gebruikt worden, en als het programma
door een team wordt geschreven, kan dat heel wat vergadertijd opsouperen. Het is
veel gemakkelijker als elke programmeur zijn eigen lokale variabelen kan kiezen,
zonder daarover met collega’s te hoeven vergaderen. Ook kun je op deze manier
een procedure uit een ander programma “lenen”, zonder je druk te maken over de
naamgeving van de variabelen binnen de procedure.
Bij programma’s die door één programmeur worden geschreven gelden deze
voordelen onverkort: deze programmeur kan zich op één procedure tegelijk
concentreren, zonder zich druk te maken over de details van de, mogelijk vele,
andere procedures.
13
Vaak gebeurt het dat een deel-taak weliswaar meermalen moet worden uitgevoerd,
maar dat daarbij enkele details toch verschillen. Om in zo’n geval toch met één
procedure (die meermalen wordt aangeroepen) te kunnen volstaan kunnen
procedures parameters krijgen. Een parameter is in feite een lokale variabele, die
bij de aanroep van de procedure alvast een waarde krijgt.
De declaratie van een parameter gebeurt in de meeste talen tussen haakjes in de
procedure-kop: int x in het voorbeeld hierboven. Dit in tegenstelling tot declaraties
van lokale variabelen, die pas volgen na de procedure-kop: int a in het voorbeeld.
De waarde die de parameter bij de aanroep moet krijgen wordt, tussen haakjes, bij
de aanroep opgeschreven. Let op: bij de definitie van de procedure staat er dus een
declaratie tussen de haakjes, terwijl er bij de aanroep een expressie (een formule
met een waarde) staat. Die expressie kan een constante zijn, zoals 3 of 4, maar ook
een berekening, zoals y+1. In feite mag er alles staan wat ook aan de rechterkant
van een toekenningsopdracht mag staan. In het bijzonder is een losse variabelenaam ook toegestaan (zoals y), en vooral daar kan er allicht verwarring ontstaan
met de procedure-definitie (zoals int x in het voorbeeld).
14
Op het thema “parameters” zijn nog diverse variaties mogelijk. Zo is het in C++
mogelijk om in de definitie van een procedure de parameter van een default
waarde te voorzien. Als de procedure met “te weinig” parameters wordt
aangeroepen, dan geldt de default waarde voor de betreffende parameter. Hierbij
geldt de beperking dat de parameters met zo’n default-waarde aan het eind van de
lijst moeten staan, omdat er anders ambigue situaties kunnen onstaan. Deze
vernieuwing van C++ t.o.v. C is in Java overigens weer afgeschaft.
Een nadeel van de parameter-overdracht zoals die in Pascal, C, Java en vrijwel alle
andere talen verloopt, is de essentiële rol die de volgorde van de parameters speelt.
Bij de aanroep is niet meer te zien welk van de parameters welke rol speelt, en dat
kan bij procedures met tien parameters verwarrend en dus foutgevoelig zijn.
In enkele talen moet daarom bij aanroep van een procedure de naam van de
parameters worden genoemd, waarmee de verplichting vervalt om ze in volgorde
op te sommen. Een voorbeeld hiervan is de taal HTML. Met een beetje goede wil
kan het gebruik van tags zoals <IMG> in deze taal als procedure-aanroep worden
beschouwd; parameters worden daarbij met hun naam aangeduid.
Het voordeel van lokale declaraties was dat programmeurs van verschillende
procedures onafhankelijk van elkaar konden werken, en niet hoefden te vergaderen
over hun lokale variabelen. Het enige waar het team het over eens hoefde te
worden, waren de namen van de procedures.
Bij echt grote programma’s onstaat voor de namen van procedures hetzelfde
probleem als wat eerder gold voor het gebruik van variabele-namen: het is
onduidelijk welke namen nog gebruikt mogen worden en welke niet. Ook bij het
gebruik van procedure-bibliotheken treedt dit probleem op.
De oplossing van dit probleem ligt in weer een nieuwe laag in de hiërarchie:
procedures kunnen worden gebundeld in een zogenaamde klasse. Binnen de klasse
kunnen de procedures elkaar gewoon aanroepen; buiten de klasse moet daarbij de
naam van de klasse genoemd worden. Voor globale variabelen (die dus niet meer
echt “globaal” zijn) geldt hetzelfde.
In talen als C++ en Java kunnen, waar dit mogelijk is, programmeurs van
verschillende klassen dus hun gang gaan, zonder zich druk te maken over de
naamgeving van procedures in de andere klassen. In Java kunnen zelfs de klassen
weer worden ondergebracht in packages, mocht de naamgeving van de klassen op
zijn beurt weer verwarrend worden. Keerzijde van dit moois is dat je in Java ook
voor simpele programma’s de klasse-stuctuur moet aanleggen, zelfs als er maar
één klasse is. In kleine voorbeelden kan dat omslachtig overkomen t.o.v. bijv.
Basic, maar al bij iets grotere programma’s betaalt de moeite zich terug.
Om het programmaverloop zich te laten ontwikkelen afhankelijk van de
voorgeschiedenis of bijvoorbeeld door de gebruiker ingevoerde waarden, is het
nodig om bepaalde opdrachten voorwaardelijk uit te voeren of juist over te slaan.
In Assembler-talen is er daarom een instructie aanwezig om op grond van de
waarde van de laatst gedane berekening het programmaverloop elders voort te
zetten (in feite is dat een voorwaardelijke toekenning van een nieuwe waarde aan
een speciale variabele: de program counter). In het voorbeeld wordt de sprong
gemaakt als de uitkomst van de CMP (compare) opdracht ongelijk nul is, maar er
zijn ook sprongen mogelijk op grond van een handjevol andere criteria.
Belangrijke vernieuwing van talen als Fortran en Basic was de mogelijkheid om
een willekeurige opdracht (bijvoorbeeld een toekenning of een subroutineaanroep) voorwaardelijk uit te voeren.
15
Voor iets ingewikkeldere constructies, waarbij meerdere opdrachten
voorwaardelijk moeten worden uitgevoerd, of waarbij, in het geval dat de
voorwaarde niet waar is, juist een andere opdracht moet worden uitgevoerd, schiet
de simpele IF-opdracht van het oorspronkelijke Fortran en Basic tekort. Het is wel
mogelijk, maar het programmaverloop moet dan expliciet worden beïnvloed met
GOTO-opdrachten, waardoor het geheel wel erg op Assembler gaat lijken.
In talen als Pascal en C is het, ten behoeve van procedures-definities, toch al
mogelijk om opdrachten te groeperen met begin/end of accolades. Deze
groepering van opdrachten komt nu ook goed van pas om meerdere opdrachten tot
body van een if-opdracht te maken. Doordat de koppeling met tekstregels is
losgelaten, is het bovendien mogelijk om op een overzichtelijke manier ook nog
plaats te bieden aan een else-gedeelte. De opdrachten achter if en else kunnen op
hun beurt weer if-opdrachten bevatten. Door handig gebruik te maken van
inspringen kan in een netwerk van keuzemogelijkheden toch redelijk
overzichtelijk het programmaverloop worden vastgelegd.
16
Vaak komt het voor dat een reeks opdrachten meerdere malen achter elkaar moet
worden uitgevoerd. In eenvoudige talen kan dit worden bereikt met behulp van
twee GOTO-opdrachten. In talen met een begin/end-blokstructuur kan dit veel
duidelijker worden aangegeven met een while-opdracht. De body van deze
opdracht bevat de opdracht die moet worden herhaald; is dit er meer dan één, dan
kunnen de opdrachten worden gebundeld met begin/end (respectievelijk
accolades).
Voordeel hiervan is dat zo’n while-opdracht dichter ligt bij de belevingswereld van
de programmeur: je ziet gemakkelijk welke opdracht(en) worden herhaald, en
hoelang dat het geval is, zonder dat daarvoor de GOTO-opdrachten hoeven te
worden uitgeplozen.
Een veel voorkomende vorm van herhaling is die, waarbij het aantal malen dat een
opdracht wordt herhaald wordt bepaald door een variabele, waarin het aantal
herhalingen wordt geteld. In Basic een tweetal opdrachten beschikbaar: FOR en
NEXT. Hierin is een soort blokstructuur avant-la-lettre te herkennen, maar het
ging wel degelijk om twee aparte opdrachten. Kwaadwillende programmeurs
zouden de NEXT-opdracht voorwaardelijk kunnen maken, of in een subroutine
plaatsen…
Het belang van een constructie met een tellende variabele werd echter in zowel
Pascal als C wel onderkend. In beide talen is er daarom een for-opdracht. Een
aparte opdracht om het einde aan te geven is natuurlijk niet nodig, omdat ook hier
de blokstructuur gebruikt kan worden om de reikwijdte van de body aan te duiden.
Was het in Pascal nog alleen maar mogelijk om met een for-opdracht te tellen
(eventueel met een stapgrootte anders dan 1, of achteruit), in C is de for-opdracht
ook voor andere vormen van herhaling te gebruiken: hier kan de initialisatie, het
voortgangscriterium, en de stap-opdracht vrij worden gekozen, zelfs als er
“geteld” wordt met andere variabelen dan integers.
Het gebruik van blokstructuur in programmeertalen is nu gemeengoed. Toch is dat
niet altijd zo geweest. In 1968 was het een knuppel in het hoenderhok toen de
(Nederlandse) informaticus E.W.Dijkstra een artikel publiceerde met de toentertijd
provocerende titel Go To Statement Considered Harmful (strikt genomen was het
een ingezonden brief, van slechts één pagina, aan het vaktijdschrift
Communications of the ACM).
Dit artikel wordt vaak genoemd als zou Dijkstra hierin de “spaghetti”-structuur
van programma’s met GOTO-opdrachten veroordelen. Hij noemt echter alleen het
woord “mess”, en zijn hoofdargument is bovendien dat de toestand van het
programmaverloop met GOTO-opdrachten lastig te beschrijven is, en dat het
daarom lastig is om uitspraken te doen over de correctheid ervan. De gevaren
kunnen worden beteugeld door gebruik te maken van if- en while-constructies.
Het artikel was hoe dan ook het begin van een richting in het programmeeronderwijs die bekend staat als “gestructureerd programmeren”, zeg maar met
gebruikmaking van Pascal (1971) in plaats van Basic. Taalontwerpers waren
huiverig om de GOTO-opdracht helemaal te schrappen: in zowel Pascal als C is
deze opdracht toch nog mogelijk. Pas in Java is hij echt geschrapt.
(Het is nog een tijdje mode geweest om artikelen te publiceren met de woorden
“considered harmful” in de titel (global variables…, assignment statement…),
totdat het tijdschrift-redacties te gortig werd, en dit soort epigoon-artikelen bij
voorbaat werden geweigerd.)
17
Het strikt toepassen van de nieuwe leer van het gestructureerd programmeren bleef
niet zonder problemen. Zo was het voortijdig afbreken van een herhaling in Basic
bepaald gemakkelijker dan in Pascal, waar dit alleen mogelijk was met een whileopdracht in plaats van een for-opdracht, en een extra boolean hulpvariabele.
In C werd er daarom een nieuw soort opdracht ingevoerd: de break-opdracht. De
betekenis hiervan is dat de verwerking van de omvattende while-, for- of switchopdracht wordt onderbroken. Het is, zo men wil, een vorm van “gestructureerde
goto”, en wordt in de praktijk veel gebruikt.
Aanhangers van de strikte gestructureerd-programmeren-leer vinden de breakopdracht toch verdacht, bijvoorbeeld omdat de betekenis ervan niet zonder de
context kan worden beschreven. Een ander theoretisch nadeel is dat aan de header
van een for-opdracht niet meer eenduidig de stop-voorwaarde is af te lezen. Er zijn
dan ook C- en Java-leerboeken waarin het voortijdig stoppen van een for-opdracht
op de Pascal-manier wordt besproken. Het blijft natuurlijk de vraag of dat het voor
beginners veel duidelijker maakt.
Naast procedures is het in Pascal ook mogelijk om zogenaamde functies te maken.
Een functie heeft dezelfde eigenschappen als een procedure (je groepeert eenmalig
een aantal opdrachten onder een header, en kunt die vervolgens meermalen
aanroepen), maar daarenboven wordt door een functie een waarde berekend, die
door de aanroeper kan worden gebruikt. Zo’n functie-resultaat kan gebruikt
worden om informatie terug te geven aan de aanroeper; dit in tegenstelling tot
parameters, die juist bedoeld zijn om informatie van de aanroeper naar de
procedure of functie te transporteren.
De aanroep van een procedure heeft zelf de status van een opdracht. Je kunt zo’n
aanroep immers uitvoeren (met als gevolg dat de opdrachten in de body van de
procedure worden uitgevoerd).
De aanroep van een functie heeft daarentegen de status van een expressie: hij kan
worden gebruikt aan de rechterkant van een toekenning, in een grotere formule, of
zelfs als parameter van een andere aanroep. Expressies zijn immers programmaconstructies met een waarde, en een functie-aanroep levert een waarde op.
18
Omdat een procedure, anders dan een functie, geen resultaat-waarde oplevert,
moeten alle nuttige effecten van een procedure afkomstig zijn van manipulatie van
de buitenwereld. Elke procedure zal dus ófwel een toekenning doen aan globale
variabelen, ófwel output genereren (wat in wezen hetzelfde is). Het enige
alternatief is dat de procedure andere procedures aanroept die deze effecten
veroorzaken.
Een functie levert een resultaatwaarde op. In de header van de functie moet het
type van de resultaatwaarde worden gespecificeerd (in Pascal aan het eind van de
header, in C en Java juist aan het begin). Bovendien moet ergens in de functiebody worden aangegeven wat nu eigenlijk het resultaat is van de functie. In Pascal
gebeurt dit door toekenning aan de functienaam, in C en Java met de speciale
return-opdracht.
In Pascal zijn procedures en functies verschillende constructies, aangeduid met
verschillende keywords. Bij een functie moet een resultaat-type worden
opgegeven en bij een procedure juist niet. Toch is er grote gelijkenis in de
structuur van procedure- en functie-definities: beide hebben een naam, parameters,
en een body. In C en Java is deze gelijkenis nog duidelijker: de opbouw van
procedures is hetzelfde als die van functies. Als resultaat-type van procedures
wordt het speciale type void gebruikt. Letterlijk betekent dit “leeg” of “niets”, en
dat klopt: een procedure (of in C-terminologie: “void-functie”) heeft geen
resultaatwaarde. Er hoeft dan ook geen return-opdracht te staan in de body.
Als parameter van een functie moeten waardes worden meegegeven. Die kunnen
het resultaat zijn van het uitrekenen van een expressie, en een van de
mogelijkheden daarvoor is het aanroepen van een andere functie.
In de body van een functie staan opdrachten. Die opdrachten kunnen aanroepen
zijn van andere (void-)functies. De opdrachten in de body daarvan kunnen ook
weer aanroepen zijn van weer andere functies, enzovoorts. Na afloop van het
uitvoeren van een functie keert het programmaverloop altijd terug naar de plaats
van de aanroep. Als functies elkaar aanroepen moet er een hele stapel van posities
in het programma onthouden worden (“als dit is afgelopen moeten we verder op
positie A, en als dát is afgelopen verder op positie B”). Al die terugkeerposities
worden door de processor dan ook bijgehouden in een speciaal stuk van het
geheugen, dat de naam stack draagt.
In het voorbeeld hierboven bevat de stack, op het moment dat de body van
tekenpunt wordt uitgevoerd, de programmapostie in de functie tekenlijn van
waaruit tekenpunt werd aangeroepen, de positie in tekenvierkant van waaruit
tekenlijn werd aangeroepen, en de positie in tekenkubus van waaruit tekenvierkant
werd aangeroepen. In het andere voorbeeld is een minder grote stack nodig. De
stack bevat de positie van waaruit oplos werd aangeroepen. Dan wordt de functie
kwad aangeroepen, en gaat de stack dus de programmapositie van deze aanroep
bevatten. Na afloop van de aanroep van kwad wordt deze positie weer van de stack
verwijderd, en pas daarna wordt de functie sqrt aangeroepen.
19
Een aardig idee is dat je in de body van functies, naast andere functies, ook de
functie zelf weer kunt aanroepen. Op het eerste gezicht lijkt er dan een Drosteeffect te onstaan (want die aangeroepen functie zal ook weer zichzelf aanroepen,
enzovoorts). Voor de administratie daarvan zou een oneindig grote stack nodig
zijn. Toch kan zo’n zogenaamde recursieve aanroep heel handig zijn, mits je er
met een if-opdracht voor zorgt dat de functie niet altijd zichzelf aanroept. Via een
parameter, die bijvoorbeeld bij elke aanroep 1 kleiner wordt, kun je er voor zorgen
dat de niet-recursieve tak van de if-opdracht zich vroeg of laat zal voordoen. Vanaf
dat moment wordt de hele stack, waarop alle programmaposities in de diverse
“incarnaties” van de functie staan, weer helemaal afgebroken.
Met een recursieve functie kun je een herhalings-effect bereiken (de functie wordt
net zolang opnieuw aangeroepen, totdat het niet-recursieve geval zich voordoet),
zonder daarbij een while- of een for-opdracht te gebruiken; een if-opdracht is
voldoende.
Het debat over de wenselijkheid van recursieve aanpak van problemen slingert
heen en weer tussen enerzijds de elegantie die zo’n recursieve formulering ten
toon kan spreiden, en anderzijds de prijs van de extra benodigde stackruimte die
dat met zich meebrengt.
20
Als je alle variabelen in een programma apart zou moeten declareren, zou het lang
duren voordat je 32 megabyte vol hebt. Gelukkig is het mogelijk om met één
declaratie een hele rij variabelen te declareren (allemaal van hetzelfde type). De
notatie daarvoor varieert natuurlijk weer per taal, maar het idee is hetzelfde: er
wordt in één keer een stuk geheugen van een bepaalde afmeting gereserveerd.
Anders dan losse variabelen moeten deze zogenaamde arrays in Basic wèl
gedeclareerd worden. Dat gebeurt met het zogeheten DIM-statement, wat in feite
helemaal geen opdracht is maar een declaratie.
In Pascal moet de ondergrens en de bovengrens van de array worden opgegeven
(het is dus mogelijk om een array met indexnummers van 10 tot en met 20 te
maken!). In C en Java moet het aantal elementen worden opgegeven, en begint de
telling altijd bij 0. Bij een aantal elementen van 10, is het nummer van het laatste
element dus 9, iets wat nog wel eens voor verwarring zorgt. Deze op het eerste
gezicht merkwaardige keuze is in C bewust gemaakt; het alternatief zou zijn dat
een array die is gedeclareerd met int a[10] elf elementen bevat, en dat zou ook
weer raar zijn.
In vrijwel alle getypeerde talen zijn er een aantal standaardtypes aanwezig.
Daaronder bevinden zich meestal de gehele getallen (int of integer) en floatingpoint getallen (float of real). Dat dit twee verschillende types zijn vindt zijn reden
in het feit dat met integers sneller en zonder gevaar van afrondfouten gerekend kan
worden.
Fortran was de taal van technisch/natuurkundige berekeningen, en als extra type
onstond hierin de behoefte aan complexe getallen. In een beginnerstaal als Basic
was er veeleer behoefte aan tekst-variabelen, die de naam string kregen.
Pascal zocht het in een andere richting: vanwege de ordelijkheid werden char en
boolean hier als aparte types geïntroduceerd. Een apart type string was niet nodig,
want daarvoor kon een array van char gebruikt worden. C was meer een taal van
de praktijk dan van het onderwijs. Hierin kwamen aparte types voor “extra grote”
integers en floats: respectievelijk long en double. De boolean werd vergeten, maar
wel was er ook het type void, te gebruiken als “resultaat-type” van procedures.
Java heeft alle succesvolle types uit andere talen overgenomen en er nog wat
toegevoegd. Ook de string uit Basic keerde terug, zij het strikt genomen niet als
standaardtype.
Naast het gebruiken van standaardtypes is het in alle talen vanaf de
“gestructureerd programmeren” revolutie mogelijk om het repertoire uit te
breiden, door middel van type-definities. Zo kun je een ingewikkeld array-type
bijv. de naam tabel geven, en vervolgens variabelen van dat type declareren.
In een array hebben alle onderdelen hetzelfde type. Soms is het echter zinvol om
een aantal variabelen van verschillend type te groeperen. Klassiek voorbeeld
daarvan is een record uit een database.
In Pascal is het mogelijk om het type van zo’n record eenmalig de definiëren, en
dat type vervolgens te gebruiken in variabele-declaraties. Ook kun je arrays van
dit soort records maken, en ze als parameter aan functies meegeven (maar, gek
genoeg, niet als resultaat opleveren).
In C is een vergelijkbare constructie mogelijk. Hier wordt een record, om zich van
de concurrentie te onderscheiden, een struct genoemd. Ook hier kun je de
structuur van zo’n struct eenmalig vastleggen, en vervolgens variabelen van dat
type declareren. (Je zou in de struct-definitie eigenlijk het woord typedef weer
verwachten. Waarom dat in C++ niet hoeft, en in C wel maar met weer een andere
onlogische draai, is een beetje ingewikkeld om uit te leggen, en ook eigenlijk niet
zo interessant.)
De onderdelen van een record, respectievelijk struct, zijn te benaderen met behulp
van de “punt-notatie”: de naam van de record-variabele, gevolgd door een punt en
het gewenste onderdeel; bijvoorbeeld: “voorzitter.leeftijd” .
21
Een goede programmeer-gewoonte, die mogelijk wordt gemaakt door het bestaan
van record/structure-typen, is het gebruik van zogenaamde abstracte datatype. Dit
is dan ook een standaard ingrediënt in de “gestructureerd programmeren”
didactiek.
Eerste stap is het definiëren van een stucture-type. Vervolgens worden een aantal
functies gedefinieerd die zo’n structure als eerste parameter hebben. Door middel
van commentaar wordt duidelijk gemaakt dat die functies bij elkaar en bij het type
horen.
In het hoofdprogramma kunnen nu variabelen van het nieuwe type worden
gedeclareerd. Deze variabelen kunnen worden gemanipuleerd door middel van het
aanroepen van de daarvoor bestemde functies. Zelfbeheersing moet ervoor zorgen
dat de variabelen ook alleen maar via die functies worden gemanipuleerd, en dus
niet door middel van direct aanspreken van de component-variabelen.
Het aardige van deze aanpak is dat de programmeur van het hoofdprogramma de
details van de functies niet hoeft te kennen: hij hoeft alleen maar de headers te
kennen (en een algemene beschrijving van het doel van de functies). Ook kan in
een later stadium de body van de functies worden aangepast, zonder dat het
hoofdprogramma daaronder te lijden heeft. Zo wordt het “hoe” van het “wat”
gescheiden, een belangrijk uitgangspunt in de software engineering.
22
Een voorbeeld van een abstract datatype is de modellering van een stapel brieven.
Eerst moet daartoe gedefinieerd worden wat een “brief” precies is. In het
voorbeeld nemen we aan dat dat in een eerder stadium al gebeurd is, en gaan we
verder om een ADT te maken van het begrip “stapel”.
In de type-definitie wordt beschreven dat een brieven-stapel is geïmplementeerd
door een array van brieven, tezamen met een integer die aangeeft tot hoever de
array gevuld is.
Vervolgens zijn er een drietal functies: een void-functie “leg” om een (als
parameter te specificeren) brief bovenop een (ook als parameter te specificeren)
stapel te leggen, een functie “pak” die de bovenste brief van een stapel pakt en als
resultaatwaarde oplevert, en een functie “werk” die aangeeft of er op een bepaalde
stapel nog werk (in casu: brieven) te wachten ligt.
Nu kunnen we het hoofdprogramma schrijven. Hierin kunnen, ondanks de
ingewikkelde interne structuur, met gemak variabelen van het type “Stapel”
worden gedeclareerd. Door aanroep van de functies kan nu bijvoorbeeld een
programma geschreven worden dat de brieven van de stapel “in” pakt en ze weer
neerlegt op de stapel “uit”.
De methodologie schrijft voor dat de programmeur van het hoofdprogramma zich
niet direct aan de component-variabelen vergrijpt. Een manier om dat te
garanderen is dat de programmeur van het ADT de type-definitie geheim houdt.
Het begrip ADT (abstract datatype) is enigszins in onbruik geraakt, sinds dit
principe ondersteund wordt door het object-georiënteerde (OO)
programmeerparadigma. In dit paradigma worden de abstracte datatypen klassen
genoemd, en de variabelen van zo’n type objecten.
Object-georiënteerd programmeren is populair geworden met de komst van
window-gebaseerde systemen, omdat windows zo handig gemodelleerd kunnen
worden als objecten. De taal C++ onstond in 1985 als object-georiënteerde
uitbreiding van C, en in Java (1995) is de object-georiënteerde aanpak nog
rigoreuzer aangehouden. Toch is het principe al ouder. Algemeen wordt de
(inmiddels vergeten) taal Simula (1975) als eerste OO-taal beschouwd. En het
principe van ADT’s was natuurlijk ook al langer bekend. De taal Smalltalk uit
1980 was een andere vroege OO-taal. In deze taal werden de eerste muis-bediende
computersystemen geprogrammeerd.
In een OO-taal horen de functies (die methodes worden genoemd) nu officieel bij
de type-definitie: dit wordt in de programmatekst aangegeven, niet alleen in
commentaar. De programmeur hoeft nu niet meer op te schrijven dat de methodes
het object als eerste parameter meekrijgen. Achter de schermen gebeurt dat nog
wel, maar je hoeft dat niet meer te noteren en kan het dus ook niet vergeten. De
attributen van het object mogen in de methodes direct gebruikt worden.
Buiten de methodes mogen de attributen van een object niet gebruikt worden;
probeer je het toch, dan wordt dat door de compiler afgekeurd.
Hier is nog eens hetzelfde voorbeeld (modellering van een stapel brieven), ditmaal
in de OO-notatie van Java.
De typedefinitie begint nu met het woord class, en behalve de attribuut-declaraties
staan ook de methode-definities daar in (de sluit-accolade van de klasse staat pas
helemaal aan het eind!). Anders dan in de ADT-benadering hebben de methoden
nu geen Stapel-parameter meer; de methoden “pak” en “werk” hebben dus
helemaal geen (expliciete) parameter meer over. De attributen “bak” en “aantal”
mogen in de methoden direct gebruikt worden.
Declaratie van de objecten in het hoofdprogramma is hetzelfde. Bij aanroep van de
methoden hoeft het object niet meer als parameter te worden meegegeven. Maar
het moet natuurlijk wel duidelijk zijn welk object de methoden moeten
manipuleren: daartoe heeft de punt-notatie een nieuwe betekenis gekregen.
Op het eerste gezicht lijken de attributen een soort globale variabelen te zijn: ze
mogen immers in alle methoden worden gebruikt. Maar het is belangrijk om te
bedenken dat er meerdere setjes van die variabelen kunnen zijn: elk object heeft
zijn eigen exemplaar. Wil je werkelijk begrijpen wat er gebeurt, bedenk dan dat dit
gewoon een andere notatie is voor het ADT-voorbeeld, en dat de objecten wel
degelijk als extra parameter worden meegegeven.
Waarschijnlijk is dat voor beginners niet echt verhelderend, en het is daarom dat
OO-gebaseerde leerboeken zo’n geheel eigen jargon ontwikkeld hebben voor de
belangrijkste OO-concepten.
23
Bij het programmeeren komt het vaak voor dat je wilt voortborduren op werk van
anderen (of van eigen eerder werk). De oudste manier om dat te doen is om een
kopie te maken van de klasse, en daarin toe te voegen wat nieuw is; zie in het
voorbeeld de klasse “Drie” rechtsboven, waarin een derde attribuut, en een nieuwe
methode is toegevoegd t.o.v. de klasse “Twee” (linksboven). Het nadeel van deze
aanpak is dat als later de methode “oud” in de klasse “Twee” wordt verbeterd
(bijvoorbeeld door hem te vervangen door een efficiëntere versie), deze wijziging
niet automatisch in de klasse “Drie” ook wordt doorgevoerd.
Een al wat betere aanpak heet “Delegatie” (linksonder). In de nieuwe klasse
worden alleen de extra attributen en methoden opgenomen, en verder een extra
attribuut, die een object van de oorspronkelijke klasse bevat. Een nadeel van deze
aanpak is de asymmetrie: de attributen moeten op verschillende wijze worden
benaderd. In het voorbeeld: het nieuwe attribuut z kan direct worden gebruikt,
maar het gebruik van de oude attirbuten x en y moeten worden gedelegeerd aan
het object t. Bij herhaalde delegatie krijg je dan expressies als a.b.c.d.e.x, en dat is
niet overzichtelijk.
In Object-georiënteerde talen is daarom een derde aanpak mogelijk:
“Subklassen” (rechtsonder). In de header kun je specificeren dat de klasse een
“extensie” is van een andere klasse. Je kunt dan zonder meer de attirbuten en
methoden van de oorspronkelijke klasse gebruiken, net als bij de knip-en-plak
werkwijze; maar nu kun je oorspronkelijke klasse nu wel zonder versie-probleem
wijzigen.
24
In alle object-georiënteerde talen kun je voor objecten van de subklasse de
methoden en attributen gebruiken die in de klasse waar deze een extensie van is
(de “superklasse”) zijn gedefinieerd. Deze attributen en methoden worden, zoals
dat heet, “geërfd” (inherited).
Object-georiënteerde talen verschillen in de manier waarop dit mechanisme verder
is uitgewerkt. Zo is het bijvoorbeeld in C++ mogelijk om een klasse een
uitbreiding te laten zijn van meerdere superklassen. Dat lijkt op het eerste gezicht
handig, maar wat gebeurt er als die klassen zelf allebei een extensie zijn van een
vierde klasse? Erf je de attributen daarvan dan éénmaal of tweemaal? Om dit soort
vragen te vermijden is in Java slechts één superklasse toegestaan.
Attributen en methoden kunnen bedoeld zijn voor gebruik door wie dat maar wil
(public), of alleen voor gebruik door andere methoden in dezelfde klasse (private).
Bij het maken van een subklasse rijst de vraag of je daarin nu wel of niet gebruik
mag maken van private attributen en methoden van de superklassen. In C++ is het
antwoord “ja”; in Java luidt het “nee”, maar is er een derde soort beveiliging:
protected attributen en methoden mogen wél in de subklassen, maar niet door de
buitenwereld worden gebruikt.
25
Declaraties zijn aanwijzingen voor de compiler over het te reserveren geheugen.
Op dat moment is de waarde van variabelen nog niet bekend. Daarom moet de
lengte van arrays in Pascal, C en C++ tijdens de compilatie vastliggen: het moet
een constante zijn. Ook is het niet mogelijk om in bijvoorbeeld een for-opdracht
steeds opnieuw een variabele te declareren.
Toch is het in deze talen mogelijk om tijdens het uitvoeren van het programma te
beslissen om nieuw geheugen te gaan gebruiken. Daartoe is het nodig om pointers
te gebruiken. Pointers zijn variabelen die kunnen verwijzen naar geheugen. De
pointers zelf moeten wel gedeclareerd worden, en hebben dus een naam, maar het
geheugen waar ze naar wijzen wordt tijdens het programmaverloop gereserveerd.
De compiler heeft daar geen weet van, en dit geheugen is dan ook naamloos. Toch
is dit geheugen te gebruiken, en wel door de pointer (de “wijzer”) te volgen. Daar
is een speciale notatie voor.
26
Dit is een voorbeeld van het gebruik van pointers in Pascal. Er worden drie
variabelen gedeclareerd: k is een losse integer, jan is een Persoon-record, en p is
een pointer die ooit nog eens naar een Persoon-record kan gaan wijzen, maar dat
door puur de declaratie nog niet doet. In het type wordt dat aangegeven met een
pijltje (^).
In het programma worden de variabelen van waarden voorzien. De integer k krijgt
een waarde door een toekenningsopdracht. De componenten van het record jan
kunnen ook door toekenningen een waarde krijgen. De ponter-variabele p krijgt
echter een waarde door middel van de speciale opdracht new(p). De waarde van p
wordt hiermee een verwijzing naar een stuk run-time aangemaakt geheugen: een
naamloos Persoon-record.
De componenten van het record-waar-p-naar-wijst kunnen worden aangesproken
met de notatie p^. ; het pijltje duidt er op dat de pointer gevolgd moet worden.
Didactisch belangrijk bij het tekenen van dit soort plaatjes is het benadrukken dat
het de pijl is die de waarde is van de pointer-variabele. Dat wordt hier nog eens
gesymboliseerd door de stip bij de oorsprong van de pijl, die heel duidelijk in de
variabele ontspringt, terwijl de pijlpunt juist naar de rand van de aangewezen
record wijst. Het is opvallend hoe slordig hier in tekstboeken vaak mee wordt
omgesprongen (bijvoorbeeld, pijlen die ontspringen aan de rand van de pointervariabele). Let ook op dat er géén pijl staat tussen de naam van een variabele en
het hokje dat deze geheugenruimte symboliseert: daar loopt immers geen pointer.
Dit is nogmaals hetzelfde voorbeeld, maar ditmaal in C++. Ook nu wordt bij de
declaratie van p aangegeven dat het om een pointer-variabele gaat (in C++ gebeurt
dat met een sterretje in plaats van met een pijltje).
De manier waarop een pointer naar nieuw geheugen gaat wijzen verschilt van
Pascal. Ook nu wordt het keyword new gebruikt, maar in C++ heeft dit de status
van een expressie, niet van een opdracht. Daarom kan de frase new Persoon() aan
de rechterkant van een gewone toekenningsopdracht aan p staan.
Voor het manipuleren van de componenten van de structure-waar-p-naar-wijst is er
de speciale notatie “->”, die aan een pijltje moet doen denken. Ook toegestaan is
de notatie “(*p).leeftijd”, die meer op de Pascal-versie lijkt, maar die door het
benodigde extra paar haakjes in de praktijk zelden gebruikt wordt.
Je kunt in C++ pointers ook laten wijzen naar variabelen die wèl zijn gedeclareerd,
mits die van het geschikte type zijn. Daartoe kan met de speciale operator “&” een
pijl worden gecreëerd naar een variabele, die vervolgens in de pointer-variabele
kan worden opgeslagen.
Hoewel Java in veel opzichten lijkt op C++, is het gebruik van pointers duidelijk
anders. Om te beginnen is het in Java helemaal niet mogelijk om objecten direct te
declareren: de variabele jan in het voorbeeld kan dus niet worden gedeclareerd
zoals in Pascal en C++. Wel is het mogelijk om de pointer p te declareren. Omdat
verwarring met losse object-variabelen (die immers niet zijn toegestaan) niet
mogelijk is, hoef je in Java het pointer-zijn van p niet expliciet met een pijltje of
sterretje aan te geven.
Ook bij manipulatie van attributen (en methoden) van het object kan direct de
punt-notatie worden gebruikt. Voor C-programmeurs is dat even wennen.
Er wordt wel eens gezegd dat Java, in tegenstelling tot C++, geen pointers kent.
Dat is niet juist: in zekere zin kent Java eigenlijk alleen maar pointers!
Objecten, en ook arrays, worden in Java altijd dynamisch aangemaakt; dat wat
gedeclareerd wordt is alleen maar de pointer ernaartoe. Een van de voordelen
hiervan is, dat de lengte arrays in Java niet bij compilatie hoeft vast te liggen: de
lengte hoeft geen constante te zijn. Wel wordt de lengte van de array “bevroren”
op het moment dat het dynamische geheugen wordt gecreëerd.
27
Als een pointer naar één enkel object wijst, is de winst nog niet groot. Immers, dat
object hoeft dan wel niet gedeclareerd te worden, maar de pointer zelf weer wel,
en daarmee ligt de hoeveelheid geheugen toch weer reeds bij het compileren vast.
Interessant wordt het pas als de objecten zelf ook weer pointers bevatten naar
andere objecten, die ook weer pointers bevatten… Op die manier kunnen gelinkte
datastructuren ontstaan waarin alle objecten naamloos zijn, maar die via-via wel te
bereiken zijn vanuit een enkele pointer.
Ook leuk is het als een object een pointer bevat naar eenzelfde type object. Je kunt
dan recursieve datastructuren maken: ketens van objecten die allemaal naar elkaar
wijzen. De keten kan worden afgestopt met de speciale pointer-waarde nil
(Pascal), NULL (C++) of null (Java). Zo’n keten van objecten kun je bijvoorbeeld
gebruiken als alternatief voor een array. Anders dan bij een array kun je in zo’n
keten door het omleggen van een paar pointers heel gemakkelijk elementen
tussenvoegen.
Helemaal spannend wordt het als er in het object twee pointers naar gelijksoortige
objecten staan: er onstaan dan vertakte, boom-vormige datastructuren. Dat soort
datastructuren kunnen op een heel natuurlijke wijze worden gemanipuleerd met
recursieve functies.
28
Hier is nog eens het schema van programmeerparadigma’s. Imperatieve talen
hebben met elkaar gemeen dat er een vorm van toekenningsopdracht is.
Procedurele talen voegen daar een concept van tekstueel afgebakende proceduredefinities aan toe. In object-georiënteerde talen kunnen de procedures (methoden)
bovendien worden gekoppeld aan de datatypen waar ze op werken.
Als we een wezenlijk ander paradigma dan het imperatieve willen bekijken, dan
moeten we dus de toekenningsopdracht afzweren. Op het eerste gezicht lijken
variabelen en toekenningsopdrachten onlosmakelijk verbonden met de computer.
Toch is het mogelijk om zonder deze begrippen tot nuttige programmeertalen te
komen. In het functionele paradigma staan de begrippen waarde, expressie, en
functie veel centraler dan variabele, opdracht en procedure. In het logische
paradigma staan de begippen feit, voorwaarde en relatie centraal.
De programma’s in een declaratieve programmeertaal kunnen gewoon worden
uitgevoerd op een computer, die wel degelijk over een toekenningsopdracht
beschikt. Deze paradox is echter niets nieuws: het was immers ook mogelijk om
de goto-opdracht af te zweren, en toch een processor te gebruiken waarin zo’n
opdracht wel degelijk bestaat. De compiler schermt ons af voor de gevaren en de
verleidingen ervan. Zo is het ook met de declaratieve talen: de compiler zorgt
ervoor dat het programma wordt vertaald naar een gangbare machinetaal; de
details daarvan zijn voor de programmeur echter niet relevant.
In een declaratieve programmeertaal zijn er geen variabelen, en geen
toekenningsopdrachten. Er zijn zelfs helemaal geen opdrachten, en we hoeven ons
dus ook geen zorgen te maken over de volgorde waarin de opdrachten worden
uitgevoerd. Maar wat is een programma dan wel?
Een programma bestaat voor het grootste deel uit de definitie van functies (in een
functionele taal) of relaties (in een logische taal). We beperken ons nu even tot
functionele talen. Die functies zijn vergelijkbaar met Pascal/C/Java-functies,
alleen mag er in de body van die functies alleen maar een return-opdracht staan.
Het hoofdprogramma bestaat uit de aanroep van één enkele functie, waarvan dan
de waarde berekend wordt. Maar dat is eigenlijk niets nieuws: ook in C/Java
bestaat het hoofdprogramma uit de aanroep van de main-functie.
Het begrip waarde moet je daarbij ruim zien: het resultaat van zo’n functie zou
bijvoorbeeld een array van characters kunnen zijn, zodat het programma wel
degelijk in staat is tot verwerking van iets ingewikkelders dan een losse
getalswaarde.
Als voorbeeld bespreken we op de volgende pagina’s enkele aspecten van de
functionele taal Haskell.
Het aardige van functionele programma’s is dat ze ideeën zo mooi compact
kunnen uitdrukken, zonder veel notationeel gedoe.
Neem bijvoorbeeld de functie even, die bepaalt of zijn parameter deelbaar is door
twee (en dus een even getal is).
In Java is die functie gemakkelijk te schrijven: de body bestaat uit een enkele
return-opdracht die de gewenste test uitvoert.
In Haskell is zelfs dat woord “return” niet nodig, omdat de body altijd uit alleen
maar de resultaat-expressie bestaat. Ook de accolades om de body zijn niet nodig,
omdat de body één enkele expressie is. En nu we toch aan het bezuinigen zijn: de
haakjes rond de parameter zijn ook niet nodig. Het even-zijn van x is simpelweg
de uitkomst van de test “x%2==0”, en dat kun je dan ook direct zo opschrijven.
De types van parameter en resultaat worden in Haskell in een aparte kopregel
opgeschreven. Het symbool “::” dient daarbij uitgesproken te worden als “heeft
het type”.
29
Ook in funtionele talen ontkomen we niet aan het begip “keuze”. Maar bij het
ontbreken van opdrachten is dat niet een voorwaardelijke opdracht, maar een
voorwaardelijke functie-definitie.
In Haskell is er een speciale notatie voor zo’n voorwaardelijke functie-definitie.
Voorafgaand aan de “=” die in het midden van elke functiedefinitie staat mag,
achter een verticale streep, een voorwaarde staan. In één functiedefinitie mogen
meerdere voorwaarden met bijbehorende resultaatwaarde staan.
Als je al die voorwaarden netjes onder elkaar zet, kun je ook gemakkelijk overzien
of je niet een situatie vergeten bent.
30
In Haskell is er geen aparte constructie voor het begrip “herhaling”. Maar dat is
ook helemaal niet nodig, want we hebben al functies en keuze, en met behulp van
recursie is daarmee ook een vorm van herhaling te maken. Recursie is in
functionele talen niet iets bijzonders, maar een zeer veelgebruikt mechanisme.
Op deze manier kan bijvoorbeeld een machtsverhef-functie gemaakt worden.
Anders dan in Haskell is er geen hulpveriabele nodig, noch een tellertje. We
specificeren gewoon direct dat de uitkomst 1 is als de parameter 0 is, en anders
moet de functie recursief worden aageroepen en de uitkomst nog eenmaal met x
vermenigvuldigd worden.
Een aardigheidje van Haskell is dat je naast functies ook infix-operatoren zelf mag
definiëren. Zo kan de machtsverhef-functie als machtsverhef-operator gescheven
worden.
Een andere bijzonderheid is te zien in de alternatieve formulering van de operatordefinitie. We zien hier de expressie n+1 staan op de plaats van de formele
parameter. Dat is raar, want we zijn op die plaats een declaratie gewend! Toch
heeft dit betekenis. Zouden we bijvoorbeeld 3^6 aanroepen, dan zorgt de compiler
ervoor dat x de waarde 3 krijgt (zoals gebruikelijk), en dat n+1 de waarde 6 krijgt,
en dus n de waarde 5. Die waarde van n kunnen we direct gebruiken aan de
rechterkant van de functiedefinitie.
Ook constanten zijn toegestaan op de plaats van de formele parameter. In het
voorbeeld wordt zo het geval dat de exponent gelijk is aan 0 afgehandeld.
Om grotere hoeveelheden informatie te kunnen verwerken dan losse getallen,
kennen functionele talen ook datastructuren. Een zeer belangrijke is de lijst. (De
eerste functionele taal dankt daar ook zijn naam aan: Lisp is een afkorting van
“List Processor”).
Een lijst bevat 0 of meer elementen. Een lijst zonder elementen kun je in Haskell
aanduiden met [ ], een lijst met elementen kun je opbouwen met gebruikmakeing
van de operator : , die een nieuw element op kop van een al bestaande lijst zet.
Door herhaalde toepassing van : kun je de lijst zo lang maken als je wilt.
Functies die een lijst als parameter hebben, bestaan vaak uit twee helften: een
regel specificeert wat de uitkomst is als de parameter toevallig de lege lijst is (dit
is weer een geval waarbij een constante dienst doet als formele parameter, net als
in de definitie van x^0 in het vorige voorbeeld). Is de lijst niet leeg, dan is hij
ontstaan door met de operator : een element x op kop te zetten van een lijst xs.
Let ook op de manier waarop in het voorbeeld het type van de parameter wordt
aangegeven: [Int] is het type “lijst met elementen van type integer”, niet te
verwarren met een expressie als [3] (“de lijst met als enige element 3”).
Voor sommige functies die op lijsten werken is het niet van belang wat het type
van de elementen is. De functie lengte bijvoorbeeld, die het aantal elementen van
een lijst telt, kan dat doen op lijsten van integers, maar ook op lijsten van
characters of wat dan ook.
In Haskell kan het type van dergelijke polymorfe functies worden aangeduid met
behulp van type-variabelen. Dit is eigenlijk een heel voor de hand liggen principe,
en het is dan ook verbazingwekkend hoe lastig een vergelijkbare functie te
schrijven is in de bekende imperatieve talen.
In Pascal kan het helemaal niet, in C++ is er een speciale template constructie die
niemand begrijpt. In Java kun je objecten van verschillend type alleen in een array
onderbrengen als je er een array van Object-ojecten van maakt. Op het moment dat
je specifieke eigenschappen van zo’n array-element wilt gebruiken, moet je dat
object met een cast weer converteren tot het gewenste type. De controle of dat
geoorloofd is wordt dan run-time uitgevoerd, iets wat een verslechtering is ten op
zichte van compile-time typecontrole.
31
In een functionele taal is een functie een “waarde” met dezelfde rechten als iedere
andere waarde. Zo kan een functie bijvoorbeeld ook als parameter worden
meegegeven aan een andere functie. Let wel, de functie als geheel; niet het
resultaat van de aanroep van de functie.
Een mooi voorbeeld hiervan is de functie die meestal map wordt genoemd. Deze
functie heeft twee parameters, waarvan de eerste zelf een functie is, en de tweede
een lijst is. Het resultaat van de functie is ook een lijst. Dat staat ook duidelijk te
lezen in de typerings-regel van de functie. Bovendien is hier te zien dat de map
polymorf is: de functie-parameter mag een willekeurige functie zijn van a naar b,
mits de lijst-parameter dan een lijst van a’s is; het resultaat zal dan een lijst van b’s
zijn.
De functiedefinitie splitst de gevallen uit dat de lijst leeg is of niet. Bij een lege
lijst is het resultaat ook leeg; anders wordt de functie-parameter op het eerste
element toegepast, en map recursief aangeroepen op de overige elementen. Netto
effect: de functie-parameter wordt op alle elementen van de lijst-parameter
toegepast.
Later kun je de map-functie gebruiken om alle elementen van een bepaalde lijst te
bewerken. Het is een beetje te vergelijken met een for-opdracht in een imperatieve
taal, met dat verschil dat de for-opdracht hard is ingebouwd, en dat je de mapfunctie zelf kunt maken! Je kunt zo zelf een repertoire van controle-structuren
bouwen. De filter-functie is een andere voorbeeld hiervan. Niets ingebouwds aan!
32
Het is opmerkelijk hoe simpel bepaalde algoritmen er uit kunnen zien, als ze
eenmaal ontdaan zijn van de notationele ballast die ze bij formulering in de
gangbare imperatieve talen hebben.
Neem bijvoorbeeld het invoegen van een nieuw element in een geordende lijst. Is
de lijst leeg, dan wordt het nieuwe element het enige. Zat er al minstens één
element in de lijst, dan hangt het ervan af of het nieuwe element kleiner is dan dat.
Zo ja, dan komt het nieuwe element op kop en blijft de rest ongewijzigd; zo nee,
dan moet het nieuwe element elders in de staart worden ingevoegd, en blijft het
oude element op kop.
Sorteren is vervolgens gemakkelijk: een lege lijst is al gesorteerd, en anders voeg
je het eerste element op de juiste plaats in in de recursief gesorteerde rest.
Met heel eenvoudige middelen kunnen we in de functionele notatie de essentie van
het sorteer-algoritme in kwesie doorzien. In een imperatieve formulering met
arrays, hulpvariabelen, geneste for-opdrachten, parameter-mechanismen en wat
dies meer zij wordt het begrip toch al snel vertroebeld.
Hier is nog een ander sorteer-algoritme.
Een lege lijst is vanzelf gesorteerd. Anders is er een eerste element x en een rest xs.
We kunnen de lijst xs in twee delen splitsen: de elementen die kleiner zijn dan x,
en de elementen die groter zijn dan x. Deze twee helften kunnen we bepalen met
twee aanroepen van de functie filter.
De twee helften sorteren we allebei met een recursieve aanroep van sort, en
vervolgens plakken we de resultaten aan elkaar met de standaard-operator ++,
waarbij we het element x in het midden zetten.
Dit algoritme staat bekend onder de naam quicksort, en wie alleen de imperatieve
formulering ervan kent zal niet direct willen geloven dat het eigenlijk zo simpel is!
Let nog even op de aanroep van filter. De eerste parameter van filter moet een
functie zijn met het type a->Bool. Die functie bouwen we door de operator < , die
zelf twee parameters heeft en een Bool resultaat, alvast te voorzien van één van
zijn twee parameters. Wat overblijft is een functie met één parameter, die we
kunnen meegeven aan filter. Dit heet partiële parametrisatie, en komt goed van
pas in situaties zoals deze.
Een ander declaratief programmeerparadigma dan het functionele is het logische
paradigma. De bekendste taal uit dit paradigma is Prolog (een afkorting van
“Programming in Logic”).
In Prolog bestaat een programma uit de definitie van een aantal relaties. Er zijn
twee manieren om een relatie te definiëren: door het geven van feiten en door het
geven van afleidingsregels.
In het voorbeeld specificeren we door middel van het opsommen van een aantal
feiten welke personen met elkaar in de moeder-relatie staan (of liever gezegd: in
de moeder-kind-relatie). Elke database is in wezen een opsomming van feiten:
elke tabel specificeert een relatie met zoveel parameters als er kolommen in de
tabel staan.
Wat niet kan in een database is het geven van afleidingsregels. In het voorbeeld
specificeren we dat A de oma is van C als er een B te vinden is zodat A de moeder
is van B en B de moeder van C.
33
Het starten van een logisch programma gebeurt door het doen van een query. In
het voorbeeld kunnen we bijvoorbeeld vragen wie de oma is van Alex. Het
antwoord zal zijn dat Juul in de oma-kleinkind-relatie staat met Alex.
Het aardige is, dat het niet uitmaakt op welke plaats de onbekende staat. Je kunt
dus ook vragen van wie Mien de oma is. Er zijn twee antwoorden: Mien is de oma
van zowel Bea als Griet.
Het is zelfs mogelijk om op beide parameter-posities een onbekende te plaatsen.
Als antwoord krijg je dan alle oma-kleinkind-combinaties die door middel van de
afleidingsregels in het programma afleidbaar zijn.
Dit is het meest prominente verschil met het functionele paradigma: functies zijn
slechts in één richting te gebruiken en hebben één resultaat; relaties zijn ook in
“omgekeerde” richting te gebruiken, en queries kunnen meerdere resultaten
hebben.
Relaties hebben altijd één parameter meer dan de overeenkomstige functie in een
functioneel programma; immers, in de definitie van de relatie neemt het resultaat
van de functie ook een parameter-positie in.
34
In logische programma’s kun je relaties gebruiken om nieuw relaties te definiëren.
Zo kun je bijvoorbeeld een tante-relatie definiëren met behulp van de oma- en
moeder-relatie: T is de tante van neefje/nichtje N als er een grootmoeder X is die
de moeder is van T en tegelijkertijd de oma van N.
Op deze manier is de relatie echter te ruim gedefinieerd: iemands eigen moeder
zou volgens deze definitie ook als “tante” beschouwd worden, en dat is niet de
bedoeling. Dit soort uitzonderingen worden nogal eens over het hoofd gezien (het
is ook een rijke bron van raadseltjes). In een logisch programma komen ze echter
keihard aan het licht.
Om de definitie te repareren moeten we gebruik maken van de not-operator van
Prolog. En daarmee begeven we ons op glad eis: de voorwaarde stelt dat T niet de
moeder van N mag zijn. De computer controleert of dit het geval is – althans, op
grond van de kennis die in het programma aanwezig is. En daar zit een belangrijk
probleem: de not-operator werkt alleen zoals je zou verwachten indien de closed
world assumption geldt: alle relevante feiten zijn in het programma gemodelleerd.
De alternatieve formulering, die gebruikt maak van de “ongelijk”-operator, kent
dezelfde restrictie.
Reflectievragen
(de nummers refereren aan de nummers van de dia’s, tevens bladzijden in dit
boekje)
4. Welke concrete componenten van een moderne computer zijn direct als
geheugen te beschouwen? Welke op een enigszins gekunstelde wijze ook?
Waarom is het niet correct om een chip zoals de Pentium puur als “processor” te
omschrijven?
15. In Pascal wordt onderscheid gemaakt tussen procedures en functies. Hoe
worden deze twee respectievelijk in C genoemd? Een Java-methode is niet precies
hetzelfde als een C-functie. Wat is in Java het precieze equivalent van een Cfunctie?
16. In C en Java bestaat er toch ook een return-opdracht. Wat is daarvan de
betekenis?
5. Welke types kunnen er nog meer zinvol zijn? Ligt de codering van een bepaald
type eenduidig vast? Zo ja, hoe dan; zo nee, ken je voorbeelden van types
waarvoor verschillende coderingen gebruikt worden?
18. Kan een lokale variabele worden gebruikt om bij een eerste aanroep een
waarde te bewaren, om die bij een latere aanroep weer terug te vinden en verder te
bewerken (bijvoorbeeld: een variabele die bijhoudt hoe vaak een functie is
aangeroepen)? Zo ja, wat is dan de waarde van die variabele bij de eerste aanroep;
zo nee: hoe kun je dit dan wel doen?
9. Welke connotaties worden opgeroepen door de variabele-namen x, y, i, j, c, h, n,
f, temp, max, hulp, foo, aap? Zijn er in je eigen vakgebied variabele-namen die
een vaste betekenis hebben?
19. Bekijk het voorbeeld “Namensorteren” in paragraaf 7.6 van deel 1 van het
EduActief-boek. Welk bezwaar kent dit programma? Hoe kan het verbeterd
worden?
10. Strings worden met een speciaal symbool begonnen en geëindigd. Hoe moet
dat als je dat speciale symbool zelf in een string wilt opnemen? In diverse talen
zijn er verschillende oplossingen voor gekozen. Welke ken je?
20. Kun je in de body van een functie een toekenning doen aan een variabele die
als parameter is meegegeven? Is dat zinvol? Is dat wenselijk?
12. Vermenigvuldigen gaat voor optellen. Daarom heeft a*a+b*b de betekenis
(a*a)+(b*b), ook als je die haakjes niet opschrijft. Deze gewoonte is gegroeid,
omdat deze betekenis het meest waarschijnlijk is, en je dus alleen in
uitzonderingsgevallen met haakjes hoeft aan te geven dat je (a*a+b)*b bedoelt.
Hoe zit het met de prioriteit van de == operator ten opzichte van + en * ? En met
de prioriteit van de logische “en” en “of”-operator? Wat is het handigst? Hoe is dit
in je favoriete taal geregeld? En hoe moet herhaald gebruik van dezelfde operator
worden geïnterpreteerd, zoals 1-2-3 of 1/2/3 of x=y=z ?
13. De huisstijl van Microsoft voor het schrijven van Windows-programma’s
schrijft voor dat aan de naam van variabelen het type is af te lezen. Zo begint een
integer altijd met de letter “n” (bijvoorbeeld nAantal), een pointer met de letter
“p”, en een string met de letters “lpsz” (bijvoorbeeld “lpszNaam”), omdat het een
“long pointer to string with zero termination” gaat. Wat moeten we daarvan
vinden?
14. In C en Java mag je schrijven: int x = 0; . Is dit een declaratie of een opdracht?
Wat zijn hiervan de didactische consequenties?
21. Welke ambigue situatie onstaat er, als default-parameters ook in het midden
van de parameterlijst zijn toegestaan?
22. Welke packages kent Java zoal, en welke klassen zijn in de diverse packages
geplaatst?
25. Een gevaar van while-opdrachten is dat de voorwaarde altijd maar waar blijft,
en de computer dus steeds maar bezig blijft met het uitvoeren van de body ervan.
Zijn daar nuttige toepassingen voor? Kun je een while-opdracht schrijven die
gegarandeert “blijft hangen”? Kan een compiler waarschuwen voor dit soort
while-opdrachten?
26. Hoe kun je de for-opdracht van C en Java simuleren met behulp van een whileopdracht?
27. Hoe zou je iets kunnen zeggen over de correctheid van een while-opdracht?
28. Stel dat V een voorwaarde is bij de uitvoering van een while-opdracht. V is een
logische (Boolean) expressie, die “waar” of “onwaar” kan zijn. Welke logische
35
expressie geldt in Pascal direct na afloop van de while-opdracht in ieder geval? Is
dat in Java ook zo?
29. Een procedure levert geen resultaatwaarde op. Hoe kan een procedure dan toch
blijvende resultaten tot gevolg hebben, m.a.w.: hoe kun je merken dat een
procedure aangeroepen geworden is geweest?
30. Kun je in een (niet-void) functie iets printen? Zo nee, waarom niet; zo ja, is dat
een goed idee?
31. Wat kun je je voorstellen bij een “terugkeerpositie”? Hoeveel ruimte neemt
zo’n stack in het geheugen in?
32. Wat gebeurt er met de lokale variabelen van een functie, als deze functie
zichzelf recursief aanroept, en er dus een nieuwe incarnatie onstaat met diezelfde
variabelen?
33. Hoe ziet in C of Java een for-opdracht die een array behandelt er typisch uit?
34. Hoe is de uitspraak van het woord “boolean”? En van “bool”? En van “char”?
35. Kan een van de velden van een record van datzelfde recordtype zijn, m.a.w.
bestaan er “recursieve” records? Zo ja, is daar een toepassing voor; zo nee,
waarom niet?
36. Het gebruik van abstracte datatypes had het millenium-probleem kunnen
voorkómen. Hoe?
43. In C en C++ is het in principe ook mogelijk om een pointer naar een pointer te
maken. Is daar een toepassing voor? Kan dit in Java ook?
44. In C en C++ is het mogelijk om een pointer “op te hogen” (met de notatie “p+
+”), en zelfs om ermee te rekenen (“p=p+5”). Wat is daarvan een toepassing?
Waarom is dit in Java verboden?
45. Waarom staan pointers in C en C++, in tegenstelling tot in Java, bekend als
“onveilig”?
46. Stel dat in de getekende recursieve datastructuur in elk object een pointer
genaamd “next” naar het volgende object wijst, en er daarnaast een integer
genaamd “data” bestaat. Schrijf een functie met twee parameters die nagaat of een
gegeven integer (eerste parameter) ergens in een gegeven lijst (tweede parameter)
voorkomt.
48. Beschouw een niet-windows programma, dus een programma dat invoer krijgt
vanaf een toetsenbord, en tekstuitvoer print op een scherm. Hoe kan zo’n
programma worden beschouwd als wiskundige functie, d.w.z. wat zijn de types
van parameter en resultaat (of, in wiskunde-jargon, wat zijn het domein en het
bereik)? Hoe zit dat voor window-gebaseerde programma’s?
49. Hoe kun je in Java het symbool “=” didactisch verantwoord uitspreken? En in
Haskell? En het symbool “==”?
50. Hoe luidt de definitie van de “absolute waarde” functie in Haskell?
37. In welke zin kan het gebruik van ADT’s de correctheid van programma’s
bevorderen?
51. Waar moet je op leteen bij het schrijven van een recursieve functie? En bij het
gebruik van een while-opdracht?
38. Methoden in Java die zonder parameter worden gedeclareerd, hebben impliciet
dus tóch een parameter. Hoe kan daaraan in de body worden gerefereerd?
52. Wat is in Haskell het type van de operator : ? En van de lege lijst [ ] ?
39. Op welke manier kunnen in Java de attributen van een object worden
beschremd tegen onbevoegd gebruik?
42. Wat kun je jeconcreet voorstellen bij een “pointer”? Hoeveel ruimte neemt
zo’n pointer in het geheugen in? Is het mogelijk om de “waarde” van een
pointervariabele (dus niet van het object waar die naar wijst) af te drukken? Te
testen op gelijkheid?
36
53. Kun je in Java een array van arrays maken? Zijn al die arrays dan even lang, of
kunnen ze variëre in lengte? Hoe zit dat in Haskell? Kun je ook lijsten van lijsten
van lijsten maken? Zijn daar toepassingen voor?
54. Schrijf een Haskell-functie die alle getallen in een lijst bij elkaar optelt (en 0
oplevert bij een lege lijst). Kun je de functie generaliseren, zodat er in plaats van
optelling een andere, als parameter te specificeren operator gebruikt wordt (en een,
als extra parameter te specificeren, neutrale waarde voor het geval van de lege
lijst)? Wat is het type van die functie?
Praktijkopdrachten
55. Probeer sort te schrijven als aanroep van de hierboven geschreven functie.
Bereid met z’n tweeën een presentatie voor van circa 20 minuten, liefst
ondersteund door Powerpoint-plaatjes en/of “live” voorbeelden. Als doelgroep
gelden je collega’s; het nivo hoeft niet hoger te liggen dan je bij leerlingen zou
gebruiken. Kies één van onderstaande casus, en plaats ze in de context van de
bespreking van programmeerparadigma’s. Leg ook een relatie tot de doelen van
programmeeronderwijs, zoals je die bijvoorbeeld in de CITO-syllabus aantreft. Je
kunt je daarbij laten inspireren door de gestelde vragen, al hoeven niet alle vragen
aan de orde te komen. Het is niet nodig dat het gehoor tot in de details de taal uit
de besproken casus leert kennen (al kan een voorbeeld natuurlijk heel
verhelderend werken).
56. Een functie met twee parameters heeft het type a->b->c . Dat lijkt een
enigszins merkwaardige notatie; logischer lijkt iets als (a,b)->c . Verklaar waarom,
in verband met de mogelijkheid van partiële parametrisatie, de notatie toch zinvol
is. Moet a->b->c begrepen worden als (a->b)->c of als a->(b->c) ?
57. Een relatie heeft altijd een parameter meer dan de overeenkomstige functie.
Verklaar dit.
Leerdoelen van deze opdracht zijn:
• het kunnen hanteren van de concepten uit de verschillende paradigma’s
• het zelfstandig eigen kunnen maken van de hoofdzaken van een nieuwe
programmeertaal (zonder die taal in detail te leren), op grond van
herkenningspunten die de cursus heeft gegeven
• het doceren van informatica-theorie, en het daarbij hanteren van ITpresentatiemiddelen daarbij
• het plaatsen van leerstof binnen de doelen van informaticaonderwijs
1.
2.
Spreadsheets. Bekijk een spreadsheet, bijvoorbeeld Microsoft Excel. Dit is
op te vatten als programmeertaal. Wat is als een programma te beschouwen,
en hoe wordt het uitgevoerd? Tot welk paradigma behoort deze
programmeertaal? Zijn er variabelen; hoe verloopt de naamgeving daarvan, en
wat kun je er vanuit het programma mee doen? Zijn er functies, en kun je die
zelf ook maken? Is de taal getypeerd, en wat gebeurt er dan bij typeringsfouten? Hoe verloopt keuze? Is er herhaling, recursie? Wat komt overeen met
een “array”, en hoe verloopt indicering daarin? Hoe programmeer je een
opzoek-tabel? Er is een pakket met programmeeropdrachten in spreadsheets
beschikbaar. Gebruik tenminste één van die opdrachten om daar je verhaal
mee te illustreren.
Postscript. Postscript is een taal die in eerste instantie bedoeld is om
documenten vorm te geven (net als HTML). Maar Postscript is ook te
beschouwen als programmeertaal. Wat is een programma, en hoe wordt het
uitgevoerd? Tot welk paradigma behoort Postscript? Zijn er variabelen, en kun
je die inspecteren en veranderen? Hoe kunnen expressies worden
uitgerekend? Hoe verloopt keuze en herhaling? Kun je procedures en functies
maken, en kunnen deze parameters krijgen?
37
3.
4.
5.
6.
7.
8.
9.
38
Haskell versus wizards. Bekijk de Haskell-functies map, filter, foldr, en zip.
Hoe zou je vergelijkbare zaken in Java aanpakken? In Java schrijf je foropdrachten vaak op een routinematige manier. Kun je tot een klassificatie
komen van “typische for-opdrachten”, steunend op het begrippenkader van
Haskell? In sommige Java-compilers zijn “wizards” voorhanden om routineus
samengestelde stukjes programma voor je te schrijven. In welk opzicht schiet
Java als taal tekort, dat de behoefte aan dergelijke wizards zich doet voelen?
Diagrammen. Bij het ontwerpen van programma’s wordt soms gebruikt
gemaakt van grafische weergaves van de opzet van het programma. Bekende
typen diagrammen zijn: flow-charts, structuur-diagrammen, E-R-diagrammen,
UML-ontwerpen. Op welke programmeerparadigma’s zijn deze (en eventuele
andere) diagramtypen toegesneden? Biedt het gebruik ervan didactische
voordelen, en zo ja welke? (Let op, deze vraag dient vooral kritisch
beantwoord te worden. Sommige veelgebruikte diagramtypen bieden bepaald
geen voordelen, andere wel!)
Lay-out. Bekijk programma’s in Pascal, C, C++ en Java uit diverse bronnen.
Welke lay-out conventies zijn in gebruik voor blokstrucktuur (begin/end
respectievelijk accolades) en de naamgeving van variabelen e.d.? Welke
daarvan verdient uit didactisch oogpunt de voordelen? Hoe kan commentaar
worden geschreven in deze talen, en op welke plaats gebeurt dat vaak/zou dat
het beste kunnen gebeuren?
Ontleden in Haskell. Grammatica’s zijn een goede manier om een taal te
beschrijven. Het opstellen van zo’n grammatica is echter een theoretische
aangelegenheid, en daarna heb je nog niet iets werkends in handen. Bekijk
welke functies in Haskell geschreven kunnen worden om het gebruik van
grammatica’s te ondersteunen.
Familierelaties in Prolog. De documentatie van de programmeertaal Visual
Prolog geeft een groot aantal voorbeelden van toepassingen van deze taal.
Verzamel tenminste vijf voorbeelden van programma’s waarin familierelaties
worden beschreven, beschrijf de afleidingsregels die daarin gebruikt worden
en geef voorbeelden van relaties die niet of veel moeilijker binnen dit kader
geformuleerd kunnen worden.
De Prolog-moordzaak. Eén van de toepassingen van Prolog is het oplossen
van puzzels. Zo bevat één van de voorbeelden bij Visual Prolog de
beschrijving van allerlei feiten die te maken hebben met een moordzaak. Ook
allerlei redeneringen die je zou kunnen gebruiken om de dader te vinden zijn
in het programma opgenomen. Zoek uit welke in Prolog ingebouwde
mechanismen het mogelijk maken zo inderdaad de moordzaak op te lossen.
Practica in LOGO. De programmeertaal Logo wordt vaak gebruikt bij
inleidend programmeeronderwijs. Het gebruik van de “turtle graphics” wordt
door sommigen gezien als een attractieve manier om de eerste concepten van
het werken met een programmeertaal te behandelen. Anderen hebben het
gevoel dat het gaat om “programmeren op je hurken”. Onderzoek twee
inleidende practica in Logo. Geef aan welke doelen van programmeeronderwijs er wel en welke niet zijn te realiseren met deze practica, en plaats
Logo binnen de programmeerparadigma’s die in de cursus aan de orde zijn
geweest.
10. Simuleer een busrit. Uit de inleidende programmeercolleges van de
Nijmeegse universiteit (prof. Koster) is de opdracht afkomstig om op basis
van een aantal bouwstenen de simulatie te maken van een busrit. Van deze
opgave is inmiddels een Java-toepassing bekend. Gegeven zijn een aantal
objecten en hun methodes; te schrijven is de methode main om een en ander
aan elkaar te knopen. Presenteer je uitwerking en vertel iets over de voor- en
nadelen van een dergelijke practische opdracht.
11. Op contract werken. Gegeven is een programma waarmee een voorspelling
kan worden gedaan van de uitslag van de Amerikaanse presidentsverkiezingen. In tegenstelling tot opdracht 10 is hier het globale programma
reeds aanwezig, even als een aantal functies. Van de andere functies is alleen
de header en het commentaar bekend. Er is een contract geformuleerd waarin
staat aangegeven hoe je de betreffende functie moet laten werken en
samenwerken met de andere functies. Je moet deze functies bouwen om zo
het hele programma aan de praat te krijgen. Ook deze opdracht is naar Java
omgebouwd en daar beschikbaar.
12. Tentamens programmeren. Beschikbaar zijn een aantal tentamens die na
inleidend programmeeronderwijs aan studenten in het HBO zijn voorgelegd.
Behandel in je voordracht enkele vragen uit deze tentamens, vooral gericht op
de doelen van programmeeronderwijs in het voortgezet onderwijs. Zijn er ook
doelen die niet getoetst worden? Zo ja, geef daarvoor alternatieve
toetsmogelijkheden aan.
Download