[Informatik-Seminar WS2004/05] [Inhalt] [Zurück] [Weiter]


3. Erläuterungen zur Syntax und Semantik von Scheme

Dieses Kapitel befasst sich nun genauer mit der eigentlichen Sprache. Es werden die Sprachelemente, die Syntax, die Funktionsweise und einige typische Eigenschaften erklärt.

Der Typ Liste wird im folgenden Kapitel erläutert, weil dieser in Scheme (bzw. LISP) eine herausragende Rolle spielt.

3.1 Kommentare

3.2 Arbeiten mit Datentypen

3.2.1 Wahrheitswerte

3.2.2 Zahlen

3.2.3 Zeichen

3.2.4 Symbole (Variablen)

3.2.5 Kombinierte Datentypen

3.2.6 Strings

3.2.7 Vektoren

3.3 Konvertieren zwischen Datentypen

3.4 Prozeduren und Funktionen

3.5 Lokale Symbole

3.6 Bedingungen

3.7 Funktionen als Datentyp

3.1 Kommentare

Kommentare werden durch ein Semikolon eingeleitet. Alle folgenden Zeichen in dieser Zeile werden ignoriert.

; Dies ist ein Kommentar

3.2 Arbeiten mit Datentypen

In Scheme gibt es einfache und kombinierte Datentypen. Ausdrücke, die auf einfachen Datentypen basieren, werten sich selber aus. Kombinierte Datentypen entstehen durch das Kombinieren von einfachen Datentypen.

3.2.1 Wahrheitswerte

Scheme verwendet für die Darstellung von false und true folgende Ausdrücke:

#f ; false
#t ; true

Eine Negation ist mit der Funktion not möglich:

(not #f) => #t
(not #t) => #f

Soll ein Ausdruck als Wahrheitswert ausgewertet werden, wertet Scheme alle Ausdrücke, außer #f, als #t aus.

(not "Hallo") => #f

Wahrheitswerte können mit and und or verknüpft werden. Die Funktionen lassen mehrere Argumente zu. Diese werden von links nach rechts ausgewertet. Sobald das Ergebnis feststeht, weil and einen Ausdruck als #f oder or einen Ausdruck als #t ausgewertet hat, brechen die Funktionen ab und liefern das Ergebnis.

(and #t #t) => #t
(and #t #f) => #f
(or  #f #f #f #f #f #t) => #t

(and #f UndefinerterBezeichner)
  => #f, weil der unbekannte Bezeichner nicht ausgewertet wird
(and #t UndefinerterBezeichner)
  => Fehler, weil ein unbekannter Bezeichner nicht ausgewertet werden kann.

Ob ein Ausdruck ein Wahrheitswert ist, lässt sich mit der Funktion boolean? testen.

(boolean? #f) => true
(boolean? "Hallo") => false

3.2.2 Zahlen

Es gibt, wie in der Mathematik natürliche (z.B. 23), rationale (z.B. 23/42), reelle (z.B. 23.23) und komplexe (23+42i) Zahlen. Auch hierfür gibt es Funktionen, die testen, ob ein Ausdruck dem Typ entspricht.

(integer? 42)    => #t
(integer? 23/42) => #f
(rational? 1/4)  => #t
(rational? 0.25) => #t
(rational? 2+3i) => #f
(real? 1.23)     => #t
(real? 2+3i)     => #f
(complex? 2+3i)  => t

Für natürliche Zahlen kann auch die binäre, oktale oder hexadezimale Schreibweise verwendet werden. Dies geschieht mit Hilfe von Präfixen. Die Angabe des dezimalen Präfixes ist optional:

#b11 ;binär       => 3
#o11 ;oktal       => 9
#x11 ;hexadezimal => 17
#d11 ;dezimal     => 11

Um Zahlen zu vergleichen sind folgenden fünf Funktionen verfügbar:

(= 23 42)  => #f
(= 23 23)  => #t
(< 23 42)  => #t
(<= 23 42) => #t
(> 23 42)  => #f
(>= 23 42) => #f

In Scheme sind eine Reihe von mathematischen Funktionen definiert. Hier ein paar Beispiele:

(+ 1 2 3)   => 6
(- 42 2)    => 40
(- 20 10 5) => 5
(* 2 3)     => 6
(/ 10 2)    => 5
(expt 2 10) => 1024

(max 1 2 3) => 3
(min 1 2 3) => 1
(abs -42)   => 42

3.2.3 Zeichen

Ein Zeichen wird durch das Präfix #\ dargestellt. #\a ist also der Buchstabe a. Außerdem gibt es nicht grafische Zeichen, wie z.B. #\newline, #\tab oder #\space.

Hier gibt es ebenfalls eine Testfunktion:

(char? #\a) => #t
(char? b)   => #f

3.2.4 Symbole (Variablen)

Einfache Datentype werten sich selber aus. Anders ist dies bei Symbolen, die ähnlich wie eine Variable funktionieren. Diese stehen für einen Wert, der dem Symbol zugewiesen wurde. Ein Symbol wird durch seinen Namen beschrieben. Dieser darf aus Buchstaben, Zahlen und allen anderen Zeichen bestehen. Ein einfaches Anführungszeichen (') ist nicht erlaubt. Symbole werde nicht nach Groß- und Kleinschreibung unterschieden.

Mit dem einfachen Anführungszeichen wird eine Auswertung verhindert. Diese Schreibwiese ist die Kurzform der Funktion quote. Diese gibt das Symbol selbst aus.

'abc => abc
(quote abc) => abc

Mit folgender Funktion lässt sich testen, ob das Argument ein Symbol ist:

(symbol? 'abc) => #t
(symbol? 42) => #f

Mit der allgemein verwendbaren Funktion eqv? lassen sich auch Symbole auf Gleichheit testen. Die Funktion ist auch für alle andere Datentypen verwendbar.

(eqv 'abc 'ABC) => #t
(eqv 'abc 'xyz) => #f

Symbole können als globale Variable verwendet werden. Zu beachten ist, dass in der funktionalen Programmierung eine erneute Zuweisung eines Wertes verboten ist. Dazu wird dem Symbol mit der Funktion define ein Wert zugewiesen.

(define x 23)

Hier hat das Symbol x den Wert 23 erhalten. Soll nun dieses ausgewertet werden, liefert es den Wert 23.

x => 23

3.2.5 Kombinierte Datentypen

Kombinierte Datentypen entstehen durch die Kombination von Datentypen. Dazu gehören Strings, Vektoren und Listen.

3.2.6 Strings

Strings entstehen durch die Kombination von Zeichen. Dies kann auf zwei Arten geschehen. Entweder durch die Angabe eines Strings in doppelten Anführungszeichen oder durch das Verwenden der string Funktion, die einen String konstruiert.

"Hallo Welt" => "Hallo Welt"
(string #\H #\a #\l #\l #\o #\space #\W #\e #\l #\t) => "Hallo Welt"

Mit der Funktion string? lässt sich ein Ausdruck auf den Typ String testen.

(string? "Hallo") => #t

Mehrere Strings können mit der Funktion string-append zusammengefügt werden.

(string-append "Hallo" (string #\space) "Welt") => "Hallo Welt"

3.2.7 Vektoren

Vektoren sind eindimensionale Listen. Diese können alle Arten von Datentypen aufnehmen. Dies schließt auch Vektoren selbst ein.

Vektoren werden mit der Funktion vector erzeugt. Scheme stellt einen Vektor durch die Angabe der Anzahl der enthaltenen Elemente und durch die Elemente, welche von Klammern umgeben sind, dar.

(vector 1 2 3) => #3(1 2 3)

Auch hier gibt es wieder eine Funktion, um auf diesen Typ zu testen: vector?

(vector? #1(1)) => #t

(define v1 (vector 1 2 3))
(vector? v1) => #t

3.3 Konvertieren zwischen Datentypen

Scheme liefert eine Vielzahl von Funktion, um Daten von einem Typ in einen anderen umzuwandeln. Die Namen der Funktionen setzten sich aus dem Quelltyp, den Zeichen -> und dem Zieltyp zusammen. Ist eine Konvertierung nicht möglich, liefert die Funktion #f.

Folgende Konvertierungen sind möglich:

Bei der Umwandlung von char nach interger wird der ASCII Code  verwendet.

(char->integer #\a)      => 97
(string->list "hallo")   => (#\h #\a #\l #\l #\o)
(number->string 42)      => "42"
(symbol->string 'Symbol) => "Symbol"

Die Funktionen number->string und string->number können ein optionales zweites Argument verarbeiten: Die Basis des Zahlensystems. Die Funktionen wandeln dann die Darstellung entsprechend um.

(number->string 11 10) => "11"
(number->string 11 2)  => "1011"
(number->string 11 16) => "b"

(string->number "1011" 2) => 11
(string->number "b" 16)   => 11

3.4 Prozeduren und Funktionen

Eine Prozedur oder Funktion wird mittels define und lambda definiert. Hinter define folgt, wie üblich, der Name. Anschließend werden durch lambda die Funktionsargumente und der Funktionsrumpf definiert.

(define Funktionsname
  (lambda (Funktionsargument1 Arg2) (Funktionsrumpf) ))

Dieses Beispiel addiert zu einer Zahl 23:

(define add23
  (lambda (x) (+ x 23)))

(add23 5) => 28

Dies lässt sich auch mittels define verkürzt so schreiben:

(define (add23 x) (+ x 23))

3.5 Lokale Symbole

Als lokale Symbole sind bereits die lamba-Parameter bekannt. Diese haben als Namensraum den Funktionsrumpf. Zusätzlich können lokale Symbole mittels let deklariert werden. Dazu werden erst die lokalen Symbole angegeben. Anschließend folgt der Code, in dem diese Symbole dann definiert sind.

Hierzu ein kleines Beispiel:

(define (daneben x)
  (let ( (richtig 42) ) 
    (- richtig x) ))

(daneben 40) => 2

3.6 Bedingungen

In Scheme gibt es mehrere Möglichkeiten Bedingungen zu formulieren. Schleifen sind aber nicht möglich, diese würde dem Prinzip der funktionalen Programmierung widersprechen.

Die einfachste Form ist das if. Die Funktion if wertet zuerst das erste Argument aus. Ist dies nicht #f wird das zweite Argument ausgewertet und als Ergebnis zurückgegeben. Das dritte Argument ist der optionale else Teil.

(define x 42)
(if  (= x 42) "richtig" "falsch" )
=> "richtig"

(define x 42)
(if (= x 42) "richtig")
=> "richtig"

Anstatt mehrere if-else Bedingungen zu schachteln, kann die Funktion cond verwendet werden. Die Argumente müssen jeweils eine Bedingung und einen Ausdruck, der ausgewertet werden soll, wenn die Bedingung #t ist, enthalten. Als letztes Argument ist ein else Teil möglich, der ausgewertet wird, wenn keine Bedingung #t lieferte.

(define x 44)

(cond ((< x 42) "kleiner")
      ((> x 42) "größer")
      (else     "richtig"))
=> "größer"

Soll der gleiche Ausdruck mit mehreren Werten auf Gleichheit geprüft werden, bietet sich die case Funktion an. Diese vergleicht das erste Argument mit den folgenden Ausdrücken. Sobald ein Ausdruck gleich dem ersten Argument ist, wird der zugehörige Ausdruck ausgewertet und als Ergebnis zurückgegeben.

(define x 41)

(case x
  ((42)    "richtig")
  ((41 43) "knapp falsch")
  (else    "falsch")
)

=> "knapp falsch"

Damit dies, wie hier beschrieben, funktioniert, müssen diese Funktionen von Scheme besonders behandelt werden. Normalerweise werden Argumente erst ausgewertet und dann an die aufgerufene Funktion übergeben. Hier werden die Argumente natürlich nur bei Bedarf ausgewertet.

3.7 Funktionen als Datentyp

In funktionalen Programmiersprachen ist es besonders wichtig, dass Funktionen auch Parameter und Ergebnisse sein können.

Funktionen können in Scheme, wie jeder andere Datentyp, Parameter sein. Dazu wird einfach der Funktionsname übergeben. Hierzu dieses kurze Beispiel:

(define (DoIt fkt arg1 arg2)
  (fkt arg1 arg2))

(DoIt + 1 2) => 3

Es ist ebenfalls problemlos möglich, Funktionen als Ergebnis zurückzugeben. Dazu wird einfach der Name der Funktion angegeben.

Dazu wieder ein kleines Beispiel:

(define (sub1 x)
  (- x 1))

(define (add1 x)
  (+ x 1))

; Hier wird zurückgegeben, welche Funktion ausgeführt werden muss, 
; um der 42 näher zu kommen.
(define (WhatToDo x) 
  (if (< x 42)
    add1
    sub1))

; Dies liefert den Funktionsnamen zurück
(WhatToDo 10) => #<procedure:add1>

; Hier wird die Funktion auch ausgeführt
((WhatToDo 10) 10) => 11


[Informatik-Seminar WS2004/05] [Inhalt] [Zurück] [Weiter] [Seitenanfang]