MINI-SCHEME

In dit hoofdstuk worden de basisaspecten van Scheme syntaxis en semantiek besproken, en een minimaal aantal voorgedefinieerde procedures. In volgende hoofdstukken gaan we dieper in op bijkomende datatypes, voorgedefinieerde procedures, en aspecten van programmeerstijl.

Datatypes

Alles in SCHEME is een symbolische expressie. Wat volgt zijn zijn de belangrijkste types van expressies (datatypes). Soms worden symbolische expressies in twee subtypes onderverdeeld: atomen (getallen, booleaanse waarden en symbolen), en lijsten. Wanneer symbolische expressies beschouwd worden als een (deel van een) programma (de belichaming van een algoritme), noemen we ze forms, die geëvalueerd kunnen worden. In een lijst die de rol van form speelt, representeert het eerste element (de operand) de functie of procedureen de overige elementen representeren de argumenten van de procedure. Sommige functies zijn als primitieven aanwezig in het SCHEME systeem, andere worden door de programmeur gedefinieerd (zoals de functie kop-of-munt eerder). Wanneer een symbool de rol van form speelt is het een variabele die als waarde een willekeurige symbolische expressie kan hebben (inclusief een procedure). De getallen,  en #f en #t hebben als waarde steeds zichzelf.

SCHEME Syntaxis en Semantiek

Voor de interpretatie van Lisp forms maakt de evaluator (de SCHEME ``machine'' die reageert op wat na de prompt wordt ingevoerd) gebruik van de volgende evaluatieregels:
  1. De waarde van een symbool is zijn binding.
  2. De waarde van getallen en booleans is zichzelf.
  3. De waarde van een lijst is het resultaat van de toepassing van de procedure gebonden aan het eerste element van de lijst op de resultaten van de evaluatie van de overige elementen van de lijst (de argumenten).
Bijvoorbeeld, bij de evaluatie van (* (+ 8 1) 3) gaat de evaluator als volgt te werk: het gaat om een lijst met als eerste element de primitieve functie vermenigvuldiging (regel 3). We passen die operatie toe op het resultaat van de evaluatie van de argumenten. Het eerste argument is (+ 8 1). De evaluatie van deze lijst (regel 3) is de toepassing van de primitieve functie som op de evaluatie van de overige elementen van de lijst. Het eerste argument is het getal 8. De waarde van 8 is 8 (regel 2). Het tweede argument is het getal 1. De evaluatie van 1 levert 1 op (regel 2). De toepassing van som op 8 en 1 levert 9 op. Die waarde wordt doorgegeven als eerste argument van de vermenigvuldiging. Het tweede argument is het getal 3, waarvan de waarde 3 is (regel 2). Het uiteindelijke resultaat is dus 27.
> (* (+ 8 1) 3)
Pas + toe op waarde van 8 en waarde van 1 
Resultaat is 9         
Pas * toe op 9 en waarde van 3   
Resultaat is 27
27
Er zijn ook expressies die er uit zien als een functie, maar dat niet zijn. We noemen ze speciale vormen (special forms ) en ze zijn te herkennen aan bepaalde sleutelwoorden (keywords). Ze behoren tot de basissyntaxis van SCHEME en volgen de hogergenoemde evaluatieregels niet noodzakelijk. Daarom worden ze ook vaak syntax genoemd in plaats van special form.

De speciale vorm (QUOTE expressie) verhindert de evaluatie van de expressie die als argument aan QUOTE wordt gegeven. Een synoniem van (quote expressie) is ' expressie. Door al of niet gebruik te maken van de quote bepalen we of een lijst geïnterpreteerd wordt als data of als functie, en of een symbool geïnterpreteerd wordt als naam of als variabele-binding.

> (+ 1 2 3)
6
> (quote (+ 1 2 3)) 
(+ 1 2 3)
> '(+ 1 2 3)
(+ 1 2 3)
De programmeur kan zelf waarden toekennen aan symbolen. Dat gebeurt met de special form (DEFINE symbool expressie). Merk op dat de evaluatie van DEFINE afwijkt van de hogergenoemde evaluatieregel: het eerste argument wordt niet geëvalueerd.
> (define appel 243)

243
> appel
243
> 'appel
appel
> 'jan
jan
> jan
Error: Undefined variable "jan"
> (define lijst '(1 2 3 4))
(1 2 3 4)
> lijst
(1 2 3 4)
> (define lijst (+ 1 2 3))
6
> lijst
6

Voorgedefinieerde Procedures

Voor sommige datatypes bestaan voorgedefinieerde procedures. We bekijken hier de meest belangrijke.

Operaties op lijsten

Zowel voor de representatie van data als voor de representatie van programma's worden lijsten gebruikt. Lijsten uit elkaar halen en in elkaar zetten is dus meestal de meest uitgevoerde aktiviteit bij het ontwikkelen van SCHEME programma's. De belangrijkste primitieve functies om lijsten uiteen te halen zijn (CAR lijst) (retourneert het eerste element van zijn enige argument dat een lijst moet zijn) en (CDR lijst) (heeft ook een lijst als enige argument, en retourneert wat overblijft als het eerste element is weggenomen). Als het argument van CAR en CDR geen lijst is, de lege lijst is, of als meer dan één argument gegeven wordt, dan krijg je een foutmelding.
> (define zin '(jan eet een appel)) 
(JAN EET EEN APPEL) 
> (define onderwerp (car zin)) 
JAN 
> (define predikaat (cdr zin)) 
(EET EEN APPEL) 
> (define werkwoord (car predikaat)) 
EET 
> (define object (cdr predikaat)) 
(EEN APPEL) 
> onderwerp 
JAN 
> werkwoord 
EET 
> object 
(EEN APPEL) 
> (car onderwerp)
>>Error: Wrong type of arguments.
> (cdr predikaat onderwerp) 
>>Error: Wrong number of arguments.
> (car '()) 
>>Error: Wrong type of argument.
> (cdr '()) 
>>Error: Wrong type of argument.
Met CAR (uitgesproken "car" op z'n Engels) en CDR (uitgesproken "koeder") kunnen we tot om het even welke diepte doordringen in een lijst en er elementen of sublijsten uithalen. De resulterende combinatie van functies kan soms complex zijn. SCHEME heeft een uitgebreide verzameling functies die gebruikelijke combinaties van de functies CAR en CDR samenvatten: CAAR (de car van de car), CADR (de car van de cdr, het tweede element), CADADR (de car van de cdr van de car van de cdr). Elke combinatie van twee, drie of vier a's en d's tussen de c en de r is toegelaten.
> (define lidwoord (car (cdr (cdr zin))))
EEN
> (define lidwoord (caddr zin))
EEN
We willen ook graag lijsten in elkaar zetten. Dat kan met de functies (CONS expressie lijst), (LIST expressie*), en (APPEND expressie*).

 Bij de beschrijving van functies en de argumenten die ze toelaten houden we ons aan de volgende afspraken: expressie betekent dat een argument om het even welk Scheme datatype kan zijn. Wanneer alleen een meer specifiek datatype toegelaten is als argument, gebruiken we de naam ervan, bijv. lijst. Wanneer 1 of meer expressies of datatypes kunnen voorkomen gebruiken we het plusteken (+), wanneer er nul of meer kunnen voorkomen gebruiken we de Kleene Star (*). Optionele (niet noodzakelijke) argumenten worden aangeduid met vierkantehaken.

> (cons 1 '(2 3 4))
(1 2 3 4)
> (cons 'nooit object)
(NOOIT EEN APPEL)
> (cons 'onze (cons onderwerp nil))
(ONZE JAN)
> (list onderwerp werkwoord object)
(JAN EET (EEN APPEL))
> (append (list onderwerp) predikaat)
(JAN EET EEN APPEL)
> (append zin '(in de kerk))
(JAN EET EEN APPEL IN DE KERK)
> (list 'np object)
(NP (EEN APPEL))
> (list 'vp (list 'v werkwoord)(list 'np object))
(VP (V EET) (NP (EEN APPEL)))
> (list 's (list 'np onderwerp)
           (list 'vp (list 'v werkwoord)(list 'np object)))
(S (NP JAN) (VP (V EET) (NP (EEN APPEL))))

Operaties op getallen

Er zijn verschillende representaties en types van getallen in SCHEME. We zullen er in deze cursus slechts enkele van gebruiken. De wiskundige operaties *, -, +, /, MAX, MIN werken zoals je het intuïtief zou verwachten, met nul, 1 of meerdere getallen als argumenten. Zowel gehele en reële getallen als breuken kunnen door elkaar als argument meegegeven worden. SCHEME maakt zelf de aangewezen conversie om argumenten op elkaar af te stemmen.
(*)
1
> (* 1 2.0)
2.0
> (+ 3 3.0 3/3)
7.0
> (- 2)
-2
> (- 78 44)
34
> (/ 3)
0.3333
> (/ 25 5.0)
5.0
> (/ 2/3 3/4)
0.8888
> (max 2 3.0 15/2)
7.5
> (min 2 3.0 15/2)
2.0

Oefeningen

1. Gegeven: (define x '(1 (2 3) (4) (5 (6) 7) 8))

Geef de SCHEME expressie die uit de lijst gebonden aan symbool x de volgende expressies haalt.
Gebruik alleen CAR, CDR, en C....R

a. 3
b. 5
c. vijfde element
d. 7
e. voorlaatste element

2. Gegeven de lijst (1 (2 (3 4) 5))

a. Construeer deze lijst, alleen gebruik makend van cons, getallen en '()
b. Construeer deze lijst, alleen gebruik makend van list en getallen

3. Maak een expressie die 1 achteraan de lijst (4 3 2) toevoegt.

4. Maak een expressie die het gemiddelde van een reeks getallen (1 2 3 4 5) oplevert.
 
 

Condities en Predikaten

Een predikaat is een procedure die #f (ONWAAR) of #t (WAAR) oplevert. In SCHEME is al wat niet #f is #t. Predikaten zijn testen die uitgevoerd kunnen worden op SCHEME objecten. Predikaten kunnen gecombineerd worden met logische operatoren en worden gebruikt in conditionele expressies om het verloop van programmacontrole ( flow of control) te beïnvloeden. Predikaatnamen eindigen in SCHEME meestal op een vraagteken.

Gelijkheidspredikaten

Scheme kent een aantal predikaten om na te gaan of twee objecten gelijk zijn. Alle gelijkheidspredikaten hebben twee argumenten. De belangrijkste zijn de volgende.
> (= 1 1)
#t
> (equal? '(1 2 3) '(1 2 3))
#t
> (equal? '(cons 1 (cons (list 2 5) (cons 3 ()))) '(1 (2 5) 3))
#f
> (equal? (cons 1 (cons (list 2 5) (cons 3 ()))) '(1 (2 5) 3))
#t
Gebruik steeds = wanneer zekerheid bestaat dat de te vergelijken objecten getallen zijn.

Member

(MEMBER expressie lijst) heeft twee argumenten, en gaat na of het eerste argument een element is van het tweede argument. Als dat het geval is retourneert SCHEME de sublijst van het tweede argument vanaf de plaats waar het gezochte element de eerste keer voorkomt, anders #f. We hebben hier dus te maken met het idee dat wat niet #f is per definitie #t is.
> (member 'appel '(ik eet een appel met mosterd))
(APPEL MET MOSTERD)
> (member 'peer '(ik eet een appel met mosterd))
#f
Om te beslissen of een Lisp object gelijk is aan iets wat in de lijst zit moet natuurlijk gebruik worden gemaakt van een gelijkheidspredikaat. Het gelijkheidspredikaat dat MEMBER impliciet gebruikt is is EQUAL?.
> (member '(appel) '((peer)(appel)(pruim)))
((appel) (pruim))

Data-type predikaten

Voor de meeste types in LISP bestaat er een predikaat om na te gaan of een argument een bepaald type heeft:

(SYMBOL? expressie); (NUMBER? expressie); (LIST? expressie); (BOOLEAN? expressie). Er zijn er ook die testen of een argument een specifieke waarde heeft: (NULL? expressie) (lege lijst?), (ZERO? expressie) (nul?), (> getal getal), (< getal getal).

Logische operatoren

Er zijn drie Booleaanse operatoren (special forms), AND, OR en NOT die worden gebruikt om predikaten (testen) te combineren.
> (not 'aap)
#f
> (not (zero? 1))
#t
> (and (= 5 5) 'piet)
PIET
> (and (= 0 0.0) 'tafel (member 'appel '(peer pruim)))
#f
> (and (= 0 0.0) 'tafel (member 'appel '(peer appel pruim)))
(APPEL PRUIM)
> (or #f (zero? 3) 'piet)
PIET
> (or #f (zero? 3)(member 'appel '(peer pruim)))
#f
> (or)
#f
> (and)
#t

Conditionele expressie

IF is een veelgebruikte special form om de Flow of Control van een programma te sturen.
> (define x 0)
0
> (if (not (zero? x)) 'niet-nul 'nul)
NUL

Lambda Expressies en Proceduredefinitie

Stel dat we een functie willen definiëren die het derde element van een lijst retourneert in plaats van steeds
(caddr lijst)
te moeten schrijven. De volgende functiedefinitie zorgt hiervoor.
> (define third
       (lambda (lijst)
          (caddr lijst)))
THIRD
In dit geval kunnen we natuurlijk ook gewoon (define third caddr) schrijven, aangezien CADDR een voorgedefinieerde functie is.

Een Lambda expressie (LAMBDA (symbool*) expressie+) is een special form die evalueert tot een procedure, en bestaat uit een lijst met als eerste element het keyword LAMBDA, gevolgd door een lijst met de naam of namen van de parameter(s) van de functie (de parameter lijst). Parameters zijn variabelen (namen voor plaatsen in het geheugen waar een waarde opgeslagen kan worden). In het voorbeeld heeft de parameterlijst slechts één element: lijst. De overige argumenten van de lambda-expressie noemt men het lichaam (de body) van de functiedefinitie, deze bestaat uit een of meer Scheme expressies die achtereenvolgens worden geëvalueerd met de parameters gebonden aan de argumenten van de functieaanroep.

Net zoals met DEFINE symbolen kunnen worden gebonden aan een waarde, kan deze special form ook gebruikt worden om een procedure, gedefinieerd in een lambda expressie, te binden aan een symbool. Het eerste argument van DEFINE is een symbool, de naam van de procedure (in ons voorbeeld THIRD), en het tweede argument is de lambda expressie die de definitie bevat. De waarde die het Scheme systeem retourneert na een aanroep van DEFINE is het eerste argument (de functienaam). Belangrijker is natuurlijk het neveneffect: vanaf nu kent LISP de nieuwe functie en kunnen we ze gaan toepassen op argumenten.

> (third '(1 2 3 4)) 
3 
> (third (cons 'appels '(list 'peren 'pruimen 'bananen)))
PRUIMEN
Bij deze laatste functie-aanroep gebeurt het volgende: het resultaat van de evaluatie van het argument van third (de lijst (appels peren pruimen bananen)) wordt gebonden aan de parameter lijst. Met deze binding wordt (caddr lijst))) geëvalueerd, wat PRUIMEN oplevert. Aangezien de body van de definitie alleen uit die expressie bestaat is het resultaat ervan meteen ook het resultaat van de hele functieaanroep. Tenslotte wordt de binding van lijst ongedaan gemaakt. Als er in een functiedefinitie meer dan één parameter is, moet met elk van deze parameters een argument corresponderen wanneer de functie uitgevoerd wordt, zoniet volgt er een foutmelding. Het eerste argument wordt daarbij gebonden aan de eerste parameter, het tweede aan de tweede enz. Het is ook mogelijk dat er geen parameter is, zoals in de volgende, nogal stompzinnige functiedefinitie. Dat was ook het geval bij de functies S, NP, etc. in de inleiding.
 
> (define foo (lambda () 'hello_world!))
FOO
> (foo)
HELLO_WORLD!
Naast de syntaxis (DEFINE symbool expressie) voor variabelebinding zorgt (DEFINE symbool lambda-expressie) dus voor de associatie van een procedure met een naam.

Deze syntaxis wijkt af van die van functiedefinitie zoals gebruikt in de voorbeelden van de inleiding. Er bestaat inderdaad een alternatieve syntaxis van functiedefinitie die voor sommigen duidelijker is. We zullen beide syntaxen gebruiken:

(DEFINE (functienaam parameternaam*) expressie+)

De volgende twee functiedefinities zijn dus volledig equivalent.

(define (foo lis1 lis2)
  (length (append lis1 lis2)))

(define foo
  (lambda (lis1 lis2)
    (length (append lis1 lis2))))

Input en Output

(READ). (Geen argument). Leest een Lisp expressie van de standaard input (het Lisp interactie systeem). ``Lezen'' betekent dat een externe representatie van een Lisp object wordt omgezet in dat object zelf.

(DISPLAY expressie). Schrijft een representatie van expressie naar de standaard output (Lisp interactiesysteem; listener).

(NEWLINE). Stuurt een end-of-line naar standaard output (nieuwe regel).
 

Oefeningen


1. Definieer een procedure OPPERVLAKTE die de oppervlakte van een functie geeft, gegeven de straal.
> (oppervlakte 12)
452.389342176

2. Definieer een procedure KRUISING die twee pare kruist:
> (kruising '(a b) '(1 2))
((a 2) (1 b))

3. Definieer een predikaat KLINKER? dat #t geeft als het argument een klinker is.

4. Definieer een procedure VOEGTOE die een element toevoegt aan een lijst wanneer dat element nog niet voorkomt in de lijst.

> (voegtoe 'a '(b c d))
(a b c d)
> (voegtoe 'c '(b c d))
(b c d)


Walter Daelemans <Walter.Daelemans@kub.nl>

Last modified: Tue Mar 18 14:35:58 1997