eXamen.press
eXamen.press ist eine Reihe, die Theorie und Praxis aus allen Bereichen der Informatik für die Hochschulausbildung vermittelt.
Wolfram-Manfred Lippe
Funktionale und Applikative Programmierung Grundlagen, Sprachen, Implementierungstechniken
Prof. Dr. Wolfram-Manfred Lippe Universität Münster Institut für Informatik Einsteinstr. 62 48149 Münster
[email protected]
ISBN 978-3-540-89091-1
e-ISBN 978-3-540-89108-6
DOI 10.1007/978-3-540-89108-6 eXamen.press ISSN 1614-5216 Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. Library of Congress Control Number: 2008940420 c 2009 Springer-Verlag Berlin Heidelberg Dieses Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere die der Übersetzung, des Nachdrucks, des Vortrags, der Entnahme von Abbildungen und Tabellen, der Funksendung, der Mikroverfilmung oder der Vervielfältigung auf anderen Wegen und der Speicherung in Datenverarbeitungsanlagen, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Eine Vervielfältigung dieses Werkes oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzen der gesetzlichen Bestimmungen des Urheberrechtsgesetzes der Bundesrepublik Deutschland vom 9. September 1965 in der jeweils geltenden Fassung zulässig. Sie ist grundsätzlich vergütungspflichtig. Zuwiderhandlungen unterliegen den Strafbestimmungen des Urheberrechtsgesetzes. Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften. Herstellung: VTEX typesetting and electronic publishing services, Vilnius Einbandgestaltung: KünkelLopka, Heidelberg Gedruckt auf säurefreiem Papier 987654321 springer.com
Vorwort
In den vergangenen Jahren hat die applikative Programmierung, die auch oft als funktionale Programmierung bezeichnet wird, in der Forschung eine schwunghafte Entwicklung genommen. Nachdem in der industriellen Praxis die applikative Programmiersprache LISP sich bereits seit vielen Jahren einen festen Platz erobert hat, ist zu erwarten, daß auch die neuen Entwicklungen im Bereich der funktionalen und applikativen Programmierung auf zunehmendes Interesse stoßen werden. Das Hauptanliegen dieses Buches ist eine leicht verst¨andliche Einf¨ uhrung in die vielschichtige Thematik. Sie reicht von theoretischen Hilfsmitteln wie dem λ-Kalk¨ ul u ¨ber Implementierungsfragen bis hin zu neuen Rechnerarchitekturen. Angesichts der Stoff¨ ulle mußte eine Auswahl getroffen werden und mußten Schwerpunkte gesetzt werden. Als Zugang zur Thematik und als Bindeglied zwischen den einzelnen Kapiteln wurde der ungetypte λ-Kalk¨ ul von Church mit der klassischen Reduktionssemantik bzw. die kombinatorische Logik gew¨ ahlt. Im Hauptteil geht es um die Vorstellung funktionaler und applikativer Programmiersprachen, um Beispiele f¨ ur diesen Programmierstil und um effiziente Implementierungstechniken. Die etwas st¨arkere Fokussierung auf die nicht mehr ganz aktuelle Sprache LISP ist darin begr¨ undet, daß mit PureLISP eine kompakte Kernsprache zur Verf¨ ugung steht, die ohne zus¨atzlichen Ballast“ eine einfache didaktische Einf¨ uhrung in die verschiedenen Problem” bereiche erlaubt. Im abschließenden Teil wird dargelegt, wie sich derartige Programmiersprachen zuk¨ unftig auf die Entwicklung neuer Rechner auswirken k¨ onnen, die nicht mehr auf der von Neumann-Architektur beruhen. Bei dieser Abgrenzung des Stoffes werden im Vertrauen auf andere vorhandene bzw. noch erscheinende Lehrb¨ ucher interessante Themen, die nicht nur speziell die funktionale und applikative Programmierung betreffen, nur in geringerem Umfang ber¨ ucksichtigt, so z.B. die Fixpunkttheorie und die Theorie der abstrakten Datentypen. Die Darstellung des aktuellen Standes der rasant voranschreitenden Forschung ist Tagungsb¨ anden vorbehalten. Die regelm¨aßig von der amerikanischen Gesellschaft ACM veranstalteten Tagungen u ¨ber Func” ¨ tional Programming“ geben einen guten Uberblick.
VI
Vorwort
Das Material dieses Buches beruht auf Vorlesungen und begleitenden ¨ Ubungen, die in Kiel, M¨ unchen und M¨ unster gehalten wurden. Bei der Ausarbeitung der zugrunde liegenden Vorlesungsskripten wurde auf die Darstellung des Umfangs der einzelnen Vorlesungen und der Aufteilung in ein zweist¨ undiges Zeitraster zugunsten einer Gliederung nach inhaltlichen Gesichtspunkten verzichtet. Wenn man ein solches Buch schreibt, macht man Fehler. Sollten Sie als Leser Fehler in diesem Buch finden, so w¨ urde ich mich freuen, wenn Sie mir dies mitteilen w¨ urden, damit diese Fehler in folgenden Auflagen korrigiert werden k¨ onnen. Der Autor m¨ochte den vielen Personen, die dieses Vorhaben unterst¨ utzt haben, ihren Dank sagen, insbesondere D. Ackermann, W. Dosch, E. Fehr, K.-U. Felgentreu, Wolfgang Goerigk, D. Hillen, B. Kalhoff, W. Kluge, M. Krause, E. Meyer, Th. Feuring, D. Lammers und F. Simon die Teile des Manuskripts gelesen und wertvolle Hinweise gegeben haben. Frau I. Berg, Frau M.-L. Giesa und Frau M. Gentes gilt besonderer Dank f¨ ur die hervorragende Arbeit beim Schreiben der Druckvorlagen und ihre Geduld beim Einf¨ ugen der Verbesserungen und Erweiterungen, ebenso Herrn S. Isik f¨ ur die Erstellung ¨ der Endkorrektur und die Uberarbeitung der Abbildungen.
M¨ unster, Mai 2008
W.-M. Lippe
Inhaltsverzeichnis
1
Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
2
Mathematische Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 Berechenbare Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.2 Primitiv-rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . 2.1.3 Primitiv-rekursive Pr¨ adikate . . . . . . . . . . . . . . . . . . . . . . . . 2.1.4 Partiell rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . 2.1.5 Vergleich der betrachteten Klassen von Funktionen . . . . 2.1.6 Effektive Bereiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Der λ-Kalk¨ ul . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.2 Der klassische λ-Kalk¨ ul . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.2.1 Elementare Begriffe . . . . . . . . . . . . . . . . . . . . . . . 2.2.2.2 Reduktionsregeln des λ-Kalk¨ uls . . . . . . . . . . . . . 2.2.2.3 Extensionale Gleichheit von Funktionen . . . . . . 2.2.2.4 Die Church-Rosser Eigenschaft . . . . . . . . . . . . . 2.2.3 Wahrheitswerte und logische Verkn¨ upungen . . . . . . . . . . . 2.2.4 Arithmetik und λ-Definierbarkeit . . . . . . . . . . . . . . . . . . . . 2.2.5 Terme mit undefinierter Bedeutung . . . . . . . . . . . . . . . . . . 2.2.6 Fixpunkte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.7 Reduktionsstrategien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.8 Angewandter λ-Kalk¨ ul . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.9 Typsysteme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.9.1 Getypter λ-Kalk¨ ul . . . . . . . . . . . . . . . . . . . . . . . . 2.2.9.2 Abstrakte Datentypen . . . . . . . . . . . . . . . . . . . . 2.2.9.3 Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Kombinatorische Logik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.2 Elementare Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9 9 9 11 14 16 18 19 23 23 25 26 31 32 34 39 40 45 48 50 53 54 55 57 60 62 62 62
VIII
Inhaltsverzeichnis
2.3.3 Die Beziehung zum λ-Kalk¨ ul . . . . . . . . . . . . . . . . . . . . . . . . 68 2.3.4 Anwendungen der kombinatorischen Logik . . . . . . . . . . . . 69 3
Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 3.1 FP-systeme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 3.1.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 3.1.2 Schemasprache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 3.1.3 Ein FP-System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 3.1.4 Beispiele f¨ ur FP-Programme . . . . . . . . . . . . . . . . . . . . . . . . 79 3.1.5 Die Algebra der FP-Programme . . . . . . . . . . . . . . . . . . . . . 86 3.1.6 FFP-Systeme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 3.1.7 Beispiele f¨ ur FFP-Programme . . . . . . . . . . . . . . . . . . . . . . . 94 3.1.8 FP-Programme als Kombinatoren . . . . . . . . . . . . . . . . . . . 97 3.2 LISP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 3.2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 3.2.2 Pure-LISP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 3.2.2.1 Daten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 3.2.2.2 Basisfunktionen zur Verarbeitung von S-Ausdr¨ ucken . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 3.2.2.3 Vereinfachte Darstellung aufeinanderfolgender car‘s und cdr‘s . . . . . . . . . 112 3.2.2.4 Basispr¨ adikate f¨ ur S-Ausdr¨ ucke . . . . . . . . . . . . . 112 3.2.2.5 Bedingte Ausdr¨ ucke . . . . . . . . . . . . . . . . . . . . . . . 113 3.2.2.6 Abstraktionen und Applikationen . . . . . . . . . . . 113 3.2.2.7 Rekursive Lambda-Ausdr¨ ucke . . . . . . . . . . . . . . 114 3.2.2.8 Funktionale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 3.2.2.9 Zusammenfassung der Syntax . . . . . . . . . . . . . . 115 ¨ 3.2.2.10 Ubersetzung von Programmen der M-Sprache in S-Ausdr¨ ucke . . . . . . . . . . . . . . . . . 116 3.2.2.11 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 3.2.2.12 Der Interpretierer . . . . . . . . . . . . . . . . . . . . . . . . . 121 3.2.2.13 Interpretation eines Beispiels . . . . . . . . . . . . . . . 124 3.2.3 LISP-Programmiersysteme . . . . . . . . . . . . . . . . . . . . . . . . . 126 3.2.3.1 Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . 127 3.2.3.2 Pseudofunktionen . . . . . . . . . . . . . . . . . . . . . . . . . 131 3.2.3.3 Standardfunktionen . . . . . . . . . . . . . . . . . . . . . . . 133 3.2.3.4 Konzeptionelle Erweiterungen . . . . . . . . . . . . . . 134 3.2.3.5 Die INTERLISP-Programmierumgebung . . . . . 137 3.2.4 Kuriosit¨ aten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 3.2.5 Die Beziehung zum λ-Kalk¨ ul . . . . . . . . . . . . . . . . . . . . . . . . 143 3.2.5.1 LISP als angewandter λ-Kalk¨ ul . . . . . . . . . . . . . 143 3.2.5.2 Der Interpretierer eval1 . . . . . . . . . . . . . . . . . . . . 144 3.2.5.3 Der Interpretierer eval2 . . . . . . . . . . . . . . . . . . . . 146 3.2.5.4 Statische und dynamische Bindung von Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Inhaltsverzeichnis
IX
3.2.5.5 Der Interpretierer eval3 . . . . . . . . . . . . . . . . . . . . 149 3.2.5.6 Der Interpretierer eval4 . . . . . . . . . . . . . . . . . . . . 151 3.3 Weitere Applikative Programmiersprachen . . . . . . . . . . . . . . . . . . 154 3.3.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 3.3.2 SASL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 3.3.3 KRC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160 3.3.4 EFPL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 3.3.5 BRL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 3.3.5.1 Sprachbeschreibung . . . . . . . . . . . . . . . . . . . . . . . 172 3.3.5.2 Ein Programm zur Unifikation von Termen . . . 179 3.3.6 Scheme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 3.3.6.1 Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 3.3.6.2 Globale Definitionen . . . . . . . . . . . . . . . . . . . . . . . 188 3.3.6.3 Lokale Deklarationen . . . . . . . . . . . . . . . . . . . . . . 189 3.3.6.4 Prozeduren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 3.3.6.5 Fallunterscheidungen . . . . . . . . . . . . . . . . . . . . . . 194 3.3.6.6 Rekursionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 3.3.6.7 Programmierbeispiele . . . . . . . . . . . . . . . . . . . . . . 197 3.3.6.8 Scheme-Systeme . . . . . . . . . . . . . . . . . . . . . . . . . . 200 3.3.7 Miranda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 3.3.8 Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 3.3.8.1 Sprachkonzepte . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 3.3.8.2 Das Typsystem . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 3.3.8.3 Beispielprogramme . . . . . . . . . . . . . . . . . . . . . . . . 221 3.3.9 ML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 3.3.9.1 Entwicklung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 3.3.9.2 Sprachelemente . . . . . . . . . . . . . . . . . . . . . . . . . . . 223 3.3.10 Hope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 3.3.11 Curry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234 3.3.11.1 Einf¨ uhrung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234 3.3.11.2 Sprachkonzepte . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 3.3.11.3 Ein Programmbeispiel . . . . . . . . . . . . . . . . . . . . . 237 3.3.12 Weitere Sprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238 3.3.12.1 ASpecT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238 3.3.12.2 Caml . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 3.3.12.3 Cayenne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 3.3.12.4 CELP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 3.3.12.5 Clean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 3.3.12.6 Eden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 3.3.12.7 Erlang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240 3.3.12.8 Escher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 3.3.12.9 FALCON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 3.3.12.10 Goffin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242 3.3.12.11 λ-Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 3.3.12.12 Lλ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
X
Inhaltsverzeichnis
3.3.12.13 3.3.12.14 3.3.12.15 3.3.12.16 3.3.12.17 4
Leda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 Mercury . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244 Oz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 Scala . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 TyPiCal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
Implementierungstechniken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 4.1 Interpretierer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 4.1.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 4.1.2 Shallow-Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 4.1.3 Optimierung von einfachen Postrekursionen . . . . . . . . . . . 257 4.1.4 Optimierung von verdeckten Postrekursionen . . . . . . . . . 260 ¨ 4.2 Ubersetzer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 4.2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 4.2.2 Ein Laufzeitsystem mit kellerartiger Speicherplatzverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 4.2.3 Optimierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272 4.3 Hardware – Unterst¨ utzte Implementierungen . . . . . . . . . . . . . . . . 278 4.3.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278 4.3.1.1 Reduktionsmaschinen . . . . . . . . . . . . . . . . . . . . . . 279 4.3.1.2 Datenflußmaschinen . . . . . . . . . . . . . . . . . . . . . . . 283 4.3.2 Die GMD-Reduktionsmaschine (Berkling-Maschine) . . . 291 4.3.2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 4.3.2.2 Der interne Aufbau der GMD-Maschine . . . . . . 293 4.3.2.3 Kooperierende Reduktionsmaschinen . . . . . . . . 303 4.3.3 Die S-K-I-Graph-Reduktionsmaschine von Turner . . . . . 307 4.3.3.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307 ¨ 4.3.3.2 Ubersetzung von SASL-Programmen . . . . . . . . 308 4.3.3.3 Der Graph Reduktionsmechanismus . . . . . . . . . 313 4.3.4 Die Manchester-Datenflußmaschine . . . . . . . . . . . . . . . . . . 324 4.3.4.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324 4.3.4.2 Rechnerstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 Sachverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
1 Einleitung
Unabh¨ angig von konkreten Programmiersprachen l¨aßt sich das methodische Vorgehen beim Programmieren nach verschiedenen Programmierstilen klassifizieren. Am weitesten verbreitet ist das prozedurale (von lat. procedere) Programmieren. Seine geschichtliche Entwicklung erstreckt sich von den ersten h¨oheren Programmiersprachen, wie FORTRAN und ALGOL-60 u ¨ber COBOL und PASCAL bis zu C“. Man formuliert auf abstraktem Niveau eine Folge von ” Maschinenbefehlen, die der Rechner nacheinander ausf¨ uhren soll. Daneben wurden eine Reihe von anderer Programmierstile entwickelt: Beim applikativen bzw. funktionalen Stil ist die Funktionsanwendung das beherrschende Sprachelement. Bereits fr¨ uh entstand als erster Vertreter dieses Programmierstils die Sprache LISP. Sie hat inzwischen eine Reihe von moderneren Nachfolgesprachen gefunden. Dieser Programmstil hat sich insbesondere im Bereich der symbolischen Informationsverarbeitung und der K¨ unstlichen Intelligenz (KI) durchgesetzt und findet zunehmend auch in der industriellen Praxis Verwendung. Beim pr¨ adikativen (logischen) Stil ist das Formulieren von pr¨adikatenlogischen Formeln das beherrschende Sprachelement. Er wird ebenfalls im Bereich der K¨ unstlichen Intelligenz“ vor allem bei der Entwicklung von ” Expertensystemen, eingesetzt. Die bekannteste Sprache ist hierbei PROLOG. Ebenfalls in diesem Bereich ist der objektorientierte Programmierstil entstanden, bei dem die Definition von Objekten mit F¨ahigkeiten zum Senden und Empfangen von Nachrichten im Vordergrund steht. Der bekannteste Vertreter ist die Sprache JAVA. Viele Programmiersprachen unterst¨ utzen nur einen Programmierstil, z.B. FORTRAN den prozeduralen oder PROLOG den pr¨adikativen. Von zunehmender Bedeutung sind aber auch Sprachen, die die Benutzung mehrerer Stile gestatten. In PASCAL programmiert man zwar u ¨blicherweise prozedural; man
2
1 Einleitung
kann aber auch den applikative Programmierstil einsetzen. In der Sprache LISP programmiert man u ¨berwiegend applikativ; es stehen aber auch der objektorientierte und der prozedurale Programmierstil zur Verf¨ ugung. Die Integration des pr¨adikativen Stils bereitet prinzipielle Probleme und ist immer noch aktueller Gegenstand der Forschung. Eine vergleichende Wertung verschiedener Programmierstile wird im Rahmen dieses Buches nicht erfolgen. Im Vordergrund steht vielmehr die Vermittlung der Ideen des funktionalen und applikativen Programmierens, ihre programmiersprachlichen Auspr¨ agungen, kleinere Anwendungsbeispiele und ein Einblick in spezielle Implementierungstechniken f¨ ur deratige Sprachen. Die Verbindung zwischen den einzelnen Abschnitten erfolgt u ¨ber die Theorie der rekursiven Funktionen, den ungetypten λ-Kalk¨ ul und die kombinatorische Logik. Wenden wir uns nun einer Kl¨ arung und Abgrenzung der Begriffe appli” katives“ und funktionales“ Programmieren zu: ” Generell kann man Programme als Funktionen im mathematischen Sinne deuten, durch die Eingabedaten in Ausgabedaten abgebildet werden. Bei den prozeduralen Sprachen, die ganz entscheidend durch die von NeumannRechnerarchitektur gepr¨ agt sind, lassen sich die einzelnen Konstrukte eines Programms selbst jedoch nicht als Funktionen u ¨ber einfachen Bereichen deuten. Ihre Bedeutung ist von dem Begriff der Adresse eines Speicherplatzes abh¨ angig und von der Vorstellung einer sequentiellen Programmausf¨ uhrung gepr¨ agt. Zu einer formalen Behandlung ben¨ otigt man eine aufwendigere mathematische Begriffsbildung. Betrachtet man zwei Funktionen f, g, die ganze Zahlen in ganze Zahlen abbilden, d.h. f, g : Z → Z, so gilt das Kommutativgesetz f (a) + g(a) = g(a) + f (a). Wegen der Seiteneffekte, die durch die Wertzuweisungen bewirkt werden, gilt dieser einfache Sachverhalt nicht bei prozeduralen Sprachen, d.h. functions“ ” in PASCAL verhalten sich z. B. nicht wie mathematische Funktionen: program P (output); var a : integer; f unction f (x : integer) : integer; begin a := x + 1; f := a end; f unction g(x : integer) : integer; begin a := x + 2; g := a end; begin a := 0; write(f (a) + g(a)); a := 0; write(g(a) + f (a)); end; Man erh¨ alt verschiedene Ausgaben:
1 Einleitung
3
f (a) : a = 1, f (a) = 1 g(a) : a = 3, g(a) = 3 f (a) + g(a) = 1 + 3 = 4 bzw. g(a) : a = 2, g(a) = 2 f (a) : a = 3, f (a) = 3 g(a) + f (a) = 2 + 3 = 5 Der Funktionswert f (a) ist also abh¨ angig von seinem Vorkommen im Programm. Bei einer mathematischen Funktion bezeichnet f(a) jedoch stets denselben Funktionswert. Weiterhin hat man in der Mathematik eine konsistente Benutzung von Namen. Die Gleichung x2 − 2x + 1 = 0 hat die L¨osung x = 1. Niemand k¨ame auf die Idee zu sagen, daß eine L¨ osung vorliegt, wenn man f¨ ur das erste Vorkommen von x den Wert 3 nimmt und f¨ ur das zweite den Wert 5 (9−2∗5+ 1 = 0). Eine Variable steht also in ihrem G¨ ultigkeitsbereich stets f¨ ur denselben Wert. Diese Eigenschaft erf¨ ullt die Variable a in dem Programmbeispiel nicht, da ihr Wert durch a := x + 1 bzw. a := x + 2 ge¨andert wird. Bei Argumentationen u ¨ber Programme spielt der Begriff der Gleichheit eine entscheidende Rolle. Da, wie gezeigt, nicht einmal f (a) = f (a) gilt, sind Beweise von Aussagen u ¨ber derartige Programme wesentlich komplizierter als Beweise u ¨ber mathematische Funktionen. Das applikative und funktionale Programmieren beruht nun auf der Idee, das gesamte Programm durchgehend mit Sprachkonstrukten zu programmieren, die jede f¨ ur sich eine mathematische Funktion darstellen. Insbesondere werden also Seiteneffekte und Zeitabh¨ angigkeiten, wie sie sich aus der sequentiellen Programmausf¨ uhrung ergeben, ausgeschlossen. Die Adjektive applikativ“ bzw. funktional“ werden von verschiedenen ” ” Autoren in recht unterschiedlicher Bedeutung benutzt. Einige betrachten beide Begriffe als Synonyme, andere verstehen hierunter zwei unterschiedliche Programmierstile. Daher soll zun¨ achst eine Kl¨arung dieser Begriffe gegeben werden. Das Wort applikativ“ kommt vom lateinischen ’applicare’ (≈ anwenden). ” Applikatives Programmieren ist somit Programmieren, bei dem das tragende Prinzip zur Programmerstellung die Funktionsapplikation, d.h. die Anwendung von Funktionen auf Argumente, ist. Das Wort funktional“ kommt vom ” mathematischen Begriff des Funktionals bzw. h¨ oheren Funktionals, d.h. Funktionen, deren Argumente oder Ergebnisse wieder Funktionen sind. Funktionales Programmieren ist somit Programmieren, bei dem das tragende Konzept zur Programmerstellung die Bildung von neuen Funktionen aus gegebenen Funktionen mit Hilfe von Funktionalen ist. ungGeht man von diesen beiden Definitionen aus, die sich nur an der urspr¨ lichen Bedeutung der Begriffe applikativ“ und funktional“ orientieren, so ” ” sieht man unmittelbar ihre Gemeinsamkeit. L¨ aßt sich eine Funktion auf ein Argument, welches selbst eine Funktion ist, anwenden, so ist sie ein Funktional.
4
1 Einleitung
Die Bildung einer neuen Funktion aus gegebenen Funktionen mit Hilfe eines Funktionals ist nichts anderes als die Applikation (Anwendung) dieses Funktionals. Andererseits gibt es auch gute Gr¨ unde daf¨ ur, zwischen den Begriffen applikativ“ und funktional“ zu differenzieren, wenn man mit applikativ“ ” ” ” das Operieren auf elementaren Daten (z.B. ganzen Zahlen) charakterisiert bzw. mit funktional“ das Operieren auf Funktionen. ” Betrachten wir dazu ein Beispiel, an dem diese Unterscheidung der Programmierstile deutlich wird: F ak : N0 → N 1 falls x = 0 F ak(x) = x ∗ F ak(x − 1) sonst Zun¨ achst sei die Entwicklung eines zugeh¨ origen Programms an einer Vorgehensweise charakterisiert, die u ¨ber elementaren Daten operiert. Wir f¨ uhren eine Variable x ein, die eine nat¨ urliche Zahl als Wert besitzt. Die Fallunterscheidung wird durch das Resultat der Applikation der Funktion null : N → {true, f alse} gesteuert, d.h. durch null(x). Durch Applikation der Vorg¨ angerfunktion V org : N → N0 , der Multiplikation M ult : N0 × N0 → N0 und durch rekursive Applikation von F ak erh¨ alt man wieder einen Ausdruck, der einen elementaren Wert aus N besitzt. Der bedingte Ausdruck ist hier eine dreistellige Funktion {true, f alse} × {1} × N → N. In Infix-Notation hat man den Ausdruck null(x) → 1; M ult(x, F ak(V org(x))). Er pr¨ asentiert f¨ ur einen gegebenen Wert x die nat¨ urliche Zahl x! und ist keine Funktion. Erst durch explizite Abstraktion nach der Variablen x erh¨alt man eine Funktion. In diesem Zusammenhang ist Church’s λ-Notation gebr¨auchlich: λx.null(x) → 1; M ult(x, F ak(V org(x))). Da nun eine Funktion definiert ist, kann sie benannt werden: F ak = λx.null(x) → 1; M ult(x, F ak(V org(x))).
1 Einleitung
5
Da in dieser Funktion keine Seiteneffekte auftreten und keine Sequentialisierung vorliegt, kann in jeder Applikation, z.B. Fak(3), die Auswertung von anfallenden Teilausdr¨ ucken in beliebiger Reihenfolge und auch parallel erfolgen. F ak = N ull(3) → 1; M ult(3, F ak(V org(3))) = N ull(3) → 1; M ult(3, F ak(2)) = N ull(3) → 1; M ult(3, N ull(2) → 1; M ult(2, F ak(V org(2)))) = N ull(3) → 1; M ult(3, f alse → 1; M ult(2, F ak(V org(2)))) = N ull(3) → 1; M ult(3, M ult(2, F ak(1))) = N ull(3) → 1; M ult(3, M ult(2, N ull(1) → 1; M ult(1, F ak(V org(1))))) = M ult(3, M ult(2, M ult(1, F ak(0)))) = M ult(3, M ult(2, M ult(1, N ull(0) → 1; F ak(V org(0))))) = M ult(3, M ult(2, M ult(1, 1))) = M ult(3, M ult(2, 1)) = M ult(3, 2) =6 Kennzeichnend f¨ ur das applikative Vorgehen ist also, daß man u ¨ber dem Bereich der elementaren Daten Ausdr¨ ucke aus Konstanten, Variablen und Funktionsapplikationen bildet, die als Wert stets elementare Daten besitzen. Funktionen entstehen daraus durch explizite Abstraktion nach gewissen Variablen. Die Konstruktion einer Funktion aus gegebenen Funktionen st¨ utzt sich auf das applikative Verhalten der gegebenen Funktionen auf elementaren Daten. Funktionen lassen sich aber auch anders konstruieren, indem man gegebene Funktionen durch Applikation von Funktionalen unmittelbar zu neuen Funkupft. Man bildet Ausdr¨ ucke aus Funktionen und Funktionalen, tionen verkn¨ die selbst wieder Funktionen sind. Funktionen f, g : N0 → N0 werden z. B. durch das Funktional ◦ : [N0 → N0 ] × [N0 → N0 ] → [N0 → N0 ] zu der Komposition g ◦ f von f und g verkn¨ upft. Mit diesem Konzept kann man die Funktion F ak ebenfalls entwickeln. Ausgangspunkt sind die Basisfunktionen N ull, M ult und V org wie im vorherigen Beispiel, sowie die Identit¨ at Id und die konstante Funktion ¯1. Aus der zu konstruierenden Funktion F ak und der Vorg¨angerfunktion bildet man mit Hilfe des Funktionals ◦ (Komposition) die Funktion F ak ◦ V org . Mit Hilfe der Identit¨ atsfunktion und des Funktionals [ ] (Konstruktion) entsteht hieraus die Funktion [Id, F ak ◦ V org]
6
1 Einleitung
Die Konstruktion ist hierbei definiert durch [f1 , ..., fn ](x) = (f1 (x), ..., fn (x)) . Durch nochmalige Anwendung der Komposition auf die Basisfunktion M ult und die obige Funktion erh¨ alt man M ult ◦ [Id, F ak ◦ V org]. Mit Hilfe des Funktionals (!!) Bedingung“ bildet man schließlich aus den drei ” Funktionen N ull, ¯ 1 und M ult ◦ [Id, F ak ◦ V org] die gesuchte Funktion N ull →¯ 1; M ult ◦ [Id, F ak ◦ V org] und benennt sie F ak = N ull →¯ 1; M ult ◦ [Id, F ak ◦ V org]. Im Unterschied zum vorherigen Vorgehen ist hier eine Abstraktion nicht notwendig. Die einzige Applikation auf elementare Daten erfolgt bei der Anwendung von Fak auf ein konkretes Argument (Programmstart), z.B. Fak(3). F ak(3) = N ull →¯ 1; M ult ◦ [Id, F ak ◦ V org](3) = N ull(3) →¯1(3); M ult ◦ [Id, F ak ◦ V org](3) = M ult[Id, F ak ◦ V org](3) = M ult([Id, F ak ◦ V org](3)) = M ult(Id(3), F ak ◦ V org(3)) = M ult(3, F ak(V org(3))) = M ult(3, F ak(2)) .. . = M ult(3, M ult(2, M ult(1, F ak(0)))) = M ult(3, M ult(2, M ult(1, N ull(0) →¯ 1(0); ...))) = M ult(3, M ult(2, M ult(1, 1(0)))) = M ult(3, M ult(2, M ult(1, 1))) .. . =6 Die beiden vorgestellten Programme f¨ ur Fak unterscheiden sich zwar optisch nicht sehr stark, da beide unmittelbar auf dem rekursiven Algorithmus f¨ ur die Fakult¨ atsfunktion beruhen. Generell zeigt sich jedoch, daß die unterschiedlichen Programmierstile oft zu verschiedenen Algorithmen zur L¨osung desselben Problems f¨ uhren (siehe z.B. den Unterschied der Funktion L¨ange“ in Kapi” tel 3.1.4 zur u osung). ¨blichen rekursiven L¨ Dieser Unterschied rechtfertigt eine Differenzierung in eine im strengen Sinne funktionale Programmierung und eine im strengen Sinne applikative Programmierung.
1 Einleitung
7
Funktionales Programmieren (im strengen Sinne) Funktionales Programmieren ist ein Programmieren auf Funktionsniveau. Ausgehend von Funktionen werden mit Hilfe von Funktionalen neue Funktionen gebildet. Es treten im Programm keine Applikationen von Funktionen auf elementare Daten auf. Applikatives Programmieren (im strengen Sinne) Applikatives Programmieren ist ein Programmieren auf dem Niveau von elementaren Daten. Mit Konstanten, Variablen und Funktionsapplikationen werden Ausdr¨ ucke gebildet, die als Werte stets elementare Daten besitzen. Durch explizite Abstraktion nach gewissen Variablen erh¨ alt man Funktionen. Diese Differenzierung ist jedoch in der Literatur noch nicht allgemein gebr¨ auchlich. Hier wird oft allgemein von applikativer oder funktionaler Programmierung als Oberbegriff gesprochen. Wenden wir uns nun der Frage zu, wie man funktionale bzw. applikative Sprachen implementieren kann. Es erweist sich als ung¨ unstig, daß vonNeumann-Rechner f¨ ur das Operieren mit Speicherzellen, in denen elementare Daten abgelegt sind, konzipiert sind, und nicht f¨ ur die Programmierung mit Funktionen. Die spezifischen M¨ oglichkeiten zur effizienten Ausf¨ uhrung von funktionalen bzw. applikativen Programmen k¨onnen von einem von Neumann-Rechner nicht voll genutzt werden. Dennoch lassen sich solche Sprachen auf derartigen Rechnern erfolgreich implementieren, wie durch LISPImplementationen und LISP - Maschinen gezeigt wurde. Ein Aussch¨opfen aller Vorteile ist allerdings erst bei alternativen Rechnerarchitekturen zu erwarten. Es gibt bereits eine Reihe von neu entwickelten Rechnerarchitekturen. Ihre Programmierung erfordert zur optimalen Ausnutzung Sprachkonzepte, wie sie z.B. bei der applikativen und funktionalen Programmieren in nat¨ urlicher Weise gegeben sind. Es zeichnet sich hier eine Wechselwirkung ab, wie sie zwischen der vonNeumann- Architektur und herk¨ ommlichen Sprachen schon lange besteht.
2 Mathematische Grundlagen
2.1 Berechenbare Funktionen 2.1.1 Einleitung Ein Algorithmus ist ein Verfahren, mit dessen Hilfe man die Antwort auf Fragen eines gewissen Fragenkomplexes nach einer vorgeschriebenen Methode erh¨ alt. Er muß bis in die letzten Einzelheiten eindeutig angegeben und effektiv durchf¨ uhrbar sein. Insbesondere muß die Vorschrift, die den Algorithmus beschreibt, ein Text endlicher L¨ ange sein. Ein Algorithmus heißt abbrechend, wenn er f¨ ur jede Frage des betrachteten Fragenkomplexes nach endlich vielen Schritten ein Resultat liefert. Andernfalls heißt der Algorithmus nicht abbrechend. Eine Funktion f : M → N , M , N Mengen, heißt berechenbar, wenn es einen abbrechenden Algorithmus gibt, der f¨ ur a ∈ M den Funktionswert f (a) ∈ N als Resultat liefert. W¨ ahrend es f¨ ur manche Fragenkomplexe im Laufe der Zeit gelungen ist, abbrechende Algorithmen zu entwickeln, die eine Antwort auf jede Frage des Problemkreises liefern, sind solche Versuche bei anderen Fragenkomplexen immer wieder fehlgeschlagen. Gibt es also Problemkreise, f¨ ur die es prinzipiell unm¨ oglich ist, einen Algorithmus zu finden, der auf alle Fragen des Problemkreises eine Antwort liefert? Um solche Unm¨oglichkeitsbeweise u ¨berhaupt versuchen zu k¨ onnen, gen¨ ugt die anschauliche Vorstellung vom Begriff des Algorithmus bzw. der Berechenbarkeit nicht; man ben¨otigt mathematische Definitionen. Die historisch fr¨ uheste Pr¨ azisierung ist die Definition der rekursiven Funk” tionen“, die, anders als heute u ul mit Gleichungen ¨blich, einen speziellen Kalk¨ verwendet (G¨ odel 1931, G¨ odel 1934, Herbrand 1931, Kleene 1936 a). Nachdem ¨ in (Kleene 1936 b) die Aquivalenz dieser Definition mit einer anderen Pr¨azisierung, n¨ amlich der Definition der λ-definierbaren Funktion“ (siehe Kapitel ” 2.2) gezeigt wurde, formulierte Church (1936 b) seine ber¨ uhmte These, in der er vorschlug, den Begriff der λ-definierbaren Funktion sowie der rekursiven
10
2 Mathematische Grundlagen
Funktion mit dem anschaulichen Begriff der berechenbaren Funktion, d.h. der intuitiv berechenbaren Funktion, gleichzusetzen. 1936 f¨ uhrte Turing (Turing 1936) den Begriff der Turing-Maschine zur operationalen Pr¨azisierung des Begriffs Algorithmus“ ein und konnte zeigen, ” daß der daraus resultierende Begriff der Turing-berechenbaren Funktion“ mit ” dem der λ-definierbaren Funktionen ¨ aquivalent ist (Turing 1937). Als ein weiterer Begriff, der sich als ¨ aquivalent mit den schon genannten Begriffen herausstellte, wurde von Kleene (Kleene 1936 a) die μ-rekursiven Funktionen bzw. als Unterklasse davon die primitiv rekursiven Funktionen eingef¨ uhrt. Auch alle nachfolgenden Bem¨ uhungen um Pr¨azisierung des anschaulichen Begriffs der berechenbaren Funktionen haben stets zu Definitionen gef¨ uhrt, die sich als ¨ aquivalent zu einem der oben genannten Begriffe herausstellten. Dieses alles spricht f¨ ur die G¨ ultigkeit der Church’schen These, die heute in der Literatur bis auf ganz wenige Ausnahmen (Hermes 1961, §3) als ad¨ aquate mathematische Pr¨ azisierung des Begriffs der berechenba” ren Funktionen“ akzeptiert wird. Wir benutzen sie in folgender Variante als Pr¨ azisierung des Begriffs Algorithmus“: ” Jeder Algorithmus l¨ aßt sich durch eine Turing-Maschine verwirklichen. Als weiteres Argument f¨ ur diese nicht beweisbare These sei angef¨ uhrt, daß man große Klassen von anschaulich gegebenen Algorithmen angeben ¨ kann, f¨ ur deren Ausf¨ uhrung entsprechende Turing-Maschinen existieren. Uberhaupt erscheint es plausibel, daß alle elementaren Schritte, die man bei der Ausf¨ uhrung eines anschaulich gegebenen Algorithmus (z.B. mit Bleistift und Papier) durchf¨ uhrt, auch mit Hilfe einer Turing-Maschine vollzogen werden k¨ onnen. Eine ausf¨ uhrliche Darlegung der verschiedenen Berechenbarkeitsbegriffe ¨ und Beweise von S¨ atzen, aus denen ihre Aquivalenz folgt, findet man in dem Standardwerk von Hermes (Hermes 1961). A. Turing selbst hat mit großer Begeisterung an der Konstruktion der ersten elektronischen Rechenmaschinen mitgewirkt. Ferner war er im 2.Weltkrieg in London als leitender Wissenschaftler bei derjenigen Beh¨orde eingesetzt, die f¨ ur die Entschl¨ usselung der deutschen Code-Nachrichten zust¨andig war. Er war Mitentwickler der Colossus-Rechner , auch Bomben“ genannt, die, aus” gestaltet mit ca. 2500 R¨ ohren, die verschl¨ usselten Funkspr¨ uche der Deutschen entschl¨ usselten. Turing-Maschinen kann man hinsichtlich ihres prinzipiellen Aufbaus und ihrer Arbeitsweise als das abstrakte Konzept aller von Neumann-Rechenmaschinen ansehen. Es ist nat¨ urlich zu beachten, daß ein konkreter von Neumann-Rechner im Gegensatz zu einer Turing-Maschine nur u ¨ber einen endlichen Speicher verf¨ ugt. Man kann also ein Programm f¨ ur eine Rechenmaschine als ein TuringMaschinenprogramm ansehen, durch das eine berechenbare Funktion von der Menge der Eingabedaten in die Menge der Ausgabedaten definiert wird. Die
2.1 Berechenbare Funktionen
11
Ausf¨ uhrung startet in einer Anfangskonfiguration, bei der sich der Rechner in einem Anfangszustand befindet und die Eingabedaten im Speicher abgelegt sind: Die Anweisungen des Programms definieren Konfigurations¨ uberg¨ange, bei denen sich im allgemeinen der Rechnerzustand und die Speicherbelegung andern. Wenn eine Endkonfiguration erreicht wird, charakterisiert durch einen ¨ Endzustand des Rechners, dann findet man im Speicher das Resultat der Ausf¨ uhrung des Programms (Algorithmus). Diese Semantik von Programmen beschr¨ ankt sich jedoch nicht nur auf das maschinennahe Programmieren, z.B. mit einer Assemblersprache, sie hat auch die Definition der u ¨berwiegenden Mehrzahl der gebr¨ auchlichen h¨ oheren Programmiersprachen wie FORTRAN, ALGOL-60, PASCAL und ihrer Nachfolgesprachen gepr¨agt. Die Schwierigkeiten mit dem mathematisch recht unflexiblen Begriff der TuringBerechenbarkeit ¨ außern sich hier z.B., wenn man versucht, S¨atze u ¨ber FORTRAN-Programme zu beweisen, oder wenn man versucht, einen Kalk¨ ul f¨ ur Programme zu entwickeln, analog zum Rechnen mit algebraischen Ausdr¨ ucken. J. Backus hat 1977 in seiner Turing-Award Lecture (Backus 1978) in brillanter Weise die fatalen Auswirkungen der von Neumann-Rechnerarchitektur - und damit letztlich der Turing-Berechenbarkeit - auf den Prozeß der Programmerstellung analysiert und funktionales Programmieren als radikale Alternative gefordert. In der mathematischen Logik hat man schon recht fr¨ uh erkannt, daß man f¨ ur die Untersuchung von Eigenschaften der berechenbaren Funktionen statt der Turing-Berechenbarkeit ¨ aquivalente, aber mathematisch leichter handhabbare Berechenbarkeitsbegriffe benutzen sollte. Beim funktionalen bzw. applikativen Programmieren definiert man Funktionen und verkn¨ upft sie mit den Daten allein durch die Operation der Applikation einer Funktion auf Argumente. Ein hierf¨ ur zweckm¨ aßiger Begriff der Berechenbarkeit sollte sich also unmittelbar auf Prinzipien zur Konstruktion von berechenbaren Funktionen aus gegebenen berechenbaren Funktionen und berechenbaren Basisfunktionen abst¨ utzen. Dieser Anforderung werden die Konzepte der μ-rekursiven Funktionen sowie die eng damit verwandten Konzepte der λ-definierbaren Funktionen bzw. der kombinatorisch definierbaren Funktionen in besonderer Weise gerecht. Warnend sei jedoch schon hier erw¨ ahnt, daß man in der Regel die Menge der durch eine funktionale bzw. applikative Programmiersprache definierbaren Funktionen nicht mit der Menge der μ-rekursiven Funktionen identifizieren darf. 2.1.2 Primitiv-rekursive Funktionen Fast alle Funktionen, die beim praktischen Programmieren auftreten, geh¨oren zu der Klasse der primitiv-rekursiven Funktionen. Diese ist jedoch eine echte Teilmenge der berechenbaren Funktionen. Erst durch eine Erweiterung zu den partiell rekursiven Funktionen erh¨ alt man einen Begriff, der sich als ¨aquivalent mit dem der Turing-berechenbaren Funktionen erweist.
12
2 Mathematische Grundlagen
Unter dem Definitionsbereich dom(f ) ⊆ M einer Funktion f : M → N versteht man die Menge dom(f ) = {x | f (x) ist definiert}. f heißt partielle Funktion genau dann, wenn dom(f ) ⊂ M . f heißt totale Funktion genau dann, wenn dom(f ) = M . urlichen Zahlen einschließlich 0 bezeichnet, Mit N0 wird der Menge der nat¨ ur k ≥ 2 mit der Nk0 ist das k-fache kartesische Produkt N0 × N0 × · · · × N0 f¨ k−mal
Erweiterung N10 = N0 . Bei der Untersuchung von Begriffen der Berechenbarkeit hat sich gezeigt, daß man das Rechnen in den verschiedenen Datenbereichen, wie z.B. der Menge aller endlichen Worte u ¨ber einem endlichen Alphabet, stets auf das Rechnen mit nat¨ urlichen Zahlen zur¨ uckf¨ uhren kann. Wir betrachten deshalb unmittelbar n - stellige Funktionen f : Nn0 → N0 mit n ≥ 0. Im Falle n = 0 bezeichnet der Name der Funktionen zugleich den Funktionswert, also ein Element aus N0 . Deshalb sind die nullstelligen Funktionen die Konstanten. Definition 2.1-1 Eine Funktion f : Nn0 → N0 mit n ≥ 0 heißt genau dann primitiv-rekursiv, wenn sie eine der unter 1. - 3. definierten Grundfunktionen ist, oder wenn sie durch endliche Anwendung der Konstruktionsschemata 4. oder 5. definiert ist. 1. Nullfunktionen: K n : Nn0 → N0 , n ≥ 0 K n (x1 , . . . , xn ) = 0 2. Projektionen: Pin : Nn0 → N, n ≥ 1, 1 ≤ i ≤ n Pin (x1, . . . , xn ) = xi 3. Nachfolgerfunktion: N : N0 → N N (x) = x + 1 Durch N(x) erh¨ alt man den Nachfolger von x im Sinne der Peano-Axiome f¨ ur die nat¨ urlichen Zahlen. Er wird oft mit x bezeichnet. 4. Einsetzung: Seien gi : Nn0 → N mit 1 ≤ i ≤ m und h : Nm 0 → N0 primitiv-rekursiv. Dann ist auch folgende Funktion primitiv-rekursiv: f : Nn0 → N f (x1 , . . . , xn ) = h(g1 (x1 , . . . , xn ), . . . , gm (x1 , . . . , nn ))
2.1 Berechenbare Funktionen
13
5. Primitive Rekursion: Seien g : Nn0 → N0 und h : Nn+2 → N0 primitiv-rekursiv. Dann ist auch 0 folgende Funktion primitiv-rekursiv: f : Nn+1 → N0 0 (I) f (x1 , . . . , xn , 0) = g(x1 , . . . , xn ) (II) f (x1 , . . . , xn , N (y)) = h(x1 , . . . , xn , y, f (x1 , . . . , xn , y)) Diese Definition ist vollst¨ andig, da sich jedes xn+1 ∈ N0 als N(y) darstellen l¨ aßt, sofern xn+1 = 0 ist. Jede primitiv-rekursive Funktion ist total; das Gegenteil gilt jedoch nicht. Die Schemata f¨ ur Einsetzung und primitive Rekursion sind sehr spezieller Art; es ist daher im allgemeinen nicht zu erwarten, daß Verallgemeinerungen, wie z.B. Schachtelung von Rekursionen, wieder zu primitiv-rekursiven Funktionen f¨ uhren. Einige Verallgemeinerungen f¨ uhren jedoch nicht aus der Klasse der primitiv-rekursiven Funktionen heraus, z.B. Rekursion u ¨ber das erste Argument von h in 5. anstelle des letzten Arguments. Beispiele 2.1-1 1. Die Summe + : N0 ×N0 → N0 mit x+0 = x und x+y = x+y+1 = (x+y) f¨ ur y ≥ 0 ist primitiv-rekursiv, denn +(x, 0) = P12 (x, 0) nach 2. und +(x, y ) = H(x, y, +(x, y)), so daß primitive Rekursion gem¨aß 5. vorliegt. Die Hilfsfunktion H(x, y, z) = N (P33 (x, y, z)) ist primitiv-rekursiv nach 4. und 3.. 2. Das Produkt · : N0 × N0 → N0 mit x · 0 = 0 und x · y = x · y + x f¨ ur y ≥ 0 ist primitiv-rekursiv. Es gilt ·(x, 0) = K 2 und ·(x, y ) = H(x, y, ·(x, y)), so daß primitive Rekursion vorliegt. Die Hilfsfunktion H(x, y, z) = +(P13 (x, y, z), z) ist primitiv rekursiv. ·(x, y ) = H(x, y, ·(x, y)) = +(P13 (x, y, ·(x, y)), ·(x, y)) = +(x, ·(x, y)). 3. Die identische Funktion id : N0 × N0 → N0 mit id(x) = x ist primitivrekursiv nach 5. f¨ ur n = 0 : id(0) = K 0 = 0, id(y ) = H(y, id(y)), H(y, z) = +(N (K 0 ), z). Bemerkung: Zur Einsparung von Hilfsfunktionen kann man o.B.d.A. festsetzen, daß bei primitiver Rekursion nach 5. einige der Argumente der Funktion h fehlen d¨ urfen. Entsprechend darf man bei Einsetzung nach 4. in jeder der Funktionen gi einige Argumente weglassen. 4. Die Funktion cond : N30 → N0 mit cond(x1 , x2 , x3 ) = x2 , falls x1 = 0 und cond(x1 , x2 , x3 ) = x3 , falls x1 = 0
14
2 Mathematische Grundlagen
ist primitiv-rekursiv, da cond(0, x2 , x3 ) = id(x3 ) und cond(y , x2 , x3 ) = id(x2 ). 2.1.3 Primitiv-rekursive Pr¨ adikate Diese Pr¨ adikate spielen eine besondere Rolle bei der Definition primitivrekursiver Funktionen. Sie bilden eine wichtige Teilklasse der entscheidbaren Pr¨ adikate. Definition 2.1-2 Sei B = {true, f alse} die Menge der Wahrheitswerte. Ein n-stelliges, totales Pr¨ adikat p : Nn0 → B, n ≥ 1, heißt genau dann primitiv-rekursiv, wenn seine charakteristische Funktion χp primitiv-rekursiv ist: χp : Nn0 → N
0 falls p(x1 , . . . , xn ) = f alse χp (x1 , . . . , xn ) = 1 falls p(x1 , . . . , xn ) = true Diese Definition ist durch einen leicht zu beweisenden generellen Zusammenhang motiviert: Ein Pr¨ adikat q ist genau dann (intuitiv) entscheidbar, adikat q : Nn0 → B heißt (intuiwenn χq (intuitiv) berechenbar ist. Ein Pr¨ tiv) entscheidbar, wenn es ein Verfahren gibt, das f¨ ur einen beliebigen Wert (a1 , . . . , an ) ∈ Nn0 feststellt, ob q(a1 , . . . , an ) oder q(a1 , . . . , an ) gilt. Hierbei bezeichnet die Negation (gebr¨ auchlich ist auch die Notation ¬). Beispiele 2.1-2 1. Die Pr¨ adikate true, f alse : N0 → B mit true(x) = true bzw. f alse(x) = f alse sind primitiv-rekursiv, da N (K 1 (x)) bzw.K 1 (x) die charakteristischen Funktionen sind. ur 2. Das Pr¨ adikat zero : N0 → B mit zero(0) = true und zero(x) = f alse f¨ x = 0 ist primitiv-rekursiv, da χzero = cond(x, 0, 1) die charakteristische Funktion ist. ur x = 3. Das Pr¨ adikat one : N0 → B mit one(1) = true und one(x) = f alse f¨ 1 ist primitiv-rekursiv, da one(x) = zero(V (x)). Die Vorg¨angerfunktion V : N0 → N0 mit V (0) = 0 und V (y ) = y ist offensichtlich primitivrekursiv. Das Zusammenspiel von primitiv-rekursiven Pr¨adikaten und Funktionen zeigt sich, wenn man Funktionen durch die Angabe von Alternativen definiert. Zur Abk¨ urzung benutzen wir x f¨ ur (x1 , · · · , xn ). Sei p(x) ein n-stelliges primitivrekursives Pr¨ adikat, und seien f (x) und g(x) n-stellige primitiv-rekursive Funktionen, dann ist auch die folgende n-stellige Funktion primitiv-rekursiv:
2.1 Berechenbare Funktionen
15
if p(x) then f (x) else g(x) = cond(χp (x), f (x), g(x)) Wenn p(x) und q(x) primitiv-rekursive Pr¨ adikate sind, dann sind auch die Negation p(x) sowie die Disjunktion p(x) ∨ q(x) primitiv-rekursive Pr¨adikate, da χ p(x) = if zero (χp (x)) then 1 else 0, bzw. χp∨q (x) = if zero (+(χp (x), χq (x))) then 0 else 1. Wegen ihrer Reduzierbarkeit auf Disjunktion und Negation sind folglich auch ¨ die Konjuktion p(x) ∧ q(x), die Implikation p(x) ⊃ q(x) und die Aquivalenz p(x) ≡ q(x) primitiv-rekursive Pr¨ adikate. Die Benutzung der Quantoren ∀x und ∃x f¨ uhrt im allgemeinen zu Pr¨adikaten, die nicht mehr primitiv-rekursiv sind; es sei denn, der Bereich, u ¨ber dem eine Variable x quantifiziert wird, wird auf die Werte x = 0, . . . , k beschr¨ ankt. Wenn Q ein n-stelliges primitiv-rekursives Pr¨adikat ist, dann sind auch die folgenden Pr¨ adikate primitiv-rekursiv: P (x1 , . . . , xi−1 , xi+1 , . . . , xn , xn+1 ) = ∀xi ∈ {0, . . . , xn+1 } : Q(x1 , . . . , xn ), 1 ≤ i ≤ n, P˜ (x1 , . . . , xi−1 , xi+1 , . . . , xn , xn+1 ) = ∃xi ∈ {0, . . . , xn+1 } : Q(x1 , . . . , xn ), 1 ≤ i ≤ n), wobei der jeweilige Wert von xn+1 als obere Schranke des Bereichs dient, u ¨ber den quantifiziert wird. Anschaulich l¨aßt sich die Beschr¨ ankung der Quantoren damit begr¨ unden, daß sowohl P als auch P˜ in endlich vielen Rechenschritten berechenbar sein m¨ ussen. Man betrachte als Beispiel das Pr¨ adikat Q(x), welches definiert ist durch Qx = ∃y : f (x, y) = 0, wobei f primitiv-rekursiv ist und y unbeschr¨ankt quantifiziert ist. Falls es zu einem gegebenen x kein y gibt mit f (x, y) = 0, m¨ ußte man f¨ ur alle Werte von y die Werte f (x, y) berechnen, um dieses festzustellen. Es ist also im allgemeinen nicht zu erwarten, daß Q(x) primitivrekursiv ist. Zur Vervollst¨ andigung sei erw¨ ahnt, daß auch das Potenzieren xy , die Fakult¨ at x! und die modifizierte Differenz λ − y mit dem Zusatz λ − y = 0 f¨ ur x < y primitiv-rekursiv sind, weiterhin auch die Pr¨adikate x = y und x < y. F¨ ur das Rechnen mit nat¨ urlichen Zahlen hat man also eine reichhaltige Auswahl von primitiv-rekursiven Funktionen und Pr¨adikaten zur Verf¨ ugung. Jedoch geh¨ oren auch eine Reihe von berechenbaren Funktionen nicht zu der Klasse der primitiv-rekursiven Funktionen, insbesondere solche, die partiell sind.
16
2 Mathematische Grundlagen
2.1.4 Partiell rekursive Funktionen Der Begriff partiell rekursiv“ tritt auch h¨ aufig als Oberbegriff f¨ ur irgendeinen ” der verschiedenen, a quivalenten Berechenbarkeitsbegriffe auf. Hier betrachten ¨ wir eine spezielle Erweiterung der Klasse der primitiv-rekursiven Funktionen, von der sich zeigen l¨ aßt, daß sie genau die Turing-berechenbaren Funktionen enth¨ alt. Durch dieses Resultat ist letztlich die Identifizierung des speziellen Begriffs partiell rekursiv“ mit berechenbar“ schlechthin zu verstehen. Nach ” ” der Bemerkung zur Definition 2.1-2 hat man deshalb auch mit dem Begriff der partiell rekursiven Pr¨ adikate eine ad¨ aquate Formalisierung der (intuitiv) entscheidbaren Pr¨ adikate. Definitionen 2.1-3 Eine Funktion f : Nn0 → N0 mit n ≥ 0 heißt genau dann partiell rekursiv, wenn sie eine der in Definition 2.1-1 eingef¨ uhrten Grundfunktionen (Nullfunktion K n , Projektion Pin , Nachfolgerfunktion N ) ist, oder wenn sie durch endliche Anwendung von Einsetzung, primitiver Rekursion und unbeschr¨ankter Minimierung definiert ist. Bei der Einsetzung ist zu ber¨ ucksichtigen, daß ein Funktionswert h(g1 (x1 , . . . , xn ), . . . , gm (x1 , . . . , xn )) undefiniert ist, falls einer der eingesetzten Funktionswerte gi (x1 , . . . , xn ) undefiniert ist. Weiterhin ist primitiv-rekursiv“ jeweils durch partiell rekursiv“ ” ” zu ersetzen. n Ein n-stelliges Pr¨ adikat p : N0 → B , n ≥ 1 heißt genau dann partiell rekursiv, wenn seine charakteristische Funktion χp partiell rekursiv ist: χp : Nn0 → N0
⎧ ⎪ falls p(x1 , . . . , xn ) = f alse ⎨0 χp (x1 , . . . , xn ) = 1 falls p(x1 , . . . , xn ) = true ⎪ ⎩ undef iniert falls p(x1 , . . . , xn ) undef iniert
.
→ B, n ≥ 1 ein partiell rekursives Pr¨adikat. Dann ist auch die Sei p : Nn+1 0 folgende durch unbeschr¨ ankte Minimierung definierte Funktion f partiell rekursiv: f : Nn0 → N0 f (x) = h(x, 0) h : Nn0 × N0 → N0 h(x, y) = if p(x, y) then y else h(x, N (y))
.
˙ = true unter der VorDer Wert von f (x) ist das kleinste y˙ ∈ N0 mit p(x, y) ur y < y. ˙ Der Wert von f (x) ist undefiniert, aussetzung, daß p(x, y) = f alse f¨
2.1 Berechenbare Funktionen
17
falls gilt: 1) p(x, y) = f alse f¨ ur alle y ∈ N0 oder ur y < yˆ. 2) p(x, y) ist f¨ ur ein gewisses yˆ undefiniert und p(x, y) = f alse f¨ Beispiele 2.1-3 1. Die Funktion undef (x), die f¨ ur alle x ∈ N0 undefiniert ist, ist partiell rekursiv, da sie aus dem primitiv-rekursiven Pr¨adikat f alse(x, y) = f alse f¨ ur alle x, y ∈ N0 durch unbeschr¨ ankte Minimierung definierbar ist. 2. Eine modifizierte Vorg¨ angerfunktion V˜ : N0 → N0 mit V˜ (0) ist undefi ˜ niert und V (y ) = y ist partiell rekursiv, da sie durch primitive Rekursion definierbar ist. 3. Die Funktion m : N0 → N0 mit x falls x gerade m(x) = 2 undef iniert sonst ist partiell rekursiv, da sie durch unbeschr¨ ankte Minimierung definierbar ist: m(x) = h(x, 0) h(x, y) = if (y + y) = x then y else(x, N (y)) Von besonderem Interesse ist der Fall der unbeschr¨ankten Minimierung, bei dem das Pr¨ adikat p(x, y) total definiert ist und es zu jedem x ein y gibt mit p(x, y) = true, da dann f (x) total definiert ist. Diese Situation wird auch Anwendung des μ-Operators im Normalfall “ genannt (Hermes 1961), wobei ” der μ-Operator der Funktion h(x, y) entspricht. Wenn man nur Anwendungen des μ-Operators im Normalfall zul¨ aßt, erh¨ alt man die Klasse der μ-rekursiven Funktionen (Kleene 1936 a). Offensichtlich ist jede μ-rekursive Funktion intuitiv berechenbar, und offensichtlich ist auch jede primitiv-rekursive Funktion μ-rekursiv. Sei p(x) eine n ≥ 2-stellige Konjunktion bzw. Alternative, dann sind als Verallgemeinerungen zul¨ assig: 1. Permutation q(x1 , . . . , xn ) = p(xπ(1) , . . . , xπ(n) ), π Permutation von 1, . . . , n und 2. Identifizierung r(x1 , . . . , kk−1 , kk+1 , . . . , xn ) = p(x1 , . . . , xk−1 , xi , xk+1 , . . . , xn ) mit 1 ≤ i, k ≤ n. Satz 2.1-1 Die Operationen der Negation, der verallgemeinerten Konjunktionen und Alternativen sowie der beschr¨ ankten Quantifizierungen f¨ uhren von μ-rekursiven
18
2 Mathematische Grundlagen
Pr¨ adikaten wieder zu μ−rekursiven Pr¨ adikaten. Dasselbe gilt f¨ ur die Einsetzung einer μ-rekursiven Funktion in ein μ-rekursives Pr¨adikat. Eine durch Fallunterscheidung mit Hilfe von μ-rekursiven Funktionen und μ-rekursiven Pr¨ adikaten definierte Funktion ist μ-rekursiv. Ein etwas u ¨berraschendes Ergebnis der Theorie der partiell rekursiven Funktionen ist, daß man zur Definition einer partiell rekursiven Funktion den (unbeschr¨ ankten) μ-Operator h¨ ochstens einmal auf ein primitiv-rekursives Pr¨adikat anwenden muß. Die Beziehung zu Turing-berechenbaren Funktionen ergibt sich aus folgendem Satz: Satz 2.1-2 Eine n-stellige partielle Funktion f : Nn0 → N0 ist genau dann partiell rekursiv, wenn sie Turing-berechenbar ist. 2.1.5 Vergleich der betrachteten Klassen von Funktionen Durch die verschiedenen Berechenbarkeitsbegriffe sind Klassen von Funktionen definiert worden, die bez¨ uglich der Mengeninklusion ⊆ verglichen werden sollen. Sei F = {f |f : Nn0 → N0 , partiell} die Menge aller n-stelligen (partiellen) Funktionen mit der Teilmenge P aller partiell rekursiven Funktionen, der Teilmenge M aller μ-rekursiven Funktionen und der Teilmenge P R aller primitiv-rekursiven Funktionen. Es soll nun gezeigt werden, daß die Inklusionen F ⊇ P ⊇ M ⊇ P R echt sind. Nach dem vorangehenden Satz umfaßt P genau die Menge der Turing-berechenbaren Funktionen. Da diese Menge abz¨ ahlbar ist, w¨ahrend F u ahlbar ist, folgt F ⊃ P . Beispiele f¨ ur nicht ¨berabz¨ Turing-berechenbare Funktionen werden meist mit Hilfe des Akzeptierungsverhaltens von Turing-Maschinen konstruiert (Manna 1974, Hermes 1961). Als Abgrenzung f¨ ur das Definieren von berechenbaren Funktionen durch Gleichungen ist ein Beispiel von Kalm´ ar (Hermes 1961) erw¨ahnenswert, in dem ein Gleichungssystem angegeben wird, durch das eine Funktion eindeutig definiert ist, ohne daß man imstande ist, aus dem System beliebige Funktionswerte effektiv zu berechnen. Aus Beispiel 2.1-3, Nr.1, folgt P ⊃ M (M umfaßt genau die totalen Turing-berechenbaren Funktionen). Die Inklusion M ⊃ P R ergibt sich aus der Ackermannfunktion A(x) (Ackermann 1928), die zwar μ-rekursiv ist, jedoch nicht primitiv-rekursiv (Hermes 1961). Wir wollen diese Funktion etwas n¨aher anschauen: Man betrachte folgendes Gleichungssystem f¨ ur f : Nn0 × N0 → N0 : f (0, y) = y f (x , 0) = f (x, 1) f (x , y ) = f (x, f (x , y))
2.1 Berechenbare Funktionen
19
Durch Induktion u aßt sich zeigen, daß f¨ ur (x, y) ∈ N20 der Funktions¨ber x l¨ wert f (x, y) wohldefiniert ist und auch berechnet werden kann. Die Ackermannfunktion A : N0 → N0 ist definiert durch A(x) = f (x, x). Das bei der Definition von f benutzte Rekursionsschema f¨allt wegen der Schachtelung nicht unter das f¨ ur primitive Rekursion zul¨ assige Schema oder eine seiner Modifikationen. Wie kann man nun einsehen, daß A(x) nicht primitiv-rekursiv ist? Wenn man mit Hilfe eines Programms Funktionswerte f (x, y) berechnet, so f¨ allt das enorme Wachstum auf, insbesondere in Abh¨angigkeit von x. In der Tat majorisiert f alle primitiv-rekursiven Funktionen. Lemma 2.1-1 Zu jeder primitiv-rekursiven Funktion g : N0 → N0 gibt es eine Konstante ur alle x1 , . . . , xn ∈ N0 gilt: c ∈ N0 der Art, daß f¨ g(x1 , . . . , xn ) < f (c, x1 + · · · + xn ), falls n > 0 bzw. g < f (c, 0), falls n = 0. Nehmen wir nun an, daß A(x) primitiv-rekursiv sei, dann gibt es also c ∈ N0 mit A(x) < f (c, x) f¨ ur alle x ∈ N0 . Daraus ergibt sich aber f¨ ur x = c der Widerspruch A(c) < f (c, c) = A(c) und es folgt, daß A(x) nicht primitivDef.
rekursiv ist. Hinsichtlich der zentralen Bedeutung des Definierens von Funktionen durch Gleichungen beim funktionalen bzw. applikativen Programmieren sind die Ackermannfunktion und Kalmars Beispiel deutliche Hinweise darauf, daß man dabei mit unerwarteten Ph¨ anomenen zu rechnen hat. Insbesondere kann man nicht davon ausgehen, daß durch alle Programme, die in Form eines Gleichungssystems niedergeschrieben sind, eine wohldefinierte berechenbare Funktion repr¨ asentiert wird. 2.1.6 Effektive Bereiche Wir wollen nun der Frage nachgehen, unter welchen Voraussetzungen man den Begriff der berechenbaren Funktion auf andere Bereiche als den der nat¨ urlichen Zahlen u ¨bertragen kann. Von praktischer Bedeutung sind z.B. Zahlen vom Typ integer bzw. real oder strukturierte Daten wie Arrays, Records und Listen. Betrachten wir im intuitiven Sinne berechenbare Funktionen ur f : M1 → M 2 u ¨ber Mengen M1 und M2 , so ist es einsichtig, daß man f¨ das effektive Berechnen von Funktionswerten auf jeden Fall vern¨ unftige“ Be” nennungen f¨ ur die Elemente von M1 und M2 ben¨otigt. Man m¨ochte z.B. in endlich vielen Rechenschritten entscheiden k¨ onnen, ob zwei Benennungen b1 , b2 dasselbe Element x ∈ M1 bezeichnen. Die Zeichenreihen 13 und 26 sind offensichtlich Namen f¨ ur denselben Dezimalbruch; wie soll man jedoch effektiv berechnen, daß die unendliche Zeichenreihe 1.999. . . und die endliche Zahlenreihe 2 denselben Wert bezeichen?
20
2 Mathematische Grundlagen
Definition 2.1-4 Sei Σ ∗ die Menge aller endlichen Zeichenreihen u ¨ber dem endlichen Alphabet Σ und B ⊆ Σ ∗ . Sei M eine Menge und b : B → M surjektiv. (B, b) heißt effektives Bezeichnungssystem von M , wenn a) f¨ ur α ∈ Σ ∗ intuitiv entscheidbar ist, ob α ∈ B oder ob α ∈ / B gilt und wenn b) f¨ ur α, β ∈ B intuitiv entscheidbar ist, ob b(α) = b(β) oder ob b(α) = b(β) gilt. Wenn es zu M ein effektives Bezeichnungssystem gibt, dann heißt M effektiver Bereich. Der Begriff entscheidbar“ ist hierbei im intuitiven Sinne zu verstehen und ” kann nicht pr¨ azisiert werden. Aber in vielen F¨ allen ist man sich dar¨ uber einig, ob ein effektiver Bereich vorliegt (Oberschelp 1981). Man verlangt also, daß es f¨ ur x ∈ M mindestens einen Namen gibt, daß entscheidbar ist, ob ein Name vorliegt und daß es entscheidbar ist, ob zwei Namen dasselbe Element bezeichnen. Beispiele 2.1-4 1. Alle endlichen Mengen sind effektive Bereiche. 2. N0 ist ein effektiver Bereich. Effektive Bezeichungssysteme sind z.B. das Dezimalsystem, das Dualsystem und die Strichnotation f¨ ur nat¨ urliche Zahlen mit Hilfe der Nachfolgerfunktion (0, 0 , 0 , 0 , . . . ). 3. Z mit α bzw. −α als Namen f¨ ur positive bzw. negative ganze Zahlen und α als Namen f¨ ur Elemente von N0 ist ein effektiver Bereich. α ur rationale Zahlen und α, β als Namen 4. Q mit α β bzw. − β als Namen f¨ f¨ ur Elemente von N0 bzw. von N ist ein effektiver Bereich.
5. Die u ¨berabz¨ahlbaren Mengen R und C sind keine effektiven Bereiche. Es sind keine effektiven Bezeichnungssysteme bekannt. Insbesondere bilden die unendlichen Dezimaldarstellungen kein effektives Bezeichnungssystem. 6. Die Menge Nk0 ist ein effektiver Bereich mit den Zeichenreihen (a1 , . . . , ak ) als Namen f¨ ur k-Tupel und α1 ,. . . ,ak als Darstellungen von b(αi ) ∈ N0 . 7. Analog zu 6. schließt man, daß die Menge aller endlichen Folgen {αi }ki=1 und die aller endlichen Teilmengen (α1 , . . . , αk ) von N0 effektive Bereiche sind. Bemerkung: Die Menge aller Folgen und die aller Teilmengen sind keine effektiven Bereiche!
2.1 Berechenbare Funktionen
21
8. Die Menge Σ ∗ aller endlichen Zeichenreihen u ¨ber einem endlichen Alphabet Σ ist ein effektiver Bereich. Bei Programmiersprachen fallen skalare Datentypen unter effektive Bereiche gem¨ aß 1., der Datentyp integer unter 3., der Datentyp real unter 4. bzw. 5., wobei zu bemerken ist, daß real implementationsbedingt zu einem effektiven Teilbereich von R wird. Arrays fallen unter 6., Records bzw. Sets unter 7. und Strings unter 8. Der folgende Satz liefert die Rechtfertigung daf¨ ur, berechenbare Funktionen nur u urliche Zahlen zu betrachten anstatt u ¨ber nat¨ ¨ber effektiven Bereichen. Satz 2.1-3 Jeder effektive Bereich l¨ aßt sich umkehrbar eindeutig und in beiden Richtungen berechenbar auf die nat¨ urlichen Zahlen abbilden. Beweisskizze: Die Namen des effektiven Bezeichnungssystems lassen sich lexikographisch anordnen und abz¨ ahlen. Definition 2.1-5 odelisierung Sei M eine Menge. Eine injektive Funktion gd : M → N0 heißt G¨ (G¨ odel 1931), wenn gd und gd−1 intuitiv berechenbar sind und der Wertebereich gd(M ) ⊆ N0 entscheidbar ist. Zu jedem effektiven Bereich gibt es also eine G¨ odelisierung. Geht man umgekehrt von einer G¨ odelisierung gd von M aus, und hat man mit (B, b) ein effektives Bezeichnungssystem von N0 , so ist (B , gd−1 ◦ b ) mit b(B ) = gd(M ) ankung von b auf B ein effektives Bezeichnungssystem und mit b als Einschr¨ von M . Satz 2.1-4 M ist ein effektiver Bereich genau dann, wenn es zu M eine G¨odelisierung gibt. Mit diesem Satz ist die Beziehung zwischen Berechenbarkeit in effektiven Beart. reichen und in N0 im wesentlichen gekl¨ Definition 2.1-6 odelisierungen gd1, gd2 und sei Seien M1 , M2 effektive Bereiche mit G¨ f : M1 → M2 eine partielle Funktion.
22
2 Mathematische Grundlagen
-
M1
M2
f gd1
gd2 ?
-
N0
? N0
f˜ f heißt partiell rekursiv genau dann, wenn die Funktion f˜ : N0 → N0 partiell rekursiv ist: gd2 (f (gd−1 1 (x))) falls x ∈ gd1 (m1 ) gerade f˜(x) = 0 sonst Wenn f eine totale Funktion ist, heißt f auch (μ-) rekursiv. Aus dieser Definition ergibt sich unmittelbar, daß f genau dann μ-rekursiv ist, wenn f˜ μ-rekursiv ist. Motiviert wird die Definition 2.1-6 durch die Beobachtung, daß f genau dann intuitiv berechenbar ist, wenn f˜ intuitiv berechenbar ist. Man findet h¨ aufig Definitionen des Begriffs der berechenbaren Funktion, bei denen statt N0 die Menge Σ ∗ der endlichen Zeichenreihen u ¨ber einem endlichen Alphabet Σ zugrunde gelegt wird; insbesondere u ¨ber dem Alphabet {0, 1}, z.B. Manna (Manna 1974). Eine Rechtfertigung daf¨ ur ist, daß in dem obigen Diagramm jedes (intuitive) Berechnungsverfahren f¨ ur f ein Berechnungsverfahren f¨ ur f˜ bewirkt und umgekehrt. G¨ odelisierungen sind in erster Linie als ein theoretisches Hilfsmittel zu verstehen. In Programmiersprachen stehen die ben¨otigten effektiven Bereiche in der Regel unmittelbar zur Verf¨ ugung. Eine Ausnahme bilden hier der reine λ-Kalk¨ ul und die kombinatorische Logik. Eigentlich steht N0 in realen Rechenmaschinen gar nicht zur Verf¨ ugung, sondern der vorhandene effektive Bereich ist die endliche Menge aller Bitfolgen, die eine bestimmte L¨ange nicht u onnen. ¨berschreiten k¨ Beispiel 2.1-5 Typisch f¨ ur G¨ odelisierungen ist das folgende Verfahren f¨ ur k-Tupel bzw. endliche Folgen der L¨ ange k mit Hilfe von Primzahlen. Die Folge der Primzahlen ist primitiv-rekursiv berechenbar: P0 = 2, P1 = 3, P2 = 5, P3 = 7, P4 = 11, . . . Sei gd : Nk0 → N0 mit
2.2 Der λ-Kalk¨ ul x
n−1 gd(< x0 , . . . , xk−1 >) = Πi
23
+1
und gd(< >) = 1. Die Funktion gd ist primitiv-rekursiv und injektiv nach dem Satz von der eindeutigen Zerlegbarkeit in Primzahlpotenzen. Die Umkehrfunktion gd−1 ist primitiv-rekursiv. Sei z = gd(< x0 , . . . , xk−1 >). Dann erh¨alt man xi durch z ˙ 1 und somit das Tupel bzw. die endliche Folge < x1 , . . . , xk−1 >. → exp(i, z) − ˙ ist die Differenz im Bereich der nat¨ Mit − urlichen Zahlen bezeichnet: x − y falls x ≥ y ˙ = x−y 0 sonst Der Wert von exp(i, z) ist der genaue Exponent, mit dem Pi in z aufgeht, falls z > 1 ist. Die Menge gd(Nk0 ) = {z| z > 0 ∧ (∀i ≤ z)(Pi+1 | z ⇒ Pi | z)} ist entscheidbar. Als weiterf¨ uhrende Lekt¨ ure u ¨ber Berechenbarkeit, Entscheidbarkeit und damit verwandte Themen seien die B¨ ucher von Hermes (Hermes 1961), Schoenfield (Schoenfield 1967), Boolos (Boolos 1980), Rogers (Rogers 1967) und Manna (Manna 1974) genannt.
2.2 Der λ-Kalku ¨l 2.2.1 Einleitung Der hier betrachtete, ungetypte λ-Kalk¨ ul (Church 1932, Church 1941) ist eine Theorie der Funktionen, die von der Vorstellung gepr¨agt ist, daß eine Funktion f : D → B im wesentlichen eine Rechenvorschrift ist, mit der zu jedem Argument x ∈ D der Funktionswert f (x) ∈ B berechnet wird, falls dieser existiert. Diese Vorstellung unterscheidet sich wesentlich von dem in der Mathematik u ¨blichen Begriff, bei dem eine Funktion eine Teilmenge von D × B ist, durch die eine Abbildung der Elemente des Definitionsbereichs D in den Bildbereich B als statische Beziehung festgelegt ist. Bei Funktionen des ungetypten λ-Kalk¨ uls spielen deshalb Mengen als Definitions- bzw. Bildbereiche zun¨ achst gar keine Rolle, sondern man konzentriert sich ausschließlich auf den Aspekt der Rechenvorschrift. Das Wesentliche einer Funktion erfaßt man z.B. bei der konstanten Funktion K mit Wert c durch die Gleichung K(x) = c oder bei der identischen Funktion id durch id(x) = x, ohne daß man sich dabei um konkrete Mengen als Definitionsbereich f¨ ur x bzw. als Bildbereich k¨ ummern m¨ ußte. Die Analogie zwischen Funktionen des ungetypten λ-Kalk¨ uls und Programmen geht soweit, daß man Funktionen uneingeschr¨ankt auch als Argumente benutzen kann, so wie man es vom Programmieren her kennt. Ein
24
2 Mathematische Grundlagen
Merkmal der von-Neumann-Rechnerarchitektur ist die Aufhebung der Unterscheidung zwischen Programm und Daten; beides sind Bitfolgen. Es ist im ungetypten λ-Kalk¨ ul also zul¨ assig, Selbstapplikationen der Art f (f ) zu benutzen, die beim mengenorientierten Funktionsbegriff nur unter speziellen Voraussetzungen u ¨ber die betrachteten Funktionen zul¨assig sind. Selbstapplikation muß nicht unbedingt zu Widerspr¨ uchen f¨ uhren. Die Begr¨ under des λ-Kalk¨ uls und der damit eng verwandten Theorie der Kombinatorischen Logik“ (Curry 1930) strebten einerseits die Entwicklung ” einer allgemeinen Theorie der Funktionen an, andererseits suchten sie nach Erweiterungen der Theorie um logische Begriffe, so daß diese als Grundlage f¨ ur die mathematische Logik gew¨ ahlt werden k¨onnen Bei der Kl¨arung des Begriffs der berechenbaren Funktion“ hat der λ-Kalk¨ ul eine zentrale Rolle ” gespielt (Kap. 2.1-1); sp¨ ater hat er sich auch z.B. in der Rekursionstheorie als n¨ utzliches Hilfsmittel erwiesen. Mit der zweiten Zielsetzung war man weniger erfolgreich: Bis heute ist eine allgemein akzeptierte Begr¨ undung der Logik auf der Basis des λ-Kalk¨ uls bzw. einer seiner Varianten noch nicht gegl¨ uckt. Einen Eindruck von den auftretenden Problemen wird am Ende des Abschnitts 2.3-4 gegeben. Die Renaissance des λ-Kalk¨ uls in der Informatik begann mit den bahnbrechenden Arbeiten von McCarthy (McCarthy 1960, McCarthy 1962, McCarthy 1963) und Scott (Scott 1969, Scott 1972), durch die die Relevanz f¨ ur die Programmierung gezeigt wurde bzw. theoretische Probleme im Kalk¨ ul bereinigt werden konnten. Man kann den λ-Kalk¨ ul als das Paradigma einer Programmiersprache ansehen. Hier lassen sich sehr viele Ph¨anomene in u ¨bersichtlicher Weise darstellen und untersuchen, gleichsam unter Laborbedingungen“. Die ” Beziehungen zwischen dem λ-Kalk¨ ul und realen Programmiersprachen sind vielf¨ altiger Natur (Landin 1965, Morris 1968, Reynolds 1970, Gordon 1973, Plotkin 1975). In erster Linie interessieren wir uns hier f¨ ur den Kalk¨ ul als eine funktionale Programmiersprache mit ganz einfacher Syntax und einer mathematisch definierten Semantik (Scott 1972, Stoy 1977, Barendregt 1981, Hindley 1972). Dar¨ uber hinaus soll exemplarisch aufgezeigt werden, daß der λ-Kalk¨ ul ein Bindeglied zwischen Mathematik und Programmiersprachen ist. ¨ Uber den λ-Kalk¨ ul hat man einen relativ einfachen Zugang zur denotationellen Semantik a` la Scott (Scott 1971, Stoy 1977, Wadsworth 1976, Gordon 1979), da hier die mathematischen Strukturen noch relativ einfach sind und das tragende Konzept der Fixpunktbildung“ durchschaubar ist. ” H¨ aufig benutzt man auch nur die λ-Notation von Funktionen, ohne dabei den vollen Kalk¨ ul mit einem mathematischen Modell zu meinen. Man benutzt die Notation zur eleganten Bezeichnung von ’gegebenen’ Funktionen und verwendet Teile des Kalk¨ uls zur Beschreibung des applikativen Verhaltens dieser Funktionen (McCarthy 1962, Bauer 1981).
2.2 Der λ-Kalk¨ ul
25
2.2.2 Der klassische λ-Kalku ¨l Die genaue Bedeutung von λ-Termen variiert geringf¨ ugig bei den verschiedenen Anwendungen des λ-Kalk¨ uls. Gemeinsam sind jedoch stets die folgenden intuitiven Vorstellungen: 1. Ein λ-Term λxM stellt eine einstellige Funktion dar. In Analogie zum Konzept der Funktionsprozeduren kann man die Variable x als einen formalen Parameter ansehen und M als den Funktionsrumpf, d.h. die Bezeichnung eines Ausdrucks, der den Funktionswert bestimmt. Als Argumente bzw. Funktionswerte k¨ onnen sowohl elementare“ Daten als auch ” Funktionen (λ-Terme) auftreten. Falls ausschließlich λ-Terme auftreten, so spricht man vom reinen λ-Kalk¨ ul, sonst von einem angewandten λ-Kalk¨ ul. 2. Ein λ-Term (M N ) bezeichnet die Applikation des Terms M auf den Term N , das Argument. 3. Ein λ-Term ((λxY )A), in dem x in Y auftritt, kann reduziert (ausgewertet, konvertiert) werden, indem man im G¨ ultigkeitsbereich von x f¨ ur alle Vorkommen von x den λ-Term A substituiert. Der so aus Y erhaltene ur das Argument A Term SubxA [Y ] kann als der Funktionswert von λxY f¨ betrachtet werden. 4. Es gibt drei Typen von λ-Termen: Konstanten und Variablen sind atomare λ-Terme, Terme der Art λxM heißen Abstraktionen oder kurz Lambdas“ ” und Terme der Art (M N ) heißen Applikationen. Beispiele 2.2-1 1. Der Term λx(xy) entspricht der Anwendung einer Funktion F auf das Argument y : ∀F : (λx(xy)F ) = (F y). 2. Der Term λxy entspricht der Anwendung der konstanten Funktion mit Wert y : ∀F : ((λxy)F ) = y. 3. Der Term λx(x x) entspricht der Selbstapplikation einer Funktion F : ∀F : (λx(xx)F ) = (F F ). Die zun¨ achst merkw¨ urdig erscheinende Einschr¨ankung auf einstellige Funktionen geht auf einen Sachverhalt zur¨ uck, der unabh¨angig voneinander von Sch¨ onfinkel (Sch¨ onfinkel 1924) und Curry (Curry 1930) entdeckt wurde. Unter sehr allgemeinen Voraussetzungen gilt n¨ amlich, daß zu einer gegebenen n-stelligen Funktion f : [D1 × D2 × · · · × Dn ] → D stets eine a¨quivalente Funktion fcurry existiert mit fcurry : [D1 → [D2 → [. . . [Dn → D] . . . ]]] fcurry = λx1 (λx2 (. . . (λxn f (x1 , x2 , . . . , xn )) . . . ))
26
2 Mathematische Grundlagen
und somit mehrstellige Funktionen auf einstellige zur¨ uckgef¨ uhrt werden k¨ onnen. Der Trick besteht darin, nicht alle Argumente auf einmal zu u ¨bergeben, sondern sie einzeln, der Reihe nach zu verbrauchen“. Man bezeichnet ” dieses Vorgehen zu Ehren eines der Entdecker als Curryfizieren“ (engl. to ” curry). Beispiel 2.2-2 Die zweistellige Funktion h(x, y) = x − y wird repr¨asentiert durch den λ-Term h∗ = λx(λy(x − y)). ((h∗ a)b) ≡ ((λx(λy(x − y))a)b) ≡ (λy(a − y)b) ≡ a − b = h(a, b) allt ein funktionales Resultat λy(a-y) an, Bei der Anwendung von h∗ auf a f¨ wenn man h∗ als eine Funktionsprozedur deutet. Auf dieser M¨oglichkeit beruht das Curryfizieren; deshalb ist seine Anwendung in den gebr¨auchlichen prozeduralen Sprachen nicht m¨ oglich. 2.2.2.1 Elementare Begriffe Die Menge der λ-Terme wird, wie bei Programmiersprachen u ¨blich, durch eine kontextfreie Grammatik Gλ definiert: Definition 2.2-1 Sei V eine abz¨ ahlbare Menge von Variablen und C eine abz¨ahlbare Menge von Konstanten mit V ∩ C = ∅. Falls C = ∅ ist, spricht man vom reinen λ-Kalk¨ ul. Gλ = {Tλ , N Tλ , Lλ , Pλ } Tλ = {λ, (, )} ∪ V ∪ C N Tλ = {Lλ , Vλ , Cλ }
terminale Zeichen nichtterminale Zeichen
Das Axiom ist Lλ ∈ NTλ . Die Menge Pλ besteht aus folgenden Produktionen: Lλ ::= Vλ | Cλ |(λVλ Lλ ) | (Lλ Lλ ) x, y ∈ V Vλ ::= x | y | . . . Cλ ::= a | b | . . . a, b ∈ C Ein Satz der Grammatik Gλ heißt λ-Term. Die Menge aller λ-Terme wird mit Λ bezeichnet. Ein λ-Term mit der Struktur (λVλ Lλ ) heißt Abstraktion (oder λ-Funktion oder kurz Lambda“). ”
2.2 Der λ-Kalk¨ ul
27
Ein λ-Term mit der Struktur (Lλ Lλ ) heißt Applikation. Ein λ-Term x ∈ V bzw. a ∈ C heißt atomarer λ-Term. Beispiele 2.2-3 1) x
2) (
λ
x
)
3) (
λ
x
(
x
Vλ
Vλ Lλ
y
V λ Lλ H H H HH Lλ
y
)
)
Vλ Vλ Vλ
Lλ Lλ HH@ Lλ HHHHH H H HH L λ
Im λ-Kalk¨ ul sind die folgenden Konventionen u ¨blich: 1. Vordere Zeichen a, b, c, . . . des Alphabets bezeichnen Konstanten. Hintere Zeichen x, y, z, . . . des Alphabets bezeichnen Variablen. Großbuchstaben M, N, L, . . . bezeichnen beliebige λ-Terme. 2. Die syntaktische Gleichheit von M, N ∈ Λ wird durch M ≡ N bezeichnet. Das Zeichen = “ ist f¨ ur eine andere Relation reserviert. ” 3. H¨ aufig wird die Variable eines Lambdas vom Rumpf durch “ . “ getrennt, d.h. L ::= (λVλ .Lλ ). 4. Zur Einsparung von Klammern dienen die Regeln: - ¨ außerste Klammerpaare d¨ urfen fehlen - M N1 . . . Nn bedeutet (. . . ((M N1 )N2 ) . . . Nn ) (Linksklammerung), - λx1 x2 . . . xn .M bedeutet λx1 (λx2 . . . (λxn M ) . . . )) Beispiele 2.2-4 1. λxy.yx ≡ (λx(λy(yx))) 2. λxy.yx(λz.z) ≡ (λx(λy((yx)(λz z)))) F¨ ur eine Variable x, die in einem Term M vorkommt, fragt man, ob sie dort durch ein λx gebunden ist bzw. anderenfalls in M frei vorkommt.
28
2 Mathematische Grundlagen
Definitionen 2.2-2 F¨ ur M ∈ Λ ist die Menge F V (M ) der freien Variablen von M und die Menge BV (M ) der gebundenen Variablen von M induktiv definiert durch: F V (x) = {x}, F V (a) = ∅ F V (M N ) = F V (M ) ∪ F V (N ) F V (λxM ) = F V (M )\{x} und BV (x) = ∅, BV (a) = ∅ BV (M N ) = BV (M ) ∪ BV (N ) BV (λxM ) = BV (M ) ∪ {x} Eine Variable x ist frei in M genau dann, wenn x ∈ F V (M ). Eine Variable x ist gebunden in M genau dann, wenn x ∈ BV (M ). Beispiele 2.2-5 1. x ↑ frei
x 2. λxx 3. λx(y λy (y x)) ↑ ↑ ↑↑ ↑ gebunden frei gebunden frei
Eine Variable x sei nicht frei in M . Dann gilt x ∈ BV (M ) oder x ∈ / BV (M ) ∪ F V (M ), d.h. x kommt in M gar nicht vor. Die Begriffe gebunden“ und frei“ sind ” ” also nicht komplement¨ ar. Definition 2.2-3 M ∈ Λ heißt geschlossener λ-Term oder auch Kombinator genau dann, wenn F V (M } = ∅ Die Teilmenge aller geschlossener λ-Terme ist Λ0 ⊂ Λ. Die klassische Semantik (Church 1941} des λ-Kalk¨ uls ist eine Reduktionsse” mantik“, bei der ein Term seine Bedeutung dadurch erh¨alt, daß man in ihm der Reihe nach alle reduzierbaren Applikationen ausrechnet. Eine Analogie ist das Umformen von arithmetischen Ausdr¨ ucken nach den u ¨blichen Regeln. Man ben¨ otigt einen Mechanismus, der das Einsetzen eines λ-Terms N f¨ ur gewisse Vorkommen der Variablen x in dem λ-Term M vornimmt. Definition 2.2-4 ur alle freien Vorkommen von x Die Substitution SubxN [M ] des λ-Terms N f¨ im λ-Term M ist wie folgt induktiv definiert: la. SubxN [x] = N lb. SubxN [a] = a,
a ∈ (V ∪ C)\{x}
2.2 Der λ-Kalk¨ ul
29
2. SubxN [(M1 M2 )] = (SubxN [M1 ]SubxN [M2 ]) 3. SubxN [λxM ] = λxM λySubxN [M ] falls y ∈ / F V (N ) oder x ∈ / F V (M ) 4. SubxN [λyM ] = λzSubxN [Subyz [M ]] falls y ∈ F V (N ) und x ∈ F V (M ) wobei z eine neue Variable ist, die weder in N noch in M vorkommt Punkt 4. der Definition behandelt die F¨ alle, in denen man gebundene λVariablen systematisch umbenennen muß, damit Namenskonflikte vermieden werden. Eine analoge Situation findet man bei prozeduralen Programmiersprachen, wo systematisches Umbenennen beim Expandieren von Prozeduraufrufen als Bestandteil der Kopierregel“ verlangt wird. Viele Programmier” sprachen, die auf dem λ-Kalk¨ ul beruhen, haben Fehler in der Implementation von Punkt 4. der Substitution! Die Notwendigkeit von Umbenennungen wird deutlich im Vergleich mit der naiven“ Substitution Sub , bei der gilt: ” x
x
5. SubN [λyM ] = λySubN [M ]
.
Wir betrachten eine konstante Funktion λyx und benennen sie in eine konx ahlen wir jedoch y statt w, so ergibt stante Funktion Subw [λyx] = λyw um. W¨ x sich die identische Funktion Suby [λyx] = λyy. Bei der korrekten Substitution erh¨ alt man auch in diesem Falle eine konstante Funktion: Subxy [λyx] = λzSubxy [Subxz [x]] = λzSubxy [x] = λzy Bei der naiven Substitution kann es leicht geschehen, daß freie Variablen in einem einzusetzenden Term durch ein λ parasit¨ar gebunden werden und dadurch die urspr¨ ungliche Bindungsrelation verf¨ alscht wird: x
Sub(yz) [λy(xy)] = λy((yz)y) ↑ ↑ y frei y gebunden Subx(yz) [λy(xy)] = λw[Subx(yz) [Subyw [(xy)]] = λw[Subx(yz) [(xw)] = λw((yz)w) , y freie Variable Die grundlegende Definition des λ-Kalk¨ uls ist der Begriff der λ-Konvertierbar” keit“ von Termen, durch den eine Gleichheitsrelation eingef¨ uhrt wird. Definition 2.2-5 Terme M, N ∈ Λ heißen konvertierbar bzw. gleich (M = N ), wenn diese Aussage aus folgenden Axiomen und Deduktionsregeln ableitbar ist:
30
2 Mathematische Grundlagen
Axiomenschemata: / F V (M ) (α-Konversion) λxM = λySubxy [M ] , y ∈ (β-Konversion) (λxM )N = SubxN [M ] M =M (Reflexivit¨at) Deduktionsregeln: N =M ⇒N =M M = N, N = L ⇒ M = L M = N ⇒ MZ = NZ M = N ⇒ ZM = ZN M = N ⇒ λxM = λxN
(Symmetrie) (Transitivit¨at) (G¨ ultigkeit von = in Linkskontexten) (G¨ ultigkeit von = in Rechtskontexten) (schwache Extensionalit¨at )
Der λ-Kalk¨ ul ist also eine Theorie λ mit Gleichungen M = N . Beweisbarkeit in λ wird mit λ M = N bezeichnet; meist jedoch nur durch M = N . Es gibt jedoch in λ selbst keine logischen Verkn¨ upfungen; es ist ein reiner Kalk¨ ul mit Gleichungen. H¨ aufig spricht man informell u ¨ber λ und benutzt dabei logische Verkn¨ upfungen und Quantoren, z.B. ∀M : (λx x)M = M M = N ⇒ MM = NN ∧ MN = NM mit der Bedeutung ∀M ∈ Λ λ (λx.x)M = M λ M = N ⇒ λ M M = N N und λ M N = N M G¨ angige Bezeichnungen f¨ ur λ sind: λ -Kalk¨ ul, λβ-Kalk¨ ul, λK-Kalk¨ ul, λKβKalk¨ ul. Im Gegensatz zu einer eingeschr¨ ankten Version des Kalk¨ uls (λIKalk¨ ul) ist in λ der Kombinator K ≡ λxy.x vorhanden. Die Beziehung zur syntaktischen Gleichheit ergibt sich aus M ≡ N ⇒ M = N . Die Umkehrung gilt jedoch nicht. Beispiel 2.2-6 Bei zus¨ atzlicher“ Verwendung der naiven Substitution gilt: ∀M, N : M = N . ” Das ist ein Hinweis, wie empfindlich der Gleichheitsbegriff im λ-Kalk¨ ul ist. Beweis: Sei F ≡ λxy.yx . Es gilt mit Sub: ∀M, N : F M N = N M. Insbesondere gilt F yx = xy. Andererseits gilt mit Sub: F yx ≡ ((λx(λy.yx))y)x = (λy.yy)x = xx ↑ ↑ y frei y gebunden
2.2 Der λ-Kalk¨ ul
31
Aus xy = xx folgt mit den Deduktionsregeln (λxy.xy)IM = (λxy.xx)IM
.
Dabei ist M ∈ Λ ein beliebiger λ-Term und I ≡ λx.x. IM = II M =I F¨ ur M, N ∈ Λ gilt also M = I = N . 2.2.2.2 Reduktionsregeln des λ-Kalku ¨ ls Durch den Begriff der Reduktion soll das Berechnen des Funktionswertes von λxM f¨ ur das Argument N pr¨ azisiert werden. Definition 2.2-6 Ein λ-Term P wird zu einem Term P reduziert, P → P , wenn einer der folgenden zwei Reduktionsschritte auf P oder einen Unterterm von P angewandt wird. 1. α-Reduktion λxM → λySubxy [M ] α
(Systematisches Umbenennen) , y∈ / F V (M )
2. β-Reduktion (Kopierregel) (λxM )N → SubxN [M ] β
Falls X durch eine endliche, eventuell leere Folge von Reduktionsschritten zu ∗ Y reduzierbar ist, schreibt man X → Y . Durch die Forderung y ∈ / F V (M ) in der α-Reduktion sollen unzul¨assige Umbenennungen folgender Art ausgeschlossen werden: λx(xy) → λy Subxy [(xy)] = λy(yy) ↑ frei ∗
α
↑ gebunden
¨ Da → eine Aquivalenzrelation ist, die die syntaktische Gleichheit ≡ umfaßt, α wird diese auf α-¨ aquivalente Terme ausgedehnt. Wir identifizieren also z.B. λxx ≡ λyy auf syntaktischem Niveau und betrachten im Kalk¨ ul keine αReduktionen. Die einzige relevante Anwendung des systematischen Umbenennens findet man explizit in Fall 4. der Definition von SubxN [M ]. Die β-Reduktion gestattet die eigentliche Berechnung von Werten f¨ ur λTerme. In der Terminologie prozeduraler Sprachen ist x der formale Parameter einer Funktion mit dem Rumpf M , die durch (λxM )N mit dem aktuellen Parameter N aufgerufen wird. Die Funktion selbst erh¨alt jedoch keinen Namen.
32
2 Mathematische Grundlagen
Beispiele 2.2-7 1) Von besonderer Bedeutung sind die Terme I ≡ λxx
, K ≡ λxy.x
, S ≡ λxyz.xz(yz)
mit den charakteristischen Eigenschaften: IM
≡ (λxx)M → M
KM N
≡ (((λx(λyx)) M ) N ) → ((λy M ) N ) → M
β
β
SM N L ≡ (((λx(λy(λz(xz(yz))))M )N )L) → (((λy(λz(M z(yz))))N )L)
β
β
→ ((λz(M z(N z)))L) β
→ M L(N L) β
2) Ein Beispiel mit Umbenennung beim Reduzieren: λx(λy(xy))(zy) → Subx(zy) [λy(xy)] = λwSubx(zy) [Subyw [(xy)]] = λwSubx(zy) [(xw)] = λw(zyw) Die Beziehung zwischen der Gleichheitsrelation (Def. 2.2-5) und der Reduzierbarkeit ergibt sich folgendermaßen: Satz 2.2-1 Es gilt X = Y genau dann, wenn Y aus X durch eine endliche eventuell leere Folge von Reduktionsschritten und invertierten Reduktionsschritten hervorgeht. Diese Aussage ist einsichtig, da die Relation = die kleinste durch → erzeugte β
¨ Aquivalenzrelation ist. 2.2.2.3 Extensionale Gleichheit von Funktionen Von einem Gleichheitsbegriff kann man verlangen, daß funktional ¨aquivalente Terme M, N mit M V = N V f¨ ur alle V ∈ A gleich sind, also M = N gilt. Die Definition 2.2-5 erf¨ ullt diese Forderung jedoch nicht. Sei M = λx(yx) und N ≡ y . Es gilt zwar ∀V : M V = yV = N V, aber M = N .
2.2 Der λ-Kalk¨ ul
33
Definition 2.2-7 Terme M, N ∈ Λ heißen extensional gleich, wenn zus¨atzlich zu den Axiomenschemata und Deduktionsregeln aus Definition 2.2-5 die Deduktionsregel: ∀V : M V = N V ⇒ M = N
(Extensionalit¨at )
gilt. Als zugeh¨ orige Erweiterung des Reduktionsbegriffs hat man 3. η-Reduktion / F V (M ). λx(M x) → M x ∈ η
Die η-Reduktion entspricht einer Erweiterung von Definition 2.2-5 um das Axiomenschema : λx(M x) = M
,x ∈ / F V (M )
(η-Reduktion).
Satz 2.2-2 Der λβ-Kalk¨ ul mit extensionaler Gleichheit ist ¨aquivalent zum λβ-Kalk¨ ul, erweitert um das Axiomenschema der η-Reduktion. Beweis: 1. Sei extensionale Gleichheit gegeben. F¨ ur die einfache“ Gleichheit gilt: ” ∀V : λx(M x)V = M V falls x ∈ / F V (M ). Mit der Extensionalit¨ at folgt λx(M x) = M, x ∈ / F V (M ) 2. Sei das Axiomenschema der η-Reduktion gegeben. Seien M, N Terme mit: ∀V : M V = N V Mx = Nx mit V ≡ x und x ∈ / F V (M N ) ur schwache Extensionalit¨at λx(M x) = λx(N x) mit der Regel f¨ M =N mit der Regel f¨ ur η-Reduktion Der wesentliche Unterschied zwischen dem λβ-Kalk¨ ul und dem oben betrachteten λβη-Kalk¨ ul (kurz: λη-Kalk¨ ul) sind also nur Gleichungen der Art λx(M x) = M . Die η–Reduktionen sind u ¨berwiegend von theoretischem Interesse (Hindley 1972, Barendregt 1981), da es vom Standpunkt des Programmierens her irrelevant ist, ob λx(M x) oder M selbst auf ein Argument angewendet wird. Wir werden deshalb weiterhin den λβ-Kalk¨ ul betrachten und im Einzelfall darauf hinweisen, wenn die hier vorgestellten, elementaren Sachverhalte im λβη -Kalk¨ ul abweichend sind.
34
2 Mathematische Grundlagen
2.2.2.4 Die Church-Rosser Eigenschaft Beim Reduzieren von λ-Termen muß man mit den folgenden drei Situationen rechnen: 1. Man gelangt in eindeutiger Weise zu einem nicht weiter reduzierbaren Term, dem Resultat: λx(λyz)zt → (λyz)t → z 2. Man erh¨ alt kein Resultat, da das Reduzieren nicht abbricht. Der geschlossene Term Ω ≡ λx(xx)λx(xx) ist von dieser Art: Ω → Ω → Ω → ... 3. Man hat verschiedene Reduktionsfolgen zur Auswahl λx(λy(yx)z)v
λy(yv)z
λx(zx)v
zv Nun stellt sich die Frage, ob die Reduktionsfolgen stets zu dem gleichen Resultat f¨ uhren, sofern eines existiert. Da dieses der Fall ist (Satz 2.2-3), darf man nicht reduzierbare Terme als Normalformen“ bezeichnen. ” Definitionen 2.2-8 1. Ein λ-Term λxM N heißt β-Redex. Ein λ-Term λx(M x), x ∈ / F V (M ) heißt η-Redex. Ein Redex ist ein β-Redex bzw. ein η-Redex. 2. Ein λ-Term M ist in Normalform genau dann, wenn kein Unterterm von M ein Redex ist. 3. Ein λ-Term M hat eine Normalform genau dann, wenn ein λ-Term N in ∗ Normalform existiert und M → N . β,η
Der folgende Satz ist das ber¨ uhmte Church-Rosser-Theorem f¨ ur den λ-Kalk¨ ul. Es wurde in Church (Church 1941) erstmalig formuliert. Der sehr umfangreiche Beweis wurde in Curry (Curry 1958) im Detail analysiert. Leichter zu verstehen sind die Beweise, die in Hindley (Hindley 1972) bzw. Barendregt (Barendregt 1981) publiziert sind.
2.2 Der λ-Kalk¨ ul
35
Satz 2.2-3 (Church-Rosser) ∗
∗
∗
Sei M ∈ Λ mit M → N und M → N , dann existiert Q ∈ Λ mit N → Q und ∗ N → Q. Skizze: M *
*
N
N *
* Q
Korollar 1 Seien N und N Normalformen von M , dann gilt N ≡ N . Beweis: Aus dem Church-Rosser-Theorem folgt die Existenz von Q mit M *
*
N
N *
* Q
Da N und N Normalformen sind, enthalten N und N keine Redices. Folglich gilt N ≡ Q und N ≡ Q. Die Normalform eines λ-Terms ist also bis auf α-Reduktionen eindeutig bestimmt, sofern eine Normalform existiert. Die Frage nach der Existenz einer Normalform selbst ist unentscheidbar. Dies war eines der ersten Resultate u ¨ber Entscheidbarkeiten und wurde von Church entdeckt (Church 1936 a,b). Der in Definition 2.2-5 eingef¨ uhrte Gleichheitsbegriff X = Y wird durch folgende Variante des Church-Rosser-Theorems motiviert. X und Y werden gleichgesetzt, da beide dieselbe Normalform besitzen.
36
2 Mathematische Grundlagen
Korollar 2 (Church-Rosser-Theorem, 2. Form) ∗
∗
Sei M = N , dann existiert Q mit M → Q und N → Q. Beweis: Man induziert u ¨ber die Anzahl n der Reduktionen und inversen Reduktionen, die sich aus M = N ergeben. Mit X Y ist gemeint, daß entweder X → Y oder Y → X gilt. F¨ ur n = 1 gilt M → N bzw. M ← N und somit Q ≡ N bzw. Q ≡ M . Beim Induktionsschritt gehen wir aus von M · · · N N. Es gilt M = N und nach Induktionsvoraussetzung existiert P mit M → P und N → P . a) N ←− N : ∗ ∗ Dann gilt f¨ ur Q ≡ P offensichtlich M → Q und N → Q. b) N → N : ∗ ∗ Wegen des Church-Rosser-Theorems existiert Q mit P → Q und N → Q. ∗ ∗ Es gilt dann M → Q und N → Q. M · · · N *
*
P
N *
*
Q Korollar 3 ∗
Sei M = N und sei N in Normalform, dann gilt M → N . Beweis: ∗
∗
Wegen Korollar 2 existiert Q mit M → Q und N → Q. Da N in Normalform ist, gilt N ≡ Q. Korollar 4 Sei M = N . Dann gilt entweder a) M und N haben dieselbe Normalform oder b) M und N haben beide keine Normalform.
2.2 Der λ-Kalk¨ ul
37
Beweis: a) Seien M bzw. N Normalformen von M bzw. N . Aus M = N folgt M = N ∗ und mit Korollar 3 gilt M → N . Da M Normalform ist, folgt M ≡ N . b) Es existiere o.B.d.A. keine Normalform f¨ ur M , aber N als Normalform von N . ∗ ∗ Aus M = N und N → N folgt M = N und nach Korollar 3 gilt M → N im Widerspruch zur Annahme, daß M keine Normalform besitzt. Als Spezialfall erh¨ alt man: Korollar 5 Seien M, N Normalformen mit M = N , dann gilt M ≡ N . Von besonderem Interesse ist die Negation des Korollars: Seien M, N verschiedene Normalformen, d.h. M ≡ N , dann gilt M = N . Man betrachte nun die beiden λ-Terme K ≡ λxy.x und S ≡ λxyz.xz(yz), die Normalformen sind mit K ≡ S. Dann gilt S = K. Es gibt also wenigstens zwei ul mit = als Gleichheit ist nicht ineinander konvertierbare Terme. Der λ-Kalk¨ also konsistent in dem Sinne, daß nicht alle Terme gleich sind. Wie leicht der Kalk¨ ul durch Erweiterungen der Gleichheitsrelation inkonsistent werden kann, zeigt folgendes Beispiel. Beispiel 2.2-8 Wir erweitern = um das Axiom K = S und erhalten: KXY Z = SXY Z Sei X ≡ Z ≡ I : Sei Y ≡ KM :
XZ = XZ(Y Z) I = YI I=M
F¨ ur M, N ∈ Λ gilt also M = I = N , d.h. = ist durch K = S inkonsistent geworden. Der folgende Satz zeigt, daß man im reinen λ-Kalk¨ ul Terme mit verschiedenen Normalformen nicht identifizieren darf und gibt dadurch Aufschluß u ¨ber konsistente Gleichheitsbegriffe auf λ-Termen. Die Frage bleibt offen, ob man alle Terme ohne Normalform identifizieren darf.
38
2 Mathematische Grundlagen
Satz 2.2-4 Seien M, N Terme des reinen λ-Kalk¨ uls, die eine Normalform besitzen, dann gilt entweder schon M = N , oder die Hinzunahme des Axioms M = N zum Gleichheitsbegriff = f¨ uhrt zur Inkonsistenz. Der Beweis beruht auf einem tiefliegenden Resultat im λη -Kalk¨ ul (B¨ohm 1968): ˜ ≡ N ˜ verschiedene Normalformen sind, dann ist das Axiom M ˜ =N ˜ Wenn M ˜ bzw. N ˜ die Normaleine inkonsistente Erweiterung der Gleichheit. Seien M formen von M bzw. N . a)
M ≡ N : Nach dem Vorangehenden ist dann M = N eine inkonsistente Erweiterung und folglich auch M = N .
b)
M ≡ N: Dann gilt M = M = N = N
Bisher wurde der λ-Kalk¨ ul als ein formales System mit Gleichheitsbegriff und Reduktionsregeln betrachtet, wobei die Intention stets deutlich wurde, daß λ-Terme das applikative Verhalten von Funktionen modellieren sollten. Man kann nun fragen, welche konkrete Bedeutung einem beliebigen Term zukommen soll. Gesucht ist eine mathematische Struktur (Modell), in der man den λ-Termen eine Semantik zuordnen kann. Hier gibt es nat¨ urlich viele M¨oglichkeiten. Eine erste, wenn auch noch unzul¨ angliche Methode, ist die von Church, bei der man jedem λ-Term seine Normalform zuordnet, sofern vorhanden. Definition 2.2-9 Sei M ein λ-Term. Seine Semantik V al[M ] ist gegeben durch N falls N die Normalform N hat V al[M ] = undef iniert sonst Es gilt offenbar E = F =⇒ V al[E] = V al[F ] Die Umkehrung gilt nur f¨ ur den Fall, daß V al[E] und V al[F ] definiert sind. Als problematisch stellt sich heraus, alle Terme ohne Normalform als undefiniert anzusehen (s. Kap. 2.2.5). Im Zusammenhang mit Programmiersprachen z.B. bei LISP-¨ahnlichen Sprachen, ist die durch V al gegebene Reduktionssemantik durchaus brauchbar. Unter der Annahme, daß η-Reduktionen nicht erlaubt sind, berechnet
2.2 Der λ-Kalk¨ ul
39
man den Wert einer Funktion f n f¨ ur ein gegebenes Argument z im wesentlichen durch das Bestimmen der Normalform von f n(z) mit Hilfe der Substitution Subxy [M ]. Wenn man dabei nicht zu einer Normalform gelangt, so entspricht das intuitiv einer nicht terminierenden Rechnung, und es ist durchaus vern¨ unftig, V al[f n(z)] als undefiniert anzusehen. 2.2.3 Wahrheitswerte und logische Verknu ¨ pungen In diesem und dem folgenden Abschnitt soll gezeigt werden, daß sich auch Daten mit den zugeh¨ origen Operationen allein durch λ-Terme darstellen lassen. Damit rundet sich das Bild vom reinen λ-Kalk¨ ul als das Paradigma einer funktionalen bzw. applikativen Programmiersprache ab. Ein bedingter Ausdruck if B then S1 else S2 f i mit B ∈ {true, f alse} und uhrung von S1 f¨ uhrt und B = f alse zu der Bedeutung, daß B = true zur Ausf¨ der von S2 , kann man als dreistellige Funktion if −then−else−f i (B, S1 , S2 ) auffassen. Ersetzt man if − then − else − f i durch λxyz.(xyz), true durch λxy.x und f alse durch λxy.y, so reduziert λxyz.(xyz)BS1 S2 zu (BS1 S2 ) und f¨ ur den Fall, daß B = true ist, zu S1 und daß B = f alse ist, zu S2 . Die den Wahrheitswerten zugeordneten Terme wirken also wie Selektoren auf ihren Argumenten, wodurch die Darstellung der Wahrheitswerte im λ-Kalk¨ ul als zweistellige Funktionen“ T und F motiviert ist. ” Definition 2.2-10 Seien T, F ∈ Λ als Repr¨ asentaten der Wahrheitswerte definiert durch: i) T ≡ λxy.x und ii) F ≡ Xxy.y . Satz 2.2-5 Die Booleschen Funktionen Negation, Konjunktion und Disjunktion lassen sich durch folgende Terme darstellen: 1) not ≡ λx((xF )T ) 2) and ≡ λxy.((xy)F ) 3) or ≡ λxy.((xT )y) Beweis: zu 1) not T ≡ λx((xF )T )T = ((T F )T ) = T F T = F i)
not F ≡ λx((xF )T )F = ((F F )T ) = F F T = T ii)
40
2 Mathematische Grundlagen
zu 2) Es werden zwei Hilfsrechnungen vorangestellt. and T ≡ λxy.((xy)F )T = λy((T y)F ) = λyy = I i)
Die identische Funktion I wurde in Beispiel 2.2-7 zusammen mit K = λxy.x eingef¨ uhrt. and F ≡ λxy.((xy)F )F = λy((F y)F ) = λyF = KF ii)
Damit gilt nun: and T T = IT and T F = IF and F T = KF T and F F = KF F zu 3) or T
=T =F =F =F
≡ λxy.((xT )y)T = λy((T T )y) = λyT = KT i)
or F
≡ λxy.((xT )y)F = λy((F T )y) = λyy = I ii)
or or or or
TT TF FT FF
= KT T = KT F = IT = IF
=T =T =T =F
Wir haben hier ein erstes Beispiel f¨ ur den funktionalen bzw. applikativen Programmierstil. Charakteristisch ist in diesem speziellen Fall, daß man keine Trennung von Datenstrukturen und Kontrollstrukturen hat; beides sind λ-Terme. In den u oheren prozeduralen Programmiersprachen haben ¨blichen h¨ Funktionsprozeduren keine Funktionsprozeduren als Resultat. Deshalb hat der λ-Kalk¨ ul und der darauf basierende applikative Programmierstil kein unmittelbares Analogon in diesen Sprachen. 2.2.4 Arithmetik und λ-Definierbarkeit Entsprechend zum Vorgehen im vorigen Abschnitt werden Terme eingef¨ uhrt, die die nat¨ urlichen Zahlen darstellen, sowie Terme, durch die die Nachfolgerbzw. Vorg¨ angerfunktion repr¨ asentiert wird. Weiterhin muß man die Darstellung der Null von den u ¨brigen Zahlen unterscheiden k¨onnen. Wenn diese generellen Voraussetzungen erf¨ ullt sind, kann man Arithmetik durch Systeme von λ-Termen definieren. Definition 2.2-11 Jeder nat¨ urlichen Zahl n ∈ N0 wird ein λ-Term n ∈ Λ zugeordnet: 0 ≡ λxy.x 1 ≡ λxy.(y 0) .. . n ≡ λxy.(y n − 1)
.
Zahlen werden also durch paarweise verschiedene Normalformen dargestellt.
2.2 Der λ-Kalk¨ ul
41
Satz 2.2-6 Es gibt λ-Terme suc, pred und zero mit 1. suc n = n + 1 2. pred n + 1 = n und pred 0 undefiniert 3. zero 0 = T und zero n + 1 = F, die die Nachfolgerfunktion, die Vorg¨ angerfunktion und den Test auf 0 darstellen. Beweis: Zu 1) Man w¨ ahle suc ≡ λzxy.(yz). Dann gilt suc 0 = λxy.(y0) = 1 und suc n = λxy.(yn) = n + 1 Zu 2) Man w¨ ahle pred ≡ λx((xΩ)I). Ω ist ein Term ohne Normalform (2.2.2.4) und I = λxx die identische Funktion. Man beachte, daß die Definition von 0 und T u ¨bereinstimmen. pred 0 ≡ λx((xΩ)I)0 = (0Ω)I ≡ T ΩI = Ω pred n + 1 ≡ λx((xΩ)I)λx(λy(yn)) = (λx(λy(yn))Ω)I = λy(yn)I = In = n Zu 3) Man w¨ ahle zero ≡ λx((xT )λxF ) zero 0 ≡ λx((xT )λxF )0 = (0T )λxF = T zero n + 1 ≡ λx((xT )λxF )λx(λy(yn)) = (λx(λy(yn))T )λxF = λy(yn)λxF = λxF n = F Im folgenden wird zero gar nicht explizit auftreten, da wir uns eine Eigenschaft der gew¨ ahlten Zahlendarstellung zunutze machen werden, die die ben¨otigten Fallunterscheidungen f¨ ur n = 0 und n > 0 enth¨ alt. M falls n = 0 i) (n M )N = N n − 1 falls n > 0 Da 0 = T ist, ist der Fall n = 0 bewiesen. Sonst gilt: (n M )N ≡ (λx(λy(y n − 1))M )N = N n − 1 .
42
2 Mathematische Grundlagen
Satz 2.2-7 Es gibt λ-Terme sum und prod mit 1. sum m n = m + n und ¨r m, n ∈ N0 2. prod m n = m ∗ n f u Beweis: Zu 1) Die Summe m + n soll unter Anwendung der Eigenschaft i) im Prinzip nach dem rekursiven Schema sum m n := if = n = 0 then m else suc(sum m n − 1) f i berechnet werden. Unter der Annahme, daß sich ein λ-Term f¨ ur sum finden l¨aßt, der folgender Gleichung gen¨ ugt: (*) sum = λxy.((yx)λz(suc(sum x z)))1
,
beweist man durch vollst¨ andige Induktion: sum m 0 = ((0 m)λz(suc(sum m z))) = m. Es gelte sum m k = m + k f¨ ur k ≥ 0.
i)
sum m k + l = ((k + 1 m)λz(suc(sum m z))) = λz(suc(sum m z))k = suc(sum m k) = suc m + k = m + k + 1 Zu 2) Unter der Annahme, daß sich ein λ-Term f¨ ur prod finden l¨aßt, der der Gleichung (**)
prod = λxy.((y 0)λz(sum(prod x z)x))
gen¨ ugt, beweist man durch vollst¨ andige Induktion: prod m 0 = ((0 0)λz(sum(prod m z)m)) = 0
.
Es gelte prod m k = m ∗ k f¨ ur k ≥ 0. prod m k + l = ((k + 1 0)λz(sum(prod m z)m)) = λz(sum(prod m z)m)k i)
= sum(prod m k)m = m ∗ (k + 1) 1
Der Satz 2.2-4 gilt nicht, da eine der gleichgesetzten Normalformen syntaktisch eine Konstante“ ist. ”
2.2 Der λ-Kalk¨ ul
43
Es muß nun noch bewiesen werden, daß es entsprechende λ-Terme f¨ ur sum und prod gibt, da in der formalen Definition der Syntax von λ-Termen Rekursionen nicht zugelassen sind. Anhand der beiden F¨ alle soll ein generelles Verfahren zur Bestimmung von λ-Termen, die einer rekursiven Gleichung gen¨ ugen, demonstriert werden. Sei A ≡ λxy.y(xxy), und sei Y ≡ AA. Dann gilt ii)
Y M ≡ AAM = M (AAM ) = M (Y M ) f¨ ur M ∈ Λ
.
Der Term Y M ist also Fixpunkt von M ; daher der Name Fixpunktkombinator f¨ ur Y . Man betrachtet die rechte Seite der Gleichung von sum und k¨ urzt sum durch s ab: λxy.((yx)λz(suc(sxz)))
.
Man abstrahiert nach der Funktion s F = λsxy.((yx)λz(suc(sxz))) und definiert: sum ≡ Y F
.
ur den λ-Term Y F ist; Es muß betont werden, daß sum nur noch ein Name f¨ ullt: sum tritt in Y F nicht auf ! Die Gleichung (*) wird durch sum erf¨ sum ≡ Y F = F (Y F ) ii)
= λsxy.((yx)λz(suc(sxz))) (Y F ) = λxy.((yx)λz(suc((Y F )x z))) = λxy.((yx)λz(suc(sum x z)))
.
Aus der rechten Seite der Gleichung (**) f¨ ur prod erh¨alt man durch Abstraktion nach der Funktion prod, abgek¨ urzt mit p, G ≡ λpxy.((y0)λz(sum (pxz)x)) und definiert prod ≡ Y G. ullt. Die Gleichung (**) wird durch prod erf¨ Schwierigkeiten kann man beim Rechnen mit sum bzw. prod allerdings dann bekommen, wenn man anf¨ angt, Y F bzw. Y G vollst¨andig zu reduzieren, da diese Terme nach ii) keine Normalform haben. Andererseits sieht man deutlich, daß man deshalb Y F bzw. Y G nicht schlechthin als undefiniert ansehen darf.
44
2 Mathematische Grundlagen
Nach den vorgestellten Beispielen f¨ ur das funktionale und applikative Programmieren mit λ-Termen erscheint es zumindest plausibel, daß man alle μ-rekursiven Funktionen durch λ-Terme definieren kann. Definition 2.2-12 Eine totale Funktion f : Nn0 → N0 heißt λ-definierbar genau dann, wenn es einen λ-Term F gibt mit F x1 . . . xn = f (x1 , . . . , xn ) f¨ ur (x1 , . . . , xn ) ∈ Nn0
.
Satz 2.2-8 (Kleene 1936) Eine totale Funktion f : Nn0 → N0 ist genau dann λ-definierbar, wenn sie μ-rekursiv ist. Beweisskizze: 1. Alle μ-rekursiven Funktionen sind λ-definierbar. Die Grundfunktionen sind λ-definierbar durch K n ≡ λx1 . . . xn .0 Pin ≡ λx1 . . . xn .xi N i ≡ λx.(suc x) Wir nehmen Bezug auf Definition 2.1-1 und bezeichnen den zu einer Funktion f geh¨ orenden definierenden Term mit F . Einsetzung: F ≡ λx.H(G1 x) . . . (Gm x) Primitive Rekursion: F ≡ λxk. if zero k then G x else Hx(pred k)(F x(pred k))f i Minimierung: (Def. 2.1-3) F ≡ λx.Hx0 H ≡ Y (λhxy. if P xy then y else h x(suc y))f i 2. Alle λ-definierbaren Funktionen sind μ-rekursiv. Sei f durch F λ-definiert. Da die Zahlen durch verschiedene Normalformen ul dargestellt sind, gilt f (x) = m genau dann, wenn F x = m im λ-Kalk¨ gilt. Damit kann man systematisch die Funktionswerte von f berechnen,
2.2 Der λ-Kalk¨ ul
45
da die Axiome f¨ ur “ = “ ein effektives Verfahren liefern, um die G¨ ultigkeit von F x = m festzustellen, wenn man normierte Ableitungen benutzt. Damit man den Begriff der λ-Definierbarkeit auf partielle Funktionen erweitern kann, ben¨ otigt man zun¨ achst im λ-Kalk¨ ul eine ad¨aquate Darstellung der Situation, daß ein Funktionswert undefiniert ist. 2.2.5 Terme mit undefinierter Bedeutung Church hat nur Termen, die eine Normalform haben, eine Bedeutung zugeordnet. Terme ohne Normalform sind bedeutungslos, d.h. undefiniert (Church 1941). Mit dieser Festsetzung kann man in dem von Church betrachteten λI-Kalk¨ ul konsistent arbeiten und insbesondere alle partiell rekursiven Funktionen darstellen. Der λI-Kalk¨ ul ist eine Teilmenge des λ-Kalk¨ uls, in der ein λ-Term mit Normalform nur Teilterme enth¨ alt, die ebenfalls eine Normalform haben. Ein λI-Term mit Bedeutung hat also ausschließlich Teilterme ebenfalls mit Bedeutung. Danach ist z.B. KIΩ kein λI-Term. Wenn man nun alle Terme ohne Normalform als bedeutungslos ansieht, dann ist es nat¨ urlich, sie alle miteinander zu identifizieren. Das ist im λIKalk¨ ul eine konsistente Erweiterung des Gleichheitsbegriffs von Termen, jedoch nicht in dem hier betrachteten Kalk¨ ul (bei Church λK-Kalk¨ ul): Beispiel 2.2-9 Sei M ≡ λx(xKΩ), und sei N ≡ λx(xSΩ). Da M und N keine Normalform haben, gelte M = N . Damit folgt K = MK = NK = S und nach Beispiel 2.2-8 folgt, daß die Gleichheit “ = “ inkonsistent erweitert wurde. Eine weitere Schwierigkeit entsteht bei der Komposition von partiellen Funktionen. Beispiel 2.2-10 ur n ∈ N0 , dann ist auch f (g(n)) undeSei f (n) = 0 und sei g(n) undefiniert f¨ fininert f¨ ur n ∈ N0 . Unter der Annahme, daß undefiniert“ durch hat keine ” ” Normalform“ dargestellt wird, w¨ ahlt man F ≡ K0 und G ≡ KΩ f¨ ur f bzw. g. Aber F ◦ G ≡ λx(F (Gx)) = λx(K0(G x)) = λx0 ist keine Funktion, durch die f ◦ g λ-definiert wird.
46
2 Mathematische Grundlagen
Da man mit dem Begriff der Normalform auch in Scott’s mathematischen Modellen des λ-Kalk¨ uls Probleme hat (Wadsworth 1971, Wadsworth 1976), die auf Church’s Identifikation von undefiniert“ mit hat keine Normalform“ be” ” ruhen, betrachtet man heute eine echte Teilmenge der λ-Terme ohne Normalform, n¨ amlich die Menge der unl¨ osbaren“ λ-Terme bzw. der λ-Terme ohne ” ” Kopfnormalform“ als diejenigen λ-Terme, denen keine Bedeutung zukommt. Definition 2.2-13 osbar (Barendregt 1971) genau dann, Ein geschlossener λ-Term M ∈ Λ0 heißt l¨ wenn es λ-Terme N1 , . . . , Nn gibt mit M N 1 . . . Nn = I
.
Ein beliebiger λ-Term M ∈ Λ heißt l¨ osbar, wenn λx1 . . . xk .M l¨osbar ist, wobei osbar, wenn er nicht l¨osbar ist. {x1 , . . . , xk } = F V (M ). M ∈ Λ heißt unl¨ ∗
Da I eine Normalform ist, folgt aus M N1 . . . Nn = I stets M N1 . . . Nn → I. Ein l¨ osbarer λ-Term hat also, als Funktion gedeutet, wenigstens einen Satz von Argumenten, f¨ ur den ein Funktionswert definiert ist. Beispiel 2.2-11 1. S ist l¨ osbar durch SIII = I 2. xIΩ ist l¨ osbar durch λx(xIΩ)K = I 3. Y ist l¨ osbar durch Y (KI) = KI(Y (KI)) = I → N ; also → M folgt M = Ω N mit N 4. Ω ist unl¨ osbar, denn aus Ω N kann Ω N = I nicht erf¨ ullt werden. Ein λ-Term λx1 . . . xn .xM1 . . . Mm mit m, n ≥ 0 l¨aßt sich h¨ochstens zu einem ∗ mit Mi → Mi f¨ ur alle i reduzieren; d.h. der λ-Term λx1 . . . xn .xM1 . . . Mm Kopf“ des Termes ¨ andert sich nicht mehr. ” Definition 2.2-14 Ein λ-Term M ist in Kopfnormalform (Wadsworth 1971) genau dann, wenn ur n = 0 ist M eine Applikation M ≡ λx1 . . . xn .xM1 . . . Mm , m, n ≥ 0. F¨ xM1 . . . Mm . Die Variable x heißt Kopfvariable von M . Ein λ-Term M hat eine Kopfnormalform, wenn es ein M in Kopfnormalform gibt mit M = M . Wenn M keine Kopfnormalform ist, dann hat es die Gestalt M ≡ λx1 . . . xn .(λx.M0 )M1 . . . Mm mit n ≥ 0, m ≥ 1, und (λx.M0 )M1 heißt Kopfredex von M .
2.2 Der λ-Kalk¨ ul
47
Der Begriff Kopfnormalform“ ist mißverst¨ andlich, da keine Teilmenge der ” Normalformen charakterisiert wird. Ein λ-Term M hat genau dann eine Kopfnormalform, wenn die Folge der Reduktionen der Kopfredizes terminiert. Es ist klar, daß jede Normalform auch eine Kopfnormalform ist. Satz 2.2-9 (Wadsworth) M ist unl¨ osbar genau dann, wenn M keine Kopfnormalform hat. Insgesamt hat man folgende Beziehungen: -
undefiniert“ ist gleichwertig mit unl¨ osbar“ ” ” unl¨ osbar“ ist gleichwertig mit hat keine Kopfnormalform“ ” ” Aus hat keine Kopfnormalform“, folgt hat keine Normalform“ ” ” Aber aus hat keine Normalform“, folgt nicht hat keine Kopfnormalform“ ” ” Es gibt eine Reihe von gewichtigen Gr¨ unden (Barendregt 1981) f¨ ur die Identifizierung von undefiniert“ und unl¨ osbar“. Insbesondere treten nun in Scott’s ” ” Modellen des λ-Kalk¨ uls keine Probleme mehr auf. Wir k¨ onnen nun eine ad¨ aquate Definition von λ-Definierbarkeit f¨ ur partielle Funktionen geben. Definition 2.2-15 Eine partielle Funktion f : Nn0 → N0 heißt λ-definierbar genau dann, wenn es einen λ-Term F gibt, so daß f¨ ur (x1 , . . . , xn ) ∈ Nn0 gilt: f (x1 , . . . , xn ) falls f (x1 , . . . , xn ) definiert ist F x1 . . . xn = unl¨ osbar sonst . Satz 2.2-10 Eine partielle Funktion f : Nn0 → N0 ist genau dann λ-definierbar, wenn sie partiell rekursiv ist. Einen Beweis findet man in Barendregt (Barendregt 1981). Mit Hilfe des Begriffs der Kopfnormalform l¨ aßt sich nach L`evy eine konsistente Semantik f¨ ur den λ-Kalk¨ ul definieren, die sich unmittelbar auf den Kalk¨ ul und seine Begriffe abst¨ utzt. Man kann dieses Modell syntaktisch“ und algebra” ” isch“ charakterisieren im Gegensatz zu Scott’s Modellen, die auf topologischen Begriffen beruhen. Man konstruiert auf einer speziellen Teilmenge N der Menge aller Kopfnormalformen eine algebraische Struktur N ∞ und eine partielle Ordnung <, die es gestattet, jedem λ-Term M ∈ Λ einen syntaktischen“ Wert in N ∞ ” zuzuordnen, der mit val[M ] ∈ N ∞ bezeichnet wird (L`evy 1976, 1977).
48
2 Mathematische Grundlagen
Anschaulich ist val[M ] der im allgemeinen unendliche Term, den man durch Reduzieren aller Redices von M erh¨ alt. Teile dieses unendlichen Terms, die keine Kopfnormalform besitzen, sind undefiniert. Etwas verk¨ urzt dargestellt gilt f¨ ur M ∈ Λ: ⎧ ⎪ falls N Normalform von M ist ⎨N val[M ] = undef iniert falls M keine Normalform hat ⎪ ⎩ N sonst . N ist der Grenzwert in N ∞ von approximierenden Kopfnormalformen aus N f¨ ur den vollst¨ andig reduzierten Term M . 2.2.6 Fixpunkte Fixpunkte spielen bei allen Programmiersprachen die entscheidende Rolle bei der Semantikdefinition f¨ ur Rekursionen und while-Schleifen. Im λ-Kalk¨ ul werden die wesentlichen Ideen bei der Benutzung von Fixpunkten besonders deutlich, da sie sich hier syntaktisch formulieren lassen, ohne expliziten Bezug auf eines von Scott’s mathematischen Modellen des λ-Kalk¨ uls nehmen zu m¨ ussen. Satz 2.2-11 (Fixpunkttheorem) F¨ ur alle λ-Terme F gibt es einen λ-Term Φ mit FΦ = Φ
.
Φ heißt Fixpunkt von F . Beweis: Sei W = λx(F (xx)) und sei Φ ≡ WW. Dann gilt Φ ≡ WW = λx(F (xx)W = F (WW) = F Φ Definition 2.2-16 Ein geschlossener λ-Term M mit M F = F (M F ) f¨ ur alle F ∈ Λ heißt Fixpunktkombinator . Da M F also stets ein Fixpunkt von F ist, ist der Name f¨ ur M gerechtfertigt. Satz 2.2-12 Die Terme Yc und Y sind Fixpunktkombinatoren: Yc ≡ λf ((λx(f (xx))) (λ(f (xx)))) Y ≡ AA mit A ≡ λxy.y(xxy) .
,
Der Term Y heißt auch Curry’s paradoxer Kombinator“ (2.3-4). ”
2.2 Der λ-Kalk¨ ul
49
Beweis: Sei W ≡ λx(F (xx)) f¨ ur F ∈ Λ. ∗)
Yc F = WW = F (WW) = F (Yc F ) Y F ≡ AAF = F (AAF ) ≡ F (Y F )
.
Man hat zwar Y F → F (Y F ), aber nicht Yc F → F (Yc F ), da in *) eine inverse β
β
β-Reduktion vorgenommen wird ! Eine Anwendung von Y ist bereits im Beweis von Satz 2.2-7 erfolgt. Beispiel 2.2-12 Wir betrachten die Gleichung F = λxy.F yxF und suchen nach einem λ-Term f¨ ur F , d.h. nach einer L¨osung der Gleichung. Dieser λ-Term verh¨ alt sich dann so, als sei er rekursiv definiert. Sei C(f ) ≡ λxy.f yxf , dann erh¨ alt man aus obiger Gleichung durch λAbstraktion nach F F = (λf.C(f ))F
.
Gesucht ist also ein Fixpunkt von λf.C(f ) und ein solcher ist nach Satz 2.2-12 der gesuchte Term Y (λf.C(f )). Diese Art Gleichungen zu l¨ osen, ist rein syntaktisch zu verstehen. Sie ist jedoch unter sehr generellen Voraussetzungen (Stoy 1977) vertr¨aglich mit der Bildung von kleinsten Fixpunkten in Scott’s semantischen Modellen des λ-Kalk¨ uls. Satz 2.2-13 Sei C(f ) die Notation f¨ ur einen λ-Term mit der freien Variablen f . Dann existiert ein λ-Term F , f¨ ur den C(F ) = SubfF [C(f )] gilt: 1. F = (λf.C(f ))F ∗ 2. F → C(F ) .
.
Zum Beweis setzt man F ≡ Y (λf.C(f )). Mit diesem Satz wird die rekursiv definierte Funktion“ F als Fixpunkt von ” λf.C(f ) charakterisiert. Teil 2. beschreibt das konkrete Rechnen mit solchen Fixpunkten. Eine korrekte Implementation von rekursiven Funktionen muß von diesem Satz ausgehen.
50
2 Mathematische Grundlagen
Rekursive Gleichungssysteme der Form F1 = λf.C1 (f)F1 . . . Fn . .. . Fn = λf.Cn (f)F1 . . . Fn mit f = f1 , . . . , fn werden sinngem¨ aß u ¨ber folgenden Satz gel¨ost: Satz 2.2-14 (Fixpunkttheorem) F¨ ur alle λ-Terme λf.C1 (f), . . . , λf.Cn ( f) mit f = f1 , . . . , fn gibt es λ-Terme F1 , . . . , Fn mit F1 = λf.C1 (f )F1 . . . Fn . .. . Fn = λf.Cn (f )F1 . . . Fn 2.2.7 Reduktionsstrategien Beim Reduzieren von λ-Termen hat man in der Regel die Auswahl zwischen verschiedenen Folgen von Reduktionsschritten. Wegen der Church-RosserEigenschaft k¨ onnen verschiedene Reduktionsfolgen jedoch nie zu verschiedenen Normalformen f¨ uhren; allerdings kann es geschehen, daß gewisse Reduktionsfolgen nicht abbrechen: 1. KIΩ → I β
2. KIΩ ≡ KI((λx.xx)(λx.xx)) → KIΩ → . . . β
β
Von besonderem Interesse sind deshalb solche Reduktionsstrategien, bei denen garantiert ist, daß die Reduktion eines gegebenen λ-Terms mit seiner Normalform terminiert, sofern diese existiert. Wenn man beim Reduzieren eines λ-Terms streng von links nach rechts vorgeht, so wie in 1., dann bleiben die Argumente des am weitesten links stehenden Redex zun¨achst unausgerechnet. Bei der Reduktion dieses Redex kann es sich nun herausstellen, daß gewisse Argumente ganz aus dem λ-Term verschwinden, z.B. Ω in KIΩ. Im Gegensatz zum Vorgehen in 2. verstrickt man sich beim Reduzieren von links nach rechts nicht in die Berechnung von Teiltermen ohne Normalform, die f¨ ur die Normalform des gesamten λ-Terms ohne Bedeutung sind. Ein Redex A ist links von einem Redex B in einem Term M , wenn A und B an verschiedenen Stellen in M stehen, und es gilt:
2.2 Der λ-Kalk¨ ul
B
A
oder
M
A
B
51
M
Definition 2.2-17 ∗
Eine Reduktionsfolge M → N heißt Normalfolge, wenn bei jedem Reduktiβ
onsschritt das am weitesten links stehende Redex reduziert wird. ∗
Eine Reduktionsfolge M → N heißt Standardreduktionsfolge, wenn das Reduβ
zieren von links nach rechts erfolgt. Im Unterschied zu einer Normalfolge k¨ onnen also bei einer Standardreduktionsfolge gewisse Redices u ¨bersprungen werden. Praktisch geht man wie folgt vor: Nach der Reduktion eines Redex R werden alle Lambdas von Redices markiert, die links von R sind. Redices mit indiziertem Lambda d¨ urfen nicht reduziert werden. Vorhandene Indizierungen ¨ andern sich nicht durch Reduktionen. Beispiele 2.2-13 1. I(Ia) ≡ I((λxx)a) → Ia β
2. I(Ia) ≡ (λxx)(Ia) → Ia β
Beide Folgen sind Standardreduktionsfolgen, 2. ist die eindeutig bestimmte Normalfolge. 3. I(Ia) ≡ I((λxx)a) → Ia → a β
β
4. I(Ia) ≡ (λxx)(Ia) → Ia → a β
β
Wenn man auf Normalform reduziert, dann gibt es genau eine Standardreduktionsfolge, hier 4), die auch die Normalfolge ist. Die Folge 3. ist keine Standardreduktionsfolge. Es gilt folgender ber¨ uhmter Satz von Curry und Feys (Curry 1958): Satz 2.2-15 (Standardisierungstheorem) ∗
Zu jeder Reduktionsfolge M → N gibt es eine Standardreduktionsfolge. β
52
2 Mathematische Grundlagen
Einen Beweis findet man in Curry (Curry 1958); einen etwas einfacheren in Barendregt (Barendregt 1981). F¨ ur die praktische Anwendung ist folgende Variante von großer Bedeutung: Satz 2.2-16 ∗
Sei N Normalform von M , dann existiert eine Normalfolge M → N . β
F¨ ur Programmiersprachen, die auf dem λ-Kalk¨ ul beruhen wie z.B. die LISPa¨hnlichen Sprachen, w¨ are es also korrekt, Parameter¨ ubergabe mit call by na” me“ durchzuf¨ uhren, da dies der Standard Reduktionsfolge entspricht. H¨aufig wird jedoch eine effizientere Art der Parameter¨ ubergabe vorgesehen: call by ” value“. Dabei wird in einem Redex das Argument auf Normalform reduziert, ehe es im Rumpf der λ-Abstraktion substituiert wird. Falls die Reduktion des Arguments nicht terminiert, kann das betrachtete Redex nicht gem¨aß call ” by value“ reduziert werden. Leider f¨ uhrt diese Strategie nicht immer zu der Normalform eines λ-Terms, wie ii) des eingangs genannten Beispiels zeigt. In diesem Sinne ist call by value“ weniger m¨ achtig als call by name“. Plotkin ” ” (Plotkin 1975) hat Programmiersprachen mit call by value“ bzw. mit call ” ” by name“ aus der Sicht des λ-Kalk¨ uls n¨ aher untersucht. Die Reduktion von λ-Termen u ¨ber Normalfolgen, d.h. gem¨aß call by na” me“ l¨ aßt sich durch eine Auswertungsfunktion eval : Λ → Λ definieren. Da zu f¨ uhren wir eine Hilfsfunktion eval : Λ → Λ ein, durch die das am weitesten links stehende Redex eines λ-Terms reduziert wird, und ein Pr¨adikat β : Λ → {Bool}, mit dem festgestellt wird, ob ein Term ein Redex enth¨alt. β(a) β(x) β(λxM ) β((M N )) β((M N )) eval [a] eval [x] eval [λxM ]
= f alse = f alse = β(M ) = true = β(M ) ∨ β(N ) =a =x = λx [M ] ⎧ eval x ⎨ SubN [L] (∗) eval [(M N )] = (M eval [N ]) ⎩ (eval[M ]N ) eval[eval [M ]] eval[M ] = M
f¨ ur a ∈ C f¨ ur x ∈ V f¨ ur f¨ ur f¨ ur f¨ ur
M ≡ λxL M ≡ λxL a∈C x∈V
falls M ≡ λxL falls M ≡ λxL und β(M ) = f alse falls M ≡ λxL und β(M ) = true falls β(M ) = true sonst
Man erh¨ alt eine Auswertungsfunktion evaly : Λ → Λ gem¨aß call by value“, ” wenn man in der Definition von eval bei (∗) wie folgt ersetzt: eval [(M N )] = Subxevaln [N ] [L] falls M ≡ λxL.
2.2 Der λ-Kalk¨ ul
53
Das Thema Reduktionsstrategie“ ist eines der delikaten Kapitel im λ-Kalk¨ ul. ” F¨ ur die genannten, ganz einfachen Strategien hat man Implementationstechniken auf Rechenmaschinen, wodurch ihre besondere Bedeutung erkl¨arbar ist. Bei der generellen Untersuchung von Reduktionsstrategien im λ-Kalk¨ ul hat man es mit sehr komplexen Problemen zu tun. Dazu geh¨ort z.B. die Charakterisierung von korrekten“ Strategien bez¨ uglich der Semantik in einem gege” benen Modell des λ-Kalk¨ uls. Eine Einf¨ uhrung in die Problematik, allerdings u ¨ber die Theorie der rekursiven Programmschemata, findet man in Vuillemin (Vuillemin 1974). Standardartikel sind von Wadsworth (Wadsworth 1976), Hyland (Hyland 1975), Berry (Berry 1977). Ein weiteres Problem sind optimale“ Strategien, die von Levy (Levy 1977) ” untersucht wurden. Man sucht Strategien, die eine minimale Anzahl von βReduktionen ben¨ otigen, wenn ein Term zu einer Normalform reduziert werden kann. Weder call by name“ noch call by value“ sind optimal. ” ” 2.2.8 Angewandter λ-Kalku ¨l Nach Church spricht man vom reinen λ-Kalk¨ ul, wenn die Menge der Konstanten leer ist (C = ∅) und vom angewandten λ-Kalk¨ ul, falls C = ∅ ist (Definition 2.2-1). Da im reinen λ-Kalk¨ ul alle nat¨ urlichen Zahlen N0 und alle partiell rekursiven Funktionen f : N0n → N dargestellt werden k¨onnen, ist der angewandte λ-Kalk¨ ul f¨ ur die Theorie von geringerem Interesse. F¨ ur die praktische Anwendung bei Programmiersprachen ist es jedoch sinnvoll, einige Elemente aus C als Daten und einige als Basisfunktionen zu verwenden, so daß man z.B. folgendermaßen reduzieren kann: + 2 3 → 5 oder cons A B → (A.B) Solche Reduktionen mit Elementen aus C heißen δ-Reduktionen. Es handelt sich also nicht um eine spezielle Reduktion, sondern um eine Menge von Reduktionen. Es k¨ onnen jedoch nicht alle Terme, vom praktischen Standpunkt aus betrachtet, als sinnvoll angesehen werden, z.B. +(3 = 5)7. Definition 2.2-18 Im angewandten λ-Kalk¨ ul ohne η-Reduktion sind δ-Reduktionen definiert als eine Menge von Regeln M → N , wobei f¨ ur M, N ∈ Λ gilt: δ
1. M ≡ aM1 . . . Mn mit a ∈ C und M1 , . . . , Mn ∈ Λ. 2. Alle Teilterme von M sind irreduzibel bez¨ uglich β- und δ-Reduktionen. 3. F V (M N ) = ∅. Wenn Mi kein δ-Redex ist, dann ist Mi in β-Normalform. δ-Reduktionen erfolgen stets nach allen β-Reduktionen. Die Church-Rosser-Eigenschaft gilt auch
54
2 Mathematische Grundlagen
f¨ ur den angewandten λ-Kalk¨ ul mit δ-Reduktionen. Wegen 2. der Definition m¨ ussen δ-Reduktionen mit call by value“ als Reduktionsstrategie angewandt ” werden. Definition 2.2-19 Im angewandten λ-Kalk¨ ul ist ein Term M genau dann in Normalform, wenn M kein β- bzw. δ-Redex enth¨ alt. ¨ Uber den angewandten λ-Kalk¨ ul hat man einen bequemeren Zugang zur Definition einer formalen Semantik von Programmiersprachen als im reinen λKalk¨ ul. Landin (Landin 1965) hat gezeigt, daß man die Semantik von ALGOL60 u ul formal definieren kann. LISP kann man ¨ber den angewandten λ-Kalk¨ unmittelbar als angewandten λ-Kalk¨ ul definieren (Perrot 1979, Greussay 1977, Simon 1980). Die Hinzunahme von Konstanten zum reinen λ-Kalk¨ ul hat gravierende Konsequenzen f¨ ur Scott’s Modelle des reinen λ-Kalk¨ uls. Die Einzelheiten entnehme man Stoy (Stoy 1977), Plotkin (Plotkin 1975) und auch Gordon (Gordon 1973). Als weiterf¨ uhrende Lekt¨ ure u ul, ist das Werk ¨ber den klassischen λ-Kalk¨ von Curry et.al. (Curry et.al. 1958) u ul und kombinatorische ¨ber den λ-Kalk¨ Logik zu empfehlen. Das Buch von Stoy u ¨ber denotationelle Semantik (Stoy uhrung in Scott’s Modelle des 1977) enth¨ alt eine sehr leicht verst¨ andliche Einf¨ λ-Kalk¨ uls. Eine Einf¨ uhrung in das Thema denotationelle Semantik“ findet ” man auch in dem Buch von Gordon (Gordon 1979). In Meyer (Meyer 1982) werden Modelle f¨ ur den hier vorgestellten ungetypten λ-Kalk¨ ul angegeben, die auf elementaren, algebraischen Konzepten beruhen. Logische Aspekte stehen in dem ebenfalls gut zu lesenden B¨ uchlein von Hindley et.al. (Hindley et.al. 1972) im Vordergrund. Der Tagungsband von B¨ohm (B¨ohm 1975) gibt eine ¨ Ubersicht u uhen vielf¨ altigen Aktivit¨ aten zum Thema λ-Kalk¨ ul. Das ¨ber die fr¨ Standardwerk u ul von Barendregt (Barendregt 1981) ist leider, ¨ber den λ-Kalk¨ wie auch die Mehrzahl der Originalartikel, keine leichte Lekt¨ ure! 2.2.9 Typsysteme Der bisher vorgestellte Lambda-Kalk¨ ul, auch der angewandte Lambda-Kalk¨ ul, kennen kein Typkonzept. Viele h¨ ohere Programmiersprachen verlangen jedoch eine Typspezifikation f¨ ur die verwendeten Variablen und Identifikatoren. Schon die ersten h¨ oheren Programmierspachen wie FORTRAN, ALGOL-60 oder PASCAL sahen vordefinierte Typen wie Felder, Records, Listen usw. vor. Die erste nennenswerte Bedeutung erlangende Sprache, die auf den Prinzipien der funktionalen und applikativen Programmierparadigmen beruhte, die Sprache LISP, besaß nur einen h¨ oheren Datentyp, die Liste. In diesem Sinne ist LISP eine schwachgetypte Sprache. Viele neuere funktionale und applikative Sprachen besitzen jedoch ein wesentlich ausgepr¨agteres Typsystem. Hierdurch
2.2 Der λ-Kalk¨ ul
55
wird der Programmierer gezwungen, sich detailliertere Gedanken u ¨ber die zugrundliegenden Datenstrukturen und die auf sie anzuwendenden Operationen zu machen. Konzeptionelle Fehler k¨ onnen damit fr¨ uhzeitig, d. h. bereits zur ¨ Ubersetzungszeit, erkannt und somit schnell eliminiert werden. Als Bindeglied zwischen mathematischen Beweissystemen und dem Lambda-Kalk¨ ul dient die Typtheorie bzw. dienen die verschiedenen Typsystemen. So zeigt die sog. Curry-Howard-Korrespondenz den Zusammenhang zwischen logischen Aussagen und Typen, sowie zwischen formalen Beweisen und Termen. F¨ ur die in Programmiersprachen auftretenden Typisierungskonzepte existieren eine Reihe von theoretischen Konzepten. Zu ihnen geh¨oren u. a. der getypte Lambda-Kalk¨ ul, die Theorie der abstrakten Datentypen oder das Hindley-Milner-Typsystem. Hierbei existieren zwischen diesen Konzepten sehr enge Zusammenh¨ ange, auf die hier nicht detailliert eingegangen werden kann. 2.2.9.1 Getypter λ-Kalku ¨l Der getypte Lambda-Kalk¨ ul wurde urspr¨ unglich als eine Variante des klassischen Lambda-Kalk¨ uls eingef¨ uhrt. Viele Autoren betrachten heute jedoch den getypten Lambda-Kalk¨ ul als eine fundamentelle Theorie und den ungetypten klassischen Lambda-Theorie als einen Spezialfall. Von dem getypten Lambda-Kalk¨ ul existieren verschiedene Varianten. Er wurde zun¨ achst eingef¨ uhrt, um Beziehungen zu anderen Kalk¨ ulen der klassischen Logik zu untersuchen. Erst sp¨ ater trat seine Relevanz f¨ ur getypte Sprachen in den Vordergrund der Betrachtungen. Der einfache getypte Lambda-Kalk¨ ul wurde bereits von Alonzo Church im Jahre 1940 eingef¨ uhrt und besitzt lediglich einen oder mehrere Basistypen sowie Funktionstypen der Form δ → τ . Der sogenannte T-Kalk¨ ul (oder TSystem) besteht aus einer Erweiterung um nat¨ urliche Zahlen und um h¨ohere Rekursionen. Der F-Kalk¨ ul (F-System) umfaßt dar¨ uber hinaus Polymorphie. Dar¨ uber hinaus existieren weitere Varianten wie LF-Systeme, PCF-Systeme oder Systeme mit abh¨ angigen Datentypen . Ferner wurden Kalk¨ ule mit Untertypen betrachtet, d.h. ein Typ A wird als Untertyp von Typ B angesehen, wenn alle Ausdr¨ ucke des Typs A auch vom Typ B sind. Anstelle von einem Grundtyp k¨ onnen auch mehrere Grundtypen angenommen werden. Definition 2.2-20 Das getypte Lambda-Alphabet λ ist die kleinste Menge, welche die folgenden Bedingungen erf¨ ullt: 1. ∨σ = {∨σ0 , ∨σ1 , . . .} ⊆ λ f¨ ur jeder σ ∈ Typ, Menge der getypten Variablen 2. λ ∈ λ , Abstraktor
56
2 Mathematische Grundlagen
3. (, ) ∈ λ , Klammern 4. • ∈ λ , Punkt Auf der Basis des getypten Lambda-Aphabets lassen sich jetzt getypte LambdaAusdr¨ ucke einf¨ uhren. Definition 2.2-21 1. Ein getypter Lambda-Ausdruck w = w1 w2 . . . wn ist ange n u ¨ber dem getypten Lambda-Alphabet eine Zeichenreihe der L¨ mit λ ∀i∈[1,n] i wi ∈ λ 2. Die Menge Λσ der Lambda-Ausdr¨ ucke des Typs σ ist die kleinste Menge, welche induktiv definiert ist durch i. Ist x eine Variable des Typs σ, dann ist x ∈ Λσ (Atome) ii. Sind X ∈ Λσ→τ und Y ∈ Λσ sind, dann ist (XY ) ∈ Λτ (Applikation) iii. Sind Y ∈ Λτ und x eine Variable aus λ τ des Typs σ, dann ist (λx.Y ) ∈ Λσ→τ (Abstraktion) Fast allen Varianten des getypten Lambda-Kalk¨ uls ist gemeinsam, daß sie in strengem Sinne normalisierend sind, d. h. alle Berechnungen terminieren. Der einfache getypte Lambda-Kalk¨ ul besteht aus einem (oder mehreren) Basistypen und Funktionstypen. Formal l¨ aßt er sich definieren durch Definition 2.2-22 Die Menge Typ der Typen ist die kleinste Menge, sodaß 1. 0 ∈ Typ 2. Wenn σ, τ ∈ Typ sind, dann ist auch (σ → τ ) ∈ Typ. Hierbei heißt 0 Grundtyp oder Basistyp und die Typen der Form (σ → τ ) werden als Funktionstypen oder zusammengesetzte Typen bezeichnet. Auf der Basis von Typen lassen sich jetzt getypte Mengen einf¨ uhren. Definition 2.2-23 Sei X eine beliebige Menge. Dann sind die Mengen Xσ durch Induktion auf σ ∈ Typ definiert durch 1. X0 = X 2. Xσ→τ = XτXσ (Menge der mengentheoretischen Funktionen von Xσ nach Xτ ) a) Ist X ∈ Λσ→τ und ist Y ∈ Λσ , dann ist (X Y ) ∈ Λτ (Applikation) b) Ist Y ∈ Λτ und ist x eine Variable aus Σλ des Typs σ, dann ist (λ x • Y ) ∈ Λσ→τ (Abstraktion)
2.2 Der λ-Kalk¨ ul
57
3. Die Menge Λτ der getypte Lambda-Ausdr¨ ucke ist die Menge
{Λσ | σ ∈ Typ } ¨ Ublicherweise bezeichnet man mit den Großbuchstaben M σ , N σ , Lσ , . . . beliebige Lambda-Ausdr¨ ucke aus Λσ mit einem beliebigen σ ∈ Typ. Mit den Kleinbuchstaben xσ , y σ , z σ , . . . bezeichnet man beliebige Variablen von Λσ mit einem beliebigen σ ∈ Typ. Aufbauend auf diesen Definitionen l¨ aßt sich nun eine Theorie (in logischem Sinne) einf¨ uhren. Definition 2.2-24 Die Theorie λτ besteht aus Formeln der Form M σ = N σ , wobei M σ , N σ ∈ Λσ mit einem beliebigen σ ∈ Typ, und ist durch die folgenden Axiome und Ableitungsregeln axiomatisiert: (I) (λxa · M β )a→β N a = M β [xa / N a ] (β-Konversion). (II.1) M a = M a . (II.2) Wenn M a = N a , dann N a = M a . (II.3) Wenn M a = N a , N a = La , dann M a = La . (II.4) Wenn M a→β = N a→β , dann M a→β Z β = N a→β Z β . (II.5) Wenn M a = N a , dann Z a→β M β = Z a→β N β . (II.6) Wenn M β = N β , dann λxa .M β = λxa .N β (ξ-Regel) Die Beweisbarkeit in λτ einer Gleichung (einer Formel der Form M σ = N σ , wobei M σ , N σ ∈ Λσ mit einem beliebigen σ ∈ Typ) wird mit λτ M σ = N σ bezeichnet. Wenn λτ M σ = N σ , dann heißen M σ und N σ konvertierbar. ¨ Die Aquivalenzaxiome und die Ableitungsregeln sind hierbei typbeschr¨ankt, d. h. nur sie sind nur zwischen Ausdr¨ ucken des gleichen Typs definiert. Die u uls der vorher¨brigen Definitionen des klassischen ungetypten Lambda-Kalk¨ gehenden Abschnitte k¨ onnen entsprechend modifiziert u ¨bernommen werden. 2.2.9.2 Abstrakte Datentypen Eine weitere Grundlage f¨ ur Typkonzepte bei Sprachen ist die Theorie der abstrakten Datentypen. Generell bezeichnet ein Datentyp die Zusammenfassung von Objektmengen mit den darauf definierten Operationen. Dabei werden durch den Datentyp unter Verwendung einer so genannten Signatur ausschließlich die Namen dieser Objekt- und Operationsmengen spezifiziert. Ein so spezifierter Datentyp besitzt noch keine Semantik. Die Definition des Begriffs Datentyp“ ist jedoch nicht eindeutig. Eini” ge Autoren bezeichnen damit die Zusammenfassung konkreter Wertebereiche und darauf definierten Operationen zu einer Einheit. Zur Unterscheidung wird
58
2 Mathematische Grundlagen
f¨ ur diese Datentypen in der Literatur auch der Begriff Konkreter Datentyp verwendet. ¨ Der gedankliche Ubergang von der formalen Definition zu der im Umfeld von Programmiersprachen verwendeten Definition konkreter Datentypen geschieht dabei u uhrung einer Semantik zu den formal ¨ber die sukzessive Einf¨ spezifizierten Namen der Objekt- und Operationsmengen. Die Konkretisierung der Operationsmenge f¨ uhrt zu Abstrakten Datentypen beziehungsweise Algebren. Mit der weiteren Konkretisierung der Objektmenge ergibt sich der Konkrete Datentyp. Eine Signatur ist allgemein betrachtet ein Paar bestehend aus Sorten und Operationen. Hierbei sind mit Sorten Namen f¨ ur Objektmengen bezeichnet und die Operationen repr¨ asentieren Manipulationen auf diesen Mengen. Als Beispiel sei der (konkrete) Datentyp integer betrachtet: Integer Sorten int Operationen empty : − > int + : int × int − > int : int × int − > int End Integer Dies ist eine Signatur f¨ ur einen angenommenen Datentyp Integer, auf dem nur zwei Operationen + und - (neben der Erzeuger-Operation“) erlaubt sind. ” Die einzige Sorte nennen wir int. Die Operation empty dient zur Erzeugung eines int-Elementes. Die Operation + und - sind jeweils zweistellig und liefern jeweils wiederum ein Element der Sorte int. Wichtig ist, dass es sich hier um eine rein syntaktische Spezifikation handelt. Was ein int ist, wird nirgendwo definiert. Hierzu m¨ usste noch eine Zuordnung des Sortennamens zu einer Menge erfolgen. Eine sinnvolle Zuordnung w¨ are in diesem Fall etwa die Menge der nat¨ urlichen Zahlen. Auch u ¨ber die Arbeitsweise der Operationen ist nichts weiter ausgesagt als ihre Stelligkeit und ihr Ergebnis. Ob das +-Symbol der Arbeitsweise der Summenoperation entspricht, wird hier nicht festgelegt. Das w¨ahre auch v¨ ollig unm¨ oglich, da nicht einmal bekannt ist, ob die Operation auf den nat¨ urlichen Zahlen arbeitet. Derartige Zuordnungen fallen in den Bereich der Semantik. Eine um die Semantik erweiterte Spezifikation k¨onnte daher folgendermaßen aussehen: Integer Sorten int Operationen empty : − > int + : int × int − > int : int × int − > int Mengen int = NAT Funktionen empty = {} + : int × int
2.2 Der λ-Kalk¨ ul
59
entspricht der Summe zweier Zahlen aus NAT - : int × int entspricht der arithmetischen Differenz zweier Zahlen. End Integer Durch diese Spezifikation wird allerdings der u ¨bliche Umfang einer Signaturbeschreibung bereits u ¨berschritten. Eine derartige umfangreiche Spezifikation wird oft als Algebra bezeichnet. Viele Sprachen besitzen eine Menge an vordefinierten Datentypen, z.B. ¨ Ganze-Zahlen, Fließkommazahlen oder Zeichenreihen. Ublicherweise unterscheidet man zwischen elementaren, zusammengesetzten und abstrakten Datentypen. Elementare (einfache) Datentypen k¨ onnen nur ein Datum des entsprechenden Wertebereichs aufnehmen. Sie besitzen eine festgelegte Anzahl von Werten (Diskretheit) sowie eine fest definierte Ober- und Untergrenze (Endlichkeit). Es wird dabei zwischen ordinalen und nichtordinalen Datentypen unterschieden. Ordinale Datentypen sind dadurch gekennzeichnet, dass ihrem Wert eine eindeutige Ordnungsnummer zugeordnet ist, das heißt jeder außer dem ersten Wert besitzt genau einen direkten Vorg¨ anger und jeder außer dem letzten Wert besitzt genau einen direkten Nachfolger (geordnete Menge). Ordinale Datentypen sind abz¨ ahlbar und umfassen z.B. die Menge der ganzen Zahlen, der nat¨ urlichen Zahlen, der Booleschen Wahrheitswerte oder die Menge der zul¨ assigen Zeichen ( char“). ” Nichtordinale Datentypen sind dadurch gekennzeichnet, dass ihre Werte keine eindeutigen Vorg¨ anger bzw. Nachfolger besitzen. Nichtordinale Datentypen sind z. B. die Menge der Gleitkommazahlen oder die Menge der Festkommazahlen. Zusammengesetzte Datentypen sind ein Datenkonstrukt, welches aus einfachen oder ordinalen Datentypen besteht. Theoretisch k¨onnen sie beliebig komplex werden. Oft werden sie auch als h¨ ohere Datentypen, komplexere Datentypen oder Datenstukturen bezeichnet. Beispiele hierf¨ ur sind Felder oder Records. Als Abstrakte Datentypen bezeichnet man spezielle Datentypen, deren Operationen genau spezifiziert sind, die jedoch im Gegensatz zu den elementaren und zusammengesetzten Datentypen nicht an einen konkreten Wertebereich gebunden sind. Die Bindung an den Wertebereich findet erst durch die konkrete Auspr¨ angung des abstrakten Datentypen statt. Bei den abstrakten Datentypen ist die Abgrenzung zu dem Begriff Datenstrukturen schwierig, da die Datentypen je nachdem, ob der Schwerpunkt der Betrachtung auf den Daten oder auf den darauf definierten Operationen liegt, Datenstruktur oder Abstrakter Datentyp genannt werden.
60
2 Mathematische Grundlagen
2.2.9.3 Polymorphie Das Wort Polymorphie stammt aus dem Griechischen und bedeutet so viel wie Vielgestaltigkeit“. Prinzipiell bezeichnet es bei Programmiersprachen die ” M¨ oglichkeit, einem Wert oder einem Namen mehrere Datentypen zuzuordnen. Da Datentypen auch die Definition von Operatoren verlangen, sei als Beispiel der Operator + (Additionen) betrachtet. Er wird u ¨blicherweise dazu verwendet um sowohl zwei ganze Zahlen (integer) zu addieren, als auch um zwei Gleitkommazahlen (real) zu addieren. Strenggenommen handelt es sich um zwei verschiedene Operatoren, die nur den gleichen Namen besitzen. Im ersten Fall ist + ein Operator vom Typ integer × integer − > integer und im zweiten Fall ist + ein Operator vom Typ real × real − > real. Viele Programmiersprachen erlauben auch eine Verkn¨ upfung von integerTypen mit real-Typen verm¨ oge des +-Operators, d.h. in diesen F¨allen besitzt der +-Operator noch den Typ integer × real − > real bzw. real × integer − > real. Hier muß vor der Ausf¨ uhrung des +-Operators noch eine Typ-Anpassung (Coercion) erfolgen, in dem integer nach real umgewandet wird. ¨ Eine derartige Uberladung eines Operators, in dem man seinen Namen variabel f¨ ur Verkn¨ upfungen unterschiedlicher Datentypen verwendet, nennt man Polymorphie. Hierbei k¨ onnen unter dem gleichen Namen zusammengefaßte Verkn¨ upfungen intern v¨ ollig unterschiedlich implementiert sein. Inzwischen unterscheidet man verschiedene Arten der Polymorphie. Als einer der ersten unterteilte Christopher Strachey die Polymorphie in parametrische und Ad-hoc-Polymorphie. Allerdings gibt seine relativ formlose Beschreibung dieser Unterscheidung zu den Vermutung Anlaß, daß er unter Adhoc-Polymorphie lediglich u ¨berladen“ meinte. Von Luca Cardelli und Peter ” Wegner stammt eine feinere Unterteilung der Polymorphie. Sie unterscheiden die Arten - universelle Polymorphie - parametrische Polymorphie - Inklusions-/Vererbungspolymorphie - Ad-hoc-Polymorphie - Coercion Universelle Polymorphie unterscheidet sich von Ad-hoc-Polymorphie in mehreren Aspekten. Bei Ad-hoc-Polymorphie kann ein Name oder ein Wert nur ¨ endlich viele verschiedene Typen besitzen. Diese sind zudem zur Ubersetzungszeit bekannt. Universelle Polymorphie dagegen erlaubt es, unendlich viele Typen zuzuordnen.
2.2 Der λ-Kalk¨ ul
61
Ein weiterer Unterschied liegt darin, dass die Implementierung einer universell polymorphen Funktion generell gleichen Code unabh¨angig von den Typen ihrer Argumente ausf¨ uhrt, w¨ ahrend ad-hoc-polymorphe (also u ¨berladene) Funktionen abh¨ angig von den Typen ihrer Argumente unterschiedlich implementiert sein k¨ onnen. Variablen sind u ¨berladen, wenn unterschiedliche Funktionen mit dem selben Namen verbunden sind. Beispielsweise ist der Operator + in vielen Programmiersprachen von vorn herein u ¨berladen. So k¨onnen mit ihm, wie bereits oben erw¨ ahnt, ganze Zahlen und Gleitkommazahlen addiert werden. Oft wird er auch zur Stringkonkatenierung verwendet. Innerhalb des gleichen Programms k¨ onnen daher die Ausdr¨ ucke 12 + 10 3.14 + 2,5 “Adam“ + “ “ + “Eva“ auftreten. Coercion ist eine Art implizite Typumwandlung, um zum Beispiel Argumente einer Funktion in die von der Funktion erwarteten Typen umzuwandeln. ¨ Coercion ist mit dem Uberladen eng verkn¨ upft und Unterschiede sind f¨ ur den Programmierer nicht unbedingt gleich ersichtlich. W¨ urde in einem Programm z.B. der Ausdruck 3.14 + 1 auftreten, so w¨ urde dieser Ausdruck intern zun¨achst in den Ausdruck 3.14 + 1.0 transformiert werden und erst danach die Anwendung des +-Operators erfolgen. Parametrisierte Polymorphie repr¨ asentiert Typen, deren Definitionen Typvariablen enthalten. Dabei k¨ onnen Funktionen in der Regel einen oder mehrere Typparameter haben und die zul¨ assigen Typen besitzen in der Regel eine gemeinsame Struktur. Die polymorphen Funktionen haben implizite oder explizite Typparameter, die den Typ des Funktionsergebnisses f¨ ur jede Anwendung der Funktion festlegen. Bei der Inklusionspolymorphie unterscheidet man zwischen KompilationsPolymorphie und Laufzeit-Polymorphie: Kompilationszeit-Polymorphie bedeutet, dass zur Kompilationszeit der Typ des Objekts die aufgerufene Funktion (auch Methode“ genannt) be” stimmt werden kann. Laufzeit-Polymorphie bedeutet, dass erst zur Laufzeit bestimmt wird, welche Methode aufzurufen ist (sp¨ ate Bindung). Es kann also vom Programmlauf abh¨ anging sein, welche Methode in Anwendung kommt. Die LaufzeitPolymorphie ist einer der wichtigsten Bestandteile der objektorientierten Programmierung und wurde zuerst in der Programmiersprache Smalltalk realisiert.
62
2 Mathematische Grundlagen
2.3 Kombinatorische Logik 2.3.1 Einleitung Die kombinatorische Logik ist eine Theorie, die mit dem λ-Kalk¨ ul sehr eng verwandt ist. Sie wurde jedoch davon unabh¨ angig schon in den zwanziger Jahren von Sch¨ onfinkel (Sch¨ onfinkel 1924) und Curry (Curry 1930) entwickelt. Curry beabsichtigte, u ¨ber die kombinatorische Logik eine alternative Grundlage der Mathematik zu entwickeln. Bis heute ist jedoch noch kein befriedigendes, logisches System dieser Art bekannt. Curry’s Paradoxon (Kap. 2.3-4) gibt einen Eindruck von den Problemen, auf die man st¨oßt. In der Informatik m¨ ochte man mit der kombinatorischen Logik im Prinzip dieselbe Art von Rechnungen wie mit dem λ-Kalk¨ ul durchf¨ uhren k¨onnen, n¨amlich das Reduzieren von Termen. In der kombinatorischen Logik ben¨otigt man dazu jedoch nicht das Konzept der durch λ gebundenen Variablen, um das Substituieren im Term zu steuern. Es entfallen alle in Kapitel 2.2.2.1 genannten technischen Schwierigkeiten mit der Substitution. Insbesondere ist das systematische Umbenennen von Variablen nicht erforderlich. Weiterhin beruht das Rechnen auf ganz wenigen, u ¨beraus simplen Regeln. Diese Eigenschaften tragen wesentlich dazu bei, daß das maschinelle Reduzieren von kombinatorischen Termen in besonders einfacher Weise durchgef¨ uhrt werden kann. Daher werden Kombinatoren in zunehmendem Maße als Zwischensprache bei der Implementation von funktionalen Programmiersprachen verwendet. Als Preis f¨ ur die leichte Handhabung hat man allerdings die intuitive Klarheit der Notation von λ-Termen zu opfern. 2.3.2 Elementare Begriffe Kombinatorische Terme haben im allgemeinen die Bedeutung von monadischen Funktionen. Als Verkn¨ upfung zweier Terme X und Y hat man nur die Applikation X Y der Funktion X auf das Argument Y . Eine Abstraktion wie im λ-Kalk¨ ul gibt es nicht. Definition 2.3-1 Sei V eine abz¨ ahlbare Menge von Variablen, und sei C eine abz¨ahlbare Menge von Konstanten mit den speziellen Konstanten K, S ∈ C, den Basiskombinatoren . Die Elemente von A = V ∪ C heißen Atome. Die Menge CT der kombinatorischen Terme ist induktiv definiert durch: 1. A ⊂ CT 2. X, Y ∈ CT → (X Y ) ∈ CT. Ein kombinatorischer Term, in dem nur Basiskombinatoren als Atome auftreten, heißt Kombinator.
2.3 Kombinatorische Logik
63
Die syntaktischen Konventionen f¨ ur die Notation von Termen entsprechen denen im λ-Kalk¨ ul (Kap. 2.2.2.1). Die intuitive Bedeutung der Basiskombinatoren kann durch ihr funktionales Verhalten beschrieben werden: Kax = a Sf gx = f x(gx) entsprechend f (x, g(x))
.
Der Kombinator I ≡ SKK wirkt als Identit¨ at: If ≡ SKKf = Kf (Kf ) = f
.
Der Kombinator B ≡ S(KS)K bewirkt die Komposition von Funktionen: Bf gx ≡ S(KS)Kf gx = KSf (Kf )gx = S(Kf )gx = Kf x(gx) = f (gx) entsprechend f (g(x)). Der Kombinator W ≡ SS(KI) verdoppelt sein zweites Argument: W f x ≡ SS(KI)f x = Sf ((KI)f )x = Sf Ix = f x(Ix) = f xx entsprechend f (x, x). Der Begriff der freien Variablen und der Substitution vereinfachen sich gegen¨ uber den Definitionen 2.2-2 bzw. 2.2-4 wesentlich. Definition 2.3-2 Die Menge F V (M ) der freien Variablen von M ∈ CT ist induktiv definiert durch: F V (x) = {x}, F V (a) = ∅ f¨ ur x ∈ V und a ∈ C F V (M N ) = F V (M ) ∪ F V (N ) . ur alle (freien) Vorkommen von Die Substitution SubxN [M ] von N ∈ CT f¨ x ∈ V in M ∈ CT ist induktiv definiert durch f¨ ur x ∈ V SubxN [x] = N x SubN [a] = a f¨ ur a ∈ A \ {x} SubxN [(M1 M2 )] = (SubxN [M1 ]SubxN [M2 ]). Die kombinatorische Logik CL ist eine Theorie mit Gleichungen M = N f¨ ur Terme M, N ∈ CT , jedoch ohne logische Verkn¨ upfungen. Beweisbarkeit in CL wird mit CL M = N bezeichnet, kurz M = N .
64
2 Mathematische Grundlagen
Definition 2.3-3 Terme M, N ∈ CT heißen gleich, M = N , wenn diese Aussage aus folgenden Axiomen und Deduktionsregeln ableitbar ist: Axiome: KXY = X SXY Z = XZ(Y Z) M =M Deduktionsregeln: M =N → M = N, N = L → M =N → M =N →
N M MZ ZM
=M =L = NZ = ZN
In CL hat man als Reduktionsbegriff: Definition 2.3-4 Ein Term P wird zu einem Term P reduziert, P → P , wenn einer der folw
genden beiden Reduktionsschritte auf P oder auf einen Unterterm von P angewandt wird: KXY → X w
SXY Z → XZ(Y Z)
.
w
Terme KXY bzw. SXY Z heißen Redex. Ein Term X ist genau dann in Normalform, wenn er keine Redices enth¨ alt. Ein Term X hat eine Normalform ∗ genau dann, wenn es einen Term N mit X → N gibt und N ist in Normalw form. Die Reduktion → heißt schwache Reduktion (engl. weak), da SK zwar eine w
Normalform ist, der zugeh¨ orige λ-Term (λxyz.xz(yz))(λxy.x) jedoch nicht. In der Literatur findet man den Kombinator I manchmal als dritten Basiskombinator. Die Identifizierung I = SKK ist n¨amlich nur bei schwacher Reduktion zul¨ assig. In Hindley (Hindley 1972) findet man den Begriff der starken Reduktion, der es erforderlich macht, die Definition 2.3-3 um IX = X bzw. Definition 2.3-4 um IX → X zu erweitern. w
Satz 2.3-1 Es gilt M = N genau dann, wenn M aus N durch eine endliche, eventuell leere Folge von Reduktionsschritten und invertierten Reduktionsschritten hervorgeht.
2.3 Kombinatorische Logik
65
Diese Aussage ist einsichtig, da die Relation “ = “ die kleinste, durch → w ¨ erzeugte Aquivalenzrelation ist, die mit der Applikation in CT vertr¨aglich ist. Beispiele 2.3-1 1. F¨ ur C ≡ S(BBS)(KK) gilt CXY Z ≡ S(BBS)(KK)XY Z = BBSX(KKX)Y Z = BBSXKY Z = B(SX)KY Z = SX(KY )Z = SX(KY )Z = XZ(KY Z) = XZY 2. Am Beispiel eines Fixpunktkombinators soll gezeigt werden, wie man zu einem vorgegebenen funktionalen Verhalten einen Kombinator finden kann. Gesucht wird also Y ∈ CT mit Y F = F (Y F )
.
Nach Satz 2.2-12 gen¨ ugt es, einen Kombinator A mit dem funktionalen Verhalten von λxy.y(xxy) zu finden. Dann ist Y = AA ein Fixpunktkombinator. Es soll f¨ ur Argumente a und f also gelten: Aaf = f (aaf )
.
ugen von Kombinatoren f ganz nach rechts Man versucht nun durch Einf¨ zu bekommen: Aaf = SI(aa)f
.
Die Verdopplung kann man durch W ausdr¨ ucken: Aaf = SI(W la)f
.
Die Rechtsklammerung mit a versucht man durch B aufzubrechen: Aaf = B(SI)(W I) a f A ≡ B(SI)(W I) . Der Yc aus Satz 2.2-12 entsprechende Kombinator ist Yc = W S(BW B) (Curry 1958). Damit kombinatorische Terme im funktionalen Verhalten den λ-Termen entsprechen, ben¨ otigt man eine Simulation der λ-Abstraktion durch K und S. Definition 2.3-5 Sei M ein kombinatorischer Term und x eine Variable. Die Abstraktion [x]M von M nach x ist ein Term, der induktiv definiert ist durch
66
2 Mathematische Grundlagen
1. [x]x
≡I
2. [x]M ≡ KM
falls x ∈ / F V (M )
3. [x]U x ≡ U
falls x ∈ / F V (U )
4. [x]U V ≡ S([x]U )([x]V ) falls 2. und 3. nicht gelten. Anders als im λ-Kalk¨ ul ist hier die Abstraktion [x] kein syntaktischer Bestandteil von Termen, sondern [x] ist als ein Operator zu verstehen, der einen Term M in den mit [x]M bezeichneten Term u uhrt. ¨berf¨ Beispiele 2.3-2 1. [x]xy ≡ S([x]x)([x]y) ≡ SI(Ky) ([x]xy)N ≡ SI(Ky)N = IN (KyN ) = N (KyN ) = N y = SubxN [xy] 2. [x, y]yx
≡ [x]([y]yx) ≡ [x](SI(Kx)) ≡ S([x](SI))([x](Kx)) ≡ S(K(SI))K
Das Zusammenspiel von Abstraktion und Applikation ist wie im λ-Kalk¨ ul. Satz 2.3-2 Seien M, N Terme und x eine Variable. Dann gilt ([x]M )N = SubxN [M ]
.
Beweis: Es wird ([x]M )x = M induktiv gezeigt, woraus die Behauptung durch Substitution von N f¨ ur x folgt. 1. M ≡ x : ([x]x)x ≡ Ix = x 2. M ≡ a mit a ∈ A und a ≡ x : ([x]a)x ≡ Kax = a 3. M ≡ U V : Induktionsvoraussetzung ist ([x]U )x = U und ([x]V )x = V. a) x ∈ / F V (M ) : ([x]M )x ≡ KM x = M b) x ∈ / F V (U ) und V ≡ x : ([x]M )x ≡ ([x]U x)x ≡ U x ≡ M
2.3 Kombinatorische Logik
67
c) Weder a) noch b) gelten ([x]M )x ≡ S([x]U )([x]V )x = ([x]U )x(([x]V )x) = UV = M Es sind verschiedene Varianten der Abstraktion m¨oglich. Naheliegend ist es, 2. auf M ∈ A zu beschr¨ anken oder 3. ganz wegzulassen. F¨ ur die praktische An¨ wendung beim Ubersetzen von funktionalen und applikativen Programmiersprachen kommt es darauf an, relativ kurze Terme zu erhalten, die m¨oglichst effizient reduziert werden k¨ onnen. Unter diesem Gesichtspunkt sind die Definitionen der Abstraktion von Turner (Turner 1979a) interessant. ¨ Als Ubertragung auf den Fall mehrerer Variablen hat man Definition 2.3-6 Sei M ∈ CT und seien x1 , . . . , xm ∈ V. Die Abstraktion von M nach x1 , .., xm ist definiert durch [x1 , . . . xm ]M = [x1 ]([x2 ](. . . ([xm ]M ). . . ))
.
Satz 2.3-3 F¨ ur N1 , .., Nm ∈ CT gilt: m ([x1 , . . . xm ]M )N1 . . . Nm , = SubxN11,...,x ,...,Nm [M ], m wobei SubxN11,...,x ,...,Nm die simultane Ersetzung von x1 , . . . xm durch N1 , . . . , Nm in M bezeichnet.
Beweisskizze: Zu zeigen ist ([x1 , . . . , xm ]M )x1 . . . xm = M durch Satz 2.3-2 und Induktion u ¨ber m. Ohne Beweis seien zitiert (Hindley 1972): Satz 2.3-4 1. F¨ ur x ∈ / F V (M ) gilt: [x]Subyx [M ] ≡ [y]M
.
2. F¨ ur y ∈ / F V (N ) gilt: SubxN [[y]M ] = [y]SubxN [M ]
.
Satz 2.3-5 ∗
∗
∗
∗
w
w
w
w
Wenn U → X und U → Y gilt, dann existiert Z mit X → Z und Y → Z. In CL gilt also die Church-Rosser-Eigenschaft und analog zu Satz 2.2-3 gelten die Korollare:
68
2 Mathematische Grundlagen
1. Seien Y und Y Normalformen von X, dann gilt Y = Y . ∗
∗
w
w
2. Sei X = Y , dann existiert Z mit X → Z und Y → Z. ∗
3. Sei X = Y und Y in Normalform, dann gilt X → Y . w
4. Sei X = Y . Dann gilt entweder a) X und Y haben dieselbe Normalform oder b) X und Y haben beide keine Normalform. 5. Seien X, Y Normalformen mit X = Y , dann gilt X ≡ Y . 2.3.3 Die Beziehung zum λ-Kalku ¨l Mit Hilfe der Abstraktion f¨ ur kombinatorische Terme ist es klar, wie man standardm¨ aßig zwischen CL und dem λ-Kalk¨ ul zu u ¨bersetzen hat. Definition 2.3-7 1) ()λ (x)λ (K)λ (P Q)λ 2) ()CL
: CT → Λ
ist induktiv definiert durch
≡x ≡ K, (S)λ ≡ S ≡ (P )λ (Q)λ
x Atom K, S ∈ Λ wie in Beispiel 2.2-7 definiert
: Λ → CT
ist induktiv definiert durch
≡x x Atom (x)CL (M N )CL ≡ (M )CL (N )CL (λxM )CL ≡ [x](M )CL Durch Induktion beweist man: Satz 2.3-6 Wenn in CL gilt M = N , dann gilt im λ-Kalk¨ ul (M )λ = (N )λ . Die Umkehrung dieses Satzes gilt jedoch nicht, da im λ-Kalk¨ ul gilt X = Y → λxX = λxY
,
jedoch dies nicht in der kombinatorischen Logik. Aus Sxyz = xz(yz) folgt z.B. nicht [x](Sxyz) = [x](xz(yz)), da [x]Sxyz = S(SS(Ky))(Kz) und [x](xz(yz)) ≡ S(SI(Kz))(K(yz)) verschiedene Normalformen sind. Dieses Beispiel zeigt weiterhin, daß der in CL eingef¨ uhrte Gleichheitsbegriff nicht extensional ist im Gegensatz zu dem im λ-Kalk¨ ul. Es gilt zwar f¨ ur V ∈ CT
2.3 Kombinatorische Logik
69
([x]Sxyz)V = ([x]xz(yz))V , aber [x]Sxyz = [x]xz(yz)
.
Man kann nun eine extensionale Gleichheit und eine zugeh¨orige Reduktion definieren, so daß diese Variante der kombinatorischen Logik und der λ-Kalk¨ ul ur die meisten ¨aquivalent sind (Curry 1958, Hindley 1972, Barendregt 1981). F¨ Anwendungen in der Informatik gen¨ ugt jedoch die kombinatorische Logik ohne extensionale Gleichheit. Bei der Standard¨ ubersetzung ()λ bleiben Normalformen nicht erhalten, da S und K im λ-Kalk¨ ul eine Feinstruktur haben. SK ∈ CT ist eine Normalform, aber (SK)λ ≡ SK ∈ Λ nicht: ∗
SK ≡ (λxyz.xz(yz))K → λyz.Kz(yz) → λyz.z β
β
Es kann sogar geschehen, daß der u ¨bersetzte Term gar keine Normalform besitzt, wie z.B. P ≡ S(KΔ)(KΔ) mit Δ ≡ SII. Es gilt Δ ≡ [x]xx und P ≡ [y]ΔΔ und daraus folgt (P )λ ≡ λy.((λx.xx)(λx.xx)) ≡ λy.Ω. Bei der Standard¨ ubersetzung ()CL bleiben Reduktionsschritte nicht erhalten, da der Abstraktionsoperator [ ] β-Redices aufbricht. Es gilt zwar λx.II → λx.I, β
aber [x]II ≡ S(KI)(KI) ist mit → nicht zu [x]I ≡ KI reduzierbar. w
Die Beziehungen zwischen λ-Kalk¨ ul und kombinatorischer Logik sind also komplexer als man es auf den ersten Blick erwartet. Wenn man auf eine informelle Benutzung von Kombinatoren st¨ oßt, sollte man sich vergewissern, ob es sich dabei um λ-Terme oder um kombinatorische Terme handelt. 2.3.4 Anwendungen der kombinatorischen Logik Wenn man gewisse Kombinatoren als Darstellungen von nat¨ urlichen Zahlen ausw¨ ahlt und andere als Funktionen auf nat¨ urlichen Zahlen interpretiert, dann kann man beweisen: Satz 2.3-7 Jede primitiv-rekursive Funktion f kann durch einen Term Φf ∈ CL kombinatorisch definiert werden. Mit einer Darstellung von undefiniert“ durch hat keine Normalform“ gilt: ” ” Satz 2.3-8 Jede partiell rekursive Funktion f kann durch einen Term Φf ∈ CL im schwachen Sinne (mit → als Reduktion) kombinatorisch definiert werden. w
70
2 Mathematische Grundlagen
Beweise f¨ ur beide S¨ atze findet man in Hindley (Hindley 1972). Man kann alle berechenbaren Funktionen ohne die Benutzung von gebundenen Variablen definieren. F¨ ur das Programmieren ben¨otigt man im Prinzip eigentlich gar keine gebundenen Variablen, seien sie nun Funktionsparameter oder in einem Deklarationsteil definiert. Es ist klar, daß man die kombinatorische Logik selbst nicht als eine f¨ ur Menschen praktikable Programmiersprache ansehen kann. Sie ist wegen der Abwesenheit von gebundenen Variablen, der u ur S, K und I und wegen der einfachen, ¨beraus simplen Rechenregeln f¨ syntaktischen Struktur von Termen in besonderer Weise f¨ ur die unmittelbare maschinelle Verarbeitung durch Hardware geeignet. Damit ist jedoch nicht die sequentielle Verarbeitung mit einem konventionellen von Neumann-Rechner gemeint, sondern paralleles Reduzieren von Termen durch eine Vielzahl einfach aufgebauter, miteinander kommunizierender Rechnerelemente. Rechner mit kombinatorischer Logik als Maschinensprache werden in Kapitel 4.3.3 vorgestellt. Andererseits kann man funktionale und applikative Programmiersprachen in die kombinatorische Logik u ¨bersetzen, so daß diese die gleiche Rolle spielt, wie u ¨bliche Assemblersprachen (Turner 1980). In Robinet (Robinet 1980 b) wird am Beispiel von Backus’ FP-Systemen gezeigt, wie eine formale Sprachdefinition u ¨ber die kombinatorische Logik erfolgen kann. In B¨ ohm (B¨ ohm 1982) und v.d. Poel (Poel 1980) findet man weitere, mehr theoretische Resultate, die Beziehungen zwischen applikativer Programmierung und kombinatorischer Logik aus mathematischer Sicht aufzeigen. Die urspr¨ unglichen Erwartungen von Curry und Church an die kombinatorische Logik bzw. an den λ-Kalk¨ ul, n¨ amlich ein logisches System zu liefern, das man als Grundlage der Mathematik ansehen kann, konnten durch Curry und Church nicht erf¨ ullt werden (Curry 1958). Einen Eindruck von den Problemen vermittelt Curry’s Paradoxon. Analog zu Satz 2.2-5 kann man einen Term ⊃ ∈ CT definieren mit den fol¨ genden Eigenschaften der logischen Implikation. Der Ubersichtlichkeit halber benutzen wir ihn hier in Infixnotation. Das Zeichen bedeutet die Ableitbarkeit in einem logischen System, also hier in CL. ( X ⊃ Y, X) ⇒ Y (P) (X ⊃ (X ⊃ Y )) ⊃ (X ⊃ Y ) (I) X⊃X (I’) ( X = Y, X) ⇒ Y (E) Die Regel (P) ist der klassische modus ponens“. Die Regel (E) setzt die ” Gleichheit von Termen und g¨ ultige Zusicherungen in Beziehung. Die Axiomenschemata (I) und (I’) ergeben sich aus der folgenden Eigenschaft, die jeder vern¨ unftige Begriff einer Implikation“ erf¨ ullen sollte: ” (( A1 , . . . , An , X) Y ) ⇒ (( A1 , . . . , An ) (X ⊃ Y ))
(D)
2.3 Kombinatorische Logik
71
Sei A ein beliebiger Term. Nach dem Fixpunkttheorem gibt es einen Fixpunkt Φ mit Φ = (Φ ⊃ A)
(F ).
Man zeigt nun die Ableitbarkeit von A, d.h. die G¨ ultigkeit der Zusicherung A: Φ⊃Φ mit Φ ⊃ (Φ ⊃ A) mit Φ⊃A mit Φ mit A mit
I’ F und E I und P F und E P und den beiden vorangehenden Zusicherungen
Damit hat man Curry’s Paradoxon, denn A kann auch die Darstellung von f alse in CL sein. So erreicht man also keine Erweiterung um logische Begriffe. Ein konsistentes logisches System auf der Basis von λ-Termen bzw. Kombinatoren ist zu erwarten, wenn man folgendermaßen abschw¨acht: -
Einschr¨ ankung der Terme X, f¨ ur die die Abstraktion λx.X ein Term ist, damit das Fixpunkttheorem nicht gilt,
-
Einschr¨ ankung der Terme, f¨ ur die (D) gilt.
Es sind solche konsistenten Systeme bekannt; ihr Nachteil ist jedoch die extreme Schw¨ ache in der Ausdrucksf¨ ahigkeit von mathematischen Sachverhalten. Das Standardwerk u ¨ber kombinatorische Logik ist Curry (Curry 1958, Curry 1971). Unbedingt zu empfehlen sind Hindley (Hindley 1972, Hindley 1986). Die Einbindung der kombinatorischen Logik in den λ-Kalk¨ ul wird in Barendregt (Barendregt 1981) dargestellt.
3 Programmiersprachen
3.1 FP-systeme 3.1.1 Einleitung Die FP-Systeme wurden von J.W. Backus, einem der V¨ater von FORTRAN, entwickelt. Die Abk¨ urzung FP steht f¨ ur Functional Programming. Er stellte ¨ sie der breiten Offentlichkeit anl¨ aßlich seiner Turing Award Lecture“ bei der ” ACM Jahrestagung 1977 vor (Backus 1978). Sie basieren auf den von Backus bereits Anfang der 70er Jahre entwickelten Red Languages“ (Reduction Lan” guages) (Backus 1972, 1973). Die FP-Systeme sind keine in allen Einzelheiten festgelegten Programmiersprachen, sondern sie sind im Sinne einer Schemasprache als Konzept f¨ ur gewisse funktionale Sprachen zu verstehen. Aus diesem Grund werden sie etwas ausf¨ uhrlicher dargestellt, da dieses Schema sich auf die wesentlichsten Elemente einer (im strengen Sinne) funktionalen Sprache beschr¨ankt. Hierdurch lassen sich die Vorteile dieses Programmierstils, wie z.B. einfache Korrektheitsbeweise f¨ ur Programme, leicht demonstrieren. Auch der Zusammenhang mit der kombinatorischen Logik kann unmittelbarer gezeigt werden. Im Unterschied zur kombinatorischen Logik und zum λ-Kalk¨ ul wird in FPSystemen bei der Definition von Funktionen ein spezieller Datenbereich, die sogenannten Objekte, zugrunde gelegt. Objekte sind entweder atomar oder endliche Folgen von Objekten. Atomare Objekte k¨onnen z.B. die ganzen Zahlen sein. Auch ist die Benutzung von h¨ oheren Funktionalen u ¨ber diesem Datenbereich auf die fest vorgegebenen Funktionale beschr¨ankt. H¨ohere Funktionale k¨ onnen in der Sprache selbst nicht ausgedr¨ uckt werden. Beim Programmieren in einem FP- System stehen dem Benutzer die standardm¨aßig definierten Basisfunktionen zur Verf¨ ugung, die Objekte in Objekte abbilden, und er kann zur Definition von Funktionen die genannten Funktionale einsetzen. Dar¨ uber hinaus k¨ onnen Funktionen durch rekursive Gleichungssysteme definiert werden. Alle Funktionen, die in einem FP-System definierbar sind, bilden Objekte in Objekte ab. Die einzige Verkn¨ upfung in einem FP-System ist die Applikation von Funktionen auf Objekte. Der genaue Sprachumfang, der durch die
74
3 Programmiersprachen
vorhandenen atomaren Objekte, Basisfunktionen und Funktionale festgelegt wird, ist von dem speziellen System abh¨ angig. Zusammen mit den FP-Systemen entwickelt Backus eine Algebra der ” Programme“. Mit ihrer Hilfe ist es m¨ oglich, Eigenschaften von Programmen durch Programmtransformationen nachzuweisen. Die Algebra besteht aus einer Reihe von Gesetzen (laws) und Theoremen. Die Gesetze beschreiben Eigenschaften einfacher Funktionen, die Theoreme dienen zur Behandlung von Gleichungssystemen. In Backus (Backus 1978) wird die G¨ ultigkeit der Gesetze mit einer informellen Argumentation gezeigt, der wir uns hier anschließen werden. In Williams (Williams 1982) wird ihre G¨ ultigkeit bez¨ uglich einer denotationellen Semantik f¨ ur FP-Systeme bewiesen. Eine Formalisierung mit den algebraischen Methoden aus der Theorie der abstrakten Datentypen f¨ uhrt zu demselben Resultat (Dosch 1983). Wesentlich ist, daß die Gesetze und Theoreme nicht Aussagen u ¨ber FP-Programme machen, sondern funda¨ mentale Aquivalenzen zwischen FP-Programmen beschreiben. Somit k¨onnen Beweise in Backus’ Algebra der Programme auf dem Niveau und in der Notation von Programmen selbst erfolgen. Die reichhaltige Struktur in dieser Algebra beruht auf einer geschickten Abstimmung zwischen den Objekten, Basisfunktionen und vorgegebenen Funktionalen in einem FP-System (Dosch 1983). Beim Entwurf eines neuen FP-Systems ist also insbesondere die Auswirkung auf die Algebra der Programme zu ber¨ ucksichtigen. Jedes System hat seine spezielle Algebra. FP-Systeme stellen nur ein grobes Ger¨ ust f¨ ur eine den Bed¨ urfnissen der Praxis angepaßte funktionale Programmiersprache dar. So ist zum Beispiel außer der Applikation eines FP-Programms auf ein Eingabeobjekt keine weitere M¨ oglichkeit f¨ ur die Ein-/Ausgabe vorgesehen. Ihre Bedeutung haben die FPSysteme jedoch dadurch erlangt, daß sie, neben ihrer Relevanz f¨ ur theoretische Untersuchungen, maßgeblich den funktionalen Programmierstil (im engeren Sinne) gepr¨ agt und viele neuere Entwicklungen von funktionalen Sprachen beeinflußt haben. 3.1.2 Schemasprache Formal ist ein FP-System gegeben durch Beschreibung der Schemasprache. Generell gilt f¨ ur alle FP-Systeme: Definition 3.1-1 Ein FP-System (A, F, F, :) besteht aus den folgenden Komponenten: 1. A ist eine (aufz¨ ahlbare) Menge von atomaren Objekten (mit ⊥ ∈ A), die die Booleschen Atome true und f alse sowie das Sonderatom Nil umfaßt. Die Menge O der Objekte ist induktiv definiert als die kleinste Menge mit der Eigenschaft: O := A ∪ {< x1 , . . . , xn > | n ≥ 1 und xi ∈ O \{⊥}}
3.1 FP-systeme
75
Objekte < x1 , . . . , xn > heißen Folgen oder Sequenzen. Nil bezeichnet die leere Folge <> und ist das einzige Objekt, welches sowohl Atom als auch Folge ist. Undefinierte Objekte werden durch das Symbol ⊥ (“bottom“) dargestellt. Es wird eine strikte Konstruktorfunktion <>: On → O f¨ ur Folgen unterstellt. Eine Funktion f heißt strikt, wenn gilt f (. . . , ⊥, . . . )) =⊥ . 2. F ist eine endliche Menge von Basisfunktionen f | O → O. Alle Basisfunktionen sind total und strikt. 3. F ist eine endliche Menge von Funktionalen (functional forms). Die Argumente der Funktionale sind Funktionen O → O oder Objekte O; ihr Ergebnis ist stets eine Funktion O → O. 4. Aus Basisfunktionen und Objekten lassen sich mit Hilfe der Funktionale Funktionsterme T aufbauen, die Funktionen O → O beschreiben. Die Applikation eines Funktionsterms τ | O → O auf ein Objekt x ∈ O wird in Infixnotation mit τ : x bezeichnet. Funktionsbezeichner f1 , . . . , fn lassen sich weiterhin durch Gleichungssysteme def f1 ≡ τ1 .. . def fn ≡ τn definieren, wobei alle τi Funktionsterme u ¨ber F, F und O bedeuten, in denen onnen. auch die Funktionsbezeichner fi auftreten k¨ Ein FP-Programm τ ist ein Funktionsterm τ ∈ T zusammen mit dem Gleichungssystem f¨ ur die Funktionsbezeichner in τ . Die Applikation eines FP-Programms τ auf ein Eingabeobjekt a ∈ O erfolgt durch τ : a. Dies ist die einzige Stelle, an der ein Benutzer die Applikation : verwenden darf. In einem FP-Programm hat man drei verschiedene Ebenen: die Objekte, die Funktionen zwischen Objekten und die Funktionale, die als Ergebnis Funktionen zwischen Objekten haben. H¨ ohere Funktionalit¨aten sind nicht vorgesehen. Programme werden ausschließlich auf der Ebene der Funktionen durch die fest vorgegebenen Kombinatoren (Funktionale) F gebildet, ohne Benutzung der Applikation. Nur jeweils das gesamte Programm wird auf ein Objekt angewendet. In FP-Systemen wird also wie in der kombinatorischen Logik ohne Variablen f¨ ur Objekte programmiert. J.W. Backus hat 1978 und in nachfolgenden Arbeiten (Backus 1981 a, 1981 b, 1982) diesen funktionalen Stil im engeren Sinne als Alternative zum konventionellen, durch den von NeumannRechner gepr¨ agten Programmierstil propagiert.
76
3 Programmiersprachen
3.1.3 Ein FP-System Dieses Beispiel stammt aus Backus (Backus 1978) und ist dort sowie in Nachfolgearbeiten gr¨ undlich studiert worden. Mit gewissen Varianten findet man es immer wieder in der Literatur u ¨ber FP-Systeme. H¨aufig wird deshalb dieses spezielle System mit F P identifiziert, wenn von Backus’ FP-Sprache (oder FP-System) die Rede ist. Im folgenden wird die urspr¨ ungliche von Backus angegebene, operational definierte Semantik f¨ ur FP-Systeme benutzt. Objekte, Basisfunktionen und Funktionale werden in einer teilweise informellen Metanotation definiert, indem das Resultat der Applikation auf Objekte und Funktionen angegeben wird. Daraus ergibt sich die extensionale Gleichheit von FP-Programmen (siehe Def. 3.1-3). Eine formale Definition der Semantik von FP-Systemen, speziell dieses ¨ Beispiels, erfolgt in Kapitel 3.1.8 durch Ubersetzung in die kombinatorische Logik. Die Semantik von FP-Systemen ist dar¨ uber hinaus durch andere formale Konzepte abgesichert worden, z.B. in Williams (Williams 1982), Dosch (Dosch 1983), Bellegarde (Bellegarde 1984). Definition 3.1-2 1. Atomare Objekte: A := {true, f alse} ∪ {N il} ∪ Idf ∪ Z Idf sei eine Menge von Identifikatoren (mit true, f alse, N il ∈ / Idf ), Z die Menge der ganzen Zahlen. 2. Basisfunktionen: Identit¨ at id : x := x Der Wert der Applikation id : x wird in der Metanotation durch := x erkl¨ art; := x ist also kein Bestandteil von FP! Im folgenden sei stets x ∈ O. Selektion von links S1 : x := x ≡ < x1 , . . . , xn > ∧ n ≥ 1 → x1 ; ⊥ S2 : x := x ≡ < x1 , . . . , xn > ∧ n ≥ 2 → x2 ; ⊥ .. . S1 wird auch hd (head) genannt. In der Metanotation bedeutet p1 → e1 ; . . . ; pn → en ; en+1 den bedingten Ausdruck if p1 then e1 else if p2 then . . . if pn then en else en+1 .
3.1 FP-systeme
77
Rest (Tail) t1 : x := x ≡ < x1 > → N il; x ≡ < x1 , . . . , xn > ∧ n ≥ 2 →< x2 , . . . , xn >; ⊥ Pr¨ ufung auf atomares Objekt atom : x := x ∈ A →true; x = ⊥→ f alse; ⊥ Gleichheitspr¨ ufung eq : x := x ≡ < y, z > ∧ y = z → true; x ≡ < y, z > ∧ y = z → f alse; ⊥ Pr¨ ufung auf leere Sequenz null : x := x ≡ N il → true; x =⊥→ f alse; ⊥ Invertieren einer Sequenz reverse : x := x ≡ N il → N il; x ≡ < x1 , . . . , xn >→< xn , . . . , x1 >; ⊥ Distribution von links distl : x := x ≡ < y, N il > → N il; x ≡< y, < z1 , . . . , zn >> →<< y, z1 >, . . . , < y, zn >>; ⊥ Distribution von rechts distr : x := x ≡ < N il, y >→ N il; x ≡<< y1 , . . . , yn >, z > →<< y1 , z >, . . . , < yn , z >>; ⊥ Addition, Subtrakion, Multiplikation, Division + : x := x = < y, z > ∧ y, z ∈ Z → y + z; ⊥ − : x := x = < y, z > ∧ y, z ∈ Z → y − z; ⊥ ∗ : x := x = < y, z > ∧ y, z ∈ Z → y ∗ z; ⊥ ÷ : x := x = < y, z > ∧ y, z ∈ Z∧ z = 0 → y ÷ z; ⊥ Pr¨ ufung auf Null eq0 : x := x ≡ 0 → true; x = 0 ∧ x ∈ Z → f alse; ⊥ Transposition trans : x := x ≡ < N il, . . . , N il > → N il; x ≡ < x1 , . . . , xn > →< y1 , . . . , ym >; ⊥ j j mit xi ≡ < x1i , . . . , xm i > und yj ≡ < x1 , . . . , xn >, 1 ≤ i ≤ n, 1 ≤ j ≤ m
78
3 Programmiersprachen
Konjunktion and : x := x ≡ < true, true > → true; x ≡ < true, f alse > → f alse; x ≡ < f alse, true > → f alse; x ≡ < f alse, f alse > → f alse; ⊥ Disjunktion und Negation Entsprechend zur Konjunktion definiert. Links anf¨ ugen apndl : x := x ≡ < y, N il > → < y >; x ≡ < y, < z1 , . . . , zn >> → < y, z1 , . . . , zn >; ⊥ Rechts anf¨ ugen apndr : x := x ≡ < N il, z > → < z >; x ≡ << y1 , . . . , yn >, z > → < y1 , . . . , yn , z >; ⊥ Selektion von rechts 1r : x := x ≡ < x1 , . . . xn > → xn ; ⊥ 2r : x := x ≡ < x1 , . . . xn > ∧ n ≥ 2 → xn−1 ; ⊥ .. . Rest von rechts (Sequenz ohne letztes Element) tlr : x := x ≡ < x1 > → N il; x ≡ < x1 , . . . , xn > ∧ n ≥ 2 → < x1 , . . . , xn−1 >; ⊥ Rotation nach links rotl : x := x ≡ N il → N il; x ≡ < x1 > → < x1 >; x ≡ < x1 , . . . , xn > ∧ n ≥ 2 → < x2 , . . . , xn , x1 >; ⊥ Rotation nach rechts rotr : := x ≡ N il → N il; x ≡ < x1 > → < x1 >; x ≡ < x1 , . . . , xn > ∧ n ≥ 2 → < xn , x1 , . . . , xn−1 >; ⊥ 3. Funktionale: Seien p, f, g beliebige Funktionen und x, y beliebige Objekte. a) Funktionale u ¨ber Funktionen Komposition (f ◦ g) : x := f : (g : x)
3.1 FP-systeme
79
Konstruktion [f1 , . . . , fn ] : x := n = 0 ∧ x = ⊥ → N il; n ≥ 1 ∧ f1 : x =⊥ ∧ · · · ∧ fn : x =⊥ → < f1 : x, . . . , fn : x >; ⊥ Bedingung (p → f : g) : x := (p : x) = true → f : x; (p : x) = f alse → g : x; ⊥ Als Abk¨ urzung f¨ ur (p1 → f1 ; (p2 → f2 ; g)) dient p1 → f1 ; p2 → f2 ; g. Reduktion /f : x := x ≡ < x1 > → x1 ; x ≡ < x1 , . . . , xn > ∧ n ≥ 2 → f : < x1 , /f : < x2 , . . . , xn >>; ⊥ Anwendung auf alle (Apply to all) αf : x := x ≡ N il → N il; x ≡ < x1 , . . . , xn > ∧ f : x1 = ⊥ ∧ · · · ∧ f : xn = ⊥ → < f : x1 , . . . , f : xn >; ⊥ Bedingte Wiederholung (Diese Regel ist als Termsetzungsregel zu verstehen.) (while p f ) : x := (p : x) = true → (while p f ) : (f : x); (p : x) = f alse → x; ⊥ b) Funktionale u ¨ber Objekten Konstante Funktion x: y := y = ⊥ →⊥ ; x
x∈O
Bin¨ ar nach Un¨ ar (bu f x) : y := x = ⊥ ∧ y = ⊥ → f :< x, y >; ⊥ Dieses FP-System ist redundant, da sich einige Basisfunktionen bzw. Funktionale durch andere ausdr¨ ucken lassen. Die Frage nach einem ¨aquivalenten, minimalen“ System wollen wir hier jedoch nicht weiter verfolgen. ” Entsprechende Untersuchungen findet man in Gram (Gram 1980 b). 3.1.4 Beispiele fu ¨ r FP-Programme ¨ 1. Gesucht ist ein Programm, das die L¨ ange einer Folge bestimmt. Uberlegen wir zun¨ achst, wie ein kleines Kind diese Aufgabe l¨ost: Es zeigt mit dem Finger auf das erste Element der Folge und sagt eins“, zeigt dann mit ”
80
3 Programmiersprachen
dem Finger auf das n¨ achste Element und sagt und noch eins sind zwei“, ” usw. . Es identifiziert also jedes Element der Folge mit Eins und addiert die Einsen auf. Setzen wir diesen Algorithmus in ein FP-Programm um! Das Identifizieren eines jeden Elements einer Folge erfolgt durch das Programm α1, und das Aufaddieren bewirkt das Programm /+ . Die Komposition dieser beiden Programme liefert das gesuchte Programm: def l¨ ange ≡ (/+) ◦ (α1) Die Anwendung auf die Eingabe < 1, 2, 3 > bewirkt: l¨ ange : < 1, 2, 3 > ≡ (/+) ◦ (α1) : < 1, 2, 3 > = (/+) : ((α1) :< 1, 2, 3 >) = /+ : < 1 : 1, 1 : 2, 1 : 3 > = /+ : < 1, 1, 1 > = + : < 1, + : < 1, 1 >> = + : < 1, + : < 1, /+ : < 1 >>> = + : < 1, + : < 1, 1 >> = + : < 1, 2 > =3 Bei Eingaben, die keine Folgen sind, liefert das Programm ⊥ . 2. Gesucht ist ein Programm, welches feststellt, ob ein bestimmtes Objekt x Element einer nicht leeren Folge y ist. Dies trifft zu, falls der Vergleich von x mit den Elementen der Folge mindestens einmal true liefert. Ein Vergleich von x mit jedem Element von y liefert das Programm (α eq)◦ distl. Seine Anwendung liefert eine Folge, deren Elemente aus den Wahrheits¨ werten true und f alse bestehen. Uberpr¨ ufen, ob eine Folge von Wahrheitswerten mindestens einmal true enth¨ alt, bewirkt das Programm /or. Das gesuchte Programm ist dann: def member
≡ (/or) ◦ (α eq) ◦ distl
member : < 1, < 2, 1 >> ≡ (/or) ◦ (α eq) ◦ distl : < 1, < 2, 1 >> = (/or) : ((α eq) : (distl : < 1, < 2, 1 >>)) = (/or) : (α eq) : << 1, 2 >, < 1, 1 >>) = (/or) : < f alse, true > = true Wenn die zweite Komponente des Arguments von member keine nicht leere Folge ist, dann liefert das Programm ⊥ . 3. Gesucht ist eine Funktion, die das innere Produkt (Skalarprodukt) zweier Vektoren bestimmt. Das innere Produkt zweier Vektoren x =< x1 , . . . , xn > und y =< y1 , . . . , yn >
3.1 FP-systeme
ist definiert durch
n
81
xi ∗ yi ,
i=1
wobei n ≥ 1 die Dimension der Vektoren ist. Aus der Eingabe << x1 , . . . , xn >, < y1 , . . . , yn >> bildet man durch trans zun¨ achst die Folge << x1 , y1 >, . . . , < xn , yn >> der Paare < xi , yi > multipliziert deren Komponenten durch (α∗) zu < x1 ∗ y1 , . . . , xn ∗ yn > und addiert die Produkte mittels /+ def IP = (/+) ◦ (α∗) ◦ trans Die Applikation auf die Vektoren < 1, 2, 3 > und < 6, 5, 4 > liefert: IP : << 1, 2, 3 >, < 6, 5, 4 >> = (/+) ◦ (α∗) ◦ trans : << 1, 2, 3 >, < 6, 5, 4 >> = (/+) ◦ (α∗) : << 1, 6 >, < 2, 5 >, < 3, 4 >> = (/+) : < ∗ : < 1, 6 >, ∗ : < 2, 5 >, ∗ : < 3, 4 >> = (/+) : < 6, 10, 12 > = + : < 6, + : < 10, 12 >> = + : < 6, 22 > = 28 . 4. Im folgenden soll die Konstruktion eines FP-Programms an einem etwas komplexeren Beispiel gezeigt werden. Gesucht ist ein Programm, welches als Ergebnis das Produkt zweier Matrizen liefert. Das Produkt einer l × m-Matrix α = (αij )1≤i≤l,1≤j≤m mit einer m × nMatrix β = (βjk )1≤j≤m,1≤k≤n ergibt eine l×n-Matrix γ = (γik )1≤i≤l,1≤k≤n m mit γik = αij βjk , anschaulich j=1
⎛ α11 ⎜α21 ⎜ ⎜ .. ⎝ . αl1
⎞ ⎛ ⎞ β11 β12 · · · β1n α12 · · · α1m ⎜ ⎟ α22 · · · α2m ⎟ ⎟ ⎜ β21 β22 · · · β2n ⎟ .. .. ⎟ × ⎜ .. .. .. ⎟ = . . ⎠ ⎝ . . . ⎠ αl2 · · · αlm βm1 βm2 · · · βmn
0α ∗ β + α ∗ β + . . . + α 11 11 12 21 1m ∗ βm1 | {z } B γ11 B Bα ∗ β + α ∗ β + . . . + α B 21 11 22 21 2m ∗ βm1 B| {z } B γ21 B B B . . B . B Bα ∗ β + α ∗ β + ... + α 11 21 l2 lm ∗ βm1 @ l1 | {z } γl1
α11 ∗ β12 + α12 ∗ β22 + . . . + α1m ∗ βm2 · · ·1 | {z } C γ12 C α21 ∗ β12 + α22 ∗ β22 + . . . + α2m ∗ βm2 · · ·C C C | {z } C γ22 C C C . . C . C αl1 ∗ β12 + αl2 ∗ β22 + . . . + αlm ∗ βm2 · · ·C A | {z } γl2
Man ben¨ otigt zun¨ achst eine Darstellung von Matrizen als FP-Objekte. Dazu ordnet man die einzelnen Zeilen nebeneinander an. Betrachten wir
82
3 Programmiersprachen
als Beispiel die Matrix α: << α11 , α12 , ..., α1m >, < α21 , α22 , ..., α2m >, ..., < αl1 , αl2 , ..., αlm >> . Diese Anordnung bewirkt, daß man durch den Selektor Si einfach auf die i-te Zeile zugreifen kann, w¨ ahrend der Zugriff auf Spalten relativ kompliziert ist. Zu entwickeln ist ein Programm, welches auf das Argument < α, β > angewandt, als Ergebnis α × β liefert. Schritt 1: Betrachtet man die Matrizenmultiplikation, so sieht man, daß stets die i-te Zeile der Matrix α mit der k-ten Spalte der Matrix β verkn¨ upft wird. Wegen der gew¨ ahlten Darstellung der Matrizen als Zeilenfolge ist es sinnvoll, die Matrix β zu transponieren, so daß man bei ihr auf die Spalten direkt zugreifen kann. Dabei l¨ aßt man die Matrix α unver¨andert. Man erh¨ alt das Programm [Sl, trans ◦ S2], f¨ ur das gilt: [Sl, trans ◦ S2] : < αβ > = < S1 : < α, β >, trans ◦ S2 : < α, β >> (Konstruktion) = < α, trans ◦ S2 : < α, β >> (Selektor) = < α, trans : (S2 : < α, β >) > (Komposition) = < α, trans : β > (Selektor) = < α, β T > (Transposition) mit
⎛
⎞ β11 β21 · · · βm1 ⎜ β12 β22 · · · βm2 ⎟ ⎜ ⎟ βT = ⎜ . .. .. ⎟ ⎝ .. . . ⎠ β1n β2n · · · βmn Schritt 2: Wegen Schritt 1 k¨ onnen nun die Zeilen der Matrix α mit den Zeilen der Matrix β T skalar multipliziert werden. Zur Vorbereitung faßt man daher zun¨ achst jede Zeile der Matrix α mit der Matrix β T zusammen. Dies bewirkt die Basisfunktion “distr“. Zusammen mit Schritt 1 entsteht distr ◦ [S1, trans ◦ S2]. Man erh¨ alt:
3.1 FP-systeme
83
distr ◦ [S1, trans ◦ S2] : < α, β > = distr : ([S1, trans ◦ S2] : < α, β >) (Komposition) = distr : < α, β T > (Schritt 1) = <<< α11 , α12 , . . . , α1m >, β T >, << α21 , α22 , . . . , α2m >, β T >, · · · > . Schritt 3: Man muß jetzt daf¨ ur sorgen, daß jede Zeile der Matrix α mit jeder Zeile der Matrix β T zusammengefaßt wird. Das geschieht mit Hilfe der Funktion distl“. Betrachten wir als Beispiel << α11 , α12 , . . . , α1m >, β T > . ” distl : << α11 , α12 , . . . , α1m > β T ≡ distl : << α11 , α12 , . . . , α1m >, < < β11 , β21 , . . . , βm1 >, < β12 , β22 , . . . , βm2 >, · · · >> = <<< α11 , α12 , . . . , α1m >, < β11 , β21 , . . . , βm1 >>, << α11 , α12 , . . . , α1m >, < β12 , β22 , . . . , βm2 >>, .. . > Diese Funktion muß nun noch auf << α21 , α22 , . . . , α2m , β T > usw. angewendet werden. Dies erreicht man durch das Funktional Anwendung auf ” alle“, und man erh¨ alt im Schritt 3 das Programm (α distl) ◦ distr ◦ [S1, trans ◦ S2]
.
Schritt 4: Nach Schritt 3 haben wir ein Objekt der folgenden Gestalt aufgebaut: < < < < α11 , α12 , . . . , α1m >, < β11 , β21 , . . . , βm1 >>, < < α11 , α12 , . . . , α1m >, < β12 , β22 , . . . , βm2 >>, .. . >, < < < α21 , α22 , . . . , α2m >, < β11 , β21 , . . . , βm1 >>, < < α21 , α22 , . . . , α2m >, < β12 , β22 , . . . , βm2 >>, .. . >, .. . >
.
Es sind jetzt genau diejenigen Elemente zu Paaren zusammengefaßt, die durch das innere Produkt verkn¨ upft ein Element der Ergebnismatrix liefern. So erh¨ alt man z.B. aus dem ersten Paar
84
3 Programmiersprachen
<< α11 , α12 , . . . , α1m >, < β11 , β21 , . . . , βm1 >> durch IP : << α11 , α12 , . . . , α1m >, < β11 , β21 , . . . , βm1 >> = α11 ∗ β11 + · · · + α1m ∗ βm1 das Element γ11 der Matrix γ = α×β . Allgemein erh¨alt man das Element γik durch Anwendung von IP auf das Paar << αi1 , . . . , αim >, < β1k , . . . , βmk >>. Da die Paare entsprechend der Matrixstruktur von γ zweifach tiefgeschachtelt sind, wird IP durch das Funktional Anwendung auf alle“ fol” gendermaßen verteilt: (α(α IP )) ◦ (α distl) ◦ distr ◦ [S1, trans ◦ S2]. Durch Applikation auf < α, β > erh¨ alt man zun¨achst das Objekt: < α IP : < < < α11 , α12 , . . . , α1m >, < β11 , β21 , . . . , βm1 >>, < < α11 , α12 , . . . , α1m >, < β12 , β22 , . . . , βm2 >>, .. . <, α IP : < < < α21 , α22 , . . . , α2m >, < β11 , β21 , . . . , βm1 >>, < < α21 , α22 , . . . , α2m >, < β12 , β22 , . . . , βm2 >>, .. . > .. .
,
> und dann unmittelbar die Ergebnismatrix α × β. Zusammengefaßt wird die Matrixmultiplikation also durch folgendes FPProgramm bewirkt: def M M ≡ (α(α IP )) ◦ (α distl) ◦ distr ◦ [S1, trans ◦ S2] def IP ≡ (/+) ◦ (α∗) ◦ trans. An diesem Beispiel zeigen sich die wesentlichen Vorteile der funktionalen Programmierung im engeren Sinne; aber auch die Nachteile speziell von FP: 1. Das Programm MM liefert auch ein korrektes Ergebnis f¨ ur den Fall, daß die Anzahl der Spalten der Matrix α und die Anzahl der Zeilen der Matrix β nicht gleich sind. Als Ergebnis erh¨ alt man in diesem Fall ⊥. Die Applikation von MM auf das FP-Objekt < α, β, γ > liefert nicht ⊥, sondern α × β . Allgemein gilt:
3.1 FP-systeme
85
Bei der Konstruktion von FP-Programmen bildet man zun¨achst die problemorientierte Datenstruktur auf FP-Objekte ab. Diese Repr¨asentationsfunktion ist im allgemeinen nicht surjektiv, d.h. es treten nur FP-Objekte bestimmter Bauart auf. Im Entwurf des Algorithmus setzt man f¨ ur die eingehenden Schritte stets Objekte dieser Teilklasse voraus; unber¨ ucksichtigt bleibt dabei, welches Verhalten er auf den u ¨brigen Objekten liefert. Insofern ist das Ergebnis der Applikation eines FP-Programms auf nicht ” passende“ Objekte eher zuf¨ allig. Wegen der Stetigkeit der FP-Funktionen erh¨ alt man jedoch in vielen F¨ allen als Ergebnis ⊥, so daß allgemein die Behandlung von Randf¨ allen nicht so umfangreich ist wie bei anderen Sprachen. 2. Das Programm besitzt keine Schleife, die explizit durch eine WiederholungsAnweisung bzw. Spr¨ unge angegeben werden muß. Die Rekursion u ¨ber Folgen steht in den Funktionalen /, α, distl usw. . 3. Das Programm ist frei von Variablen u ¨ber Objekten. 4. Das Programm ist als Gleichungssystem aufgebaut und besteht daher aus einfachen Modulen, in diesem Fall aus den Modulen M M und IP . Eine weitere Modularisierung durch funktionale Dekomposition ist durch die Einf¨ uhrung von weiteren Definitionen m¨ oglich, z.B. def def def def def
MM IP H1 H2 H3
≡ ≡ ≡ ≡ ≡
(α(α IP )) ◦ H1 ◦ H2 H3 ◦ trans (α distl) ◦ distr [Sl, trans ◦ S2] (/+) ◦ (α∗)
Man darf aber nicht verschweigen, daß das Programm M M z.B. durch Funktionsprozeduren kodiert, auf von Neumann-Rechnern bez¨ uglich des Speicherbedarfs und auch der Laufzeit nur sehr ineffizient ausf¨ uhrbar ist. Dieses Problem ist bei FP-Programmen und auch bei anderen funktionalen und applikativen Programmen von genereller Natur und bedeutet zur Zeit noch einen wesentlichen Nachteil der funktionalen Programmierung. Folglich werden Algorithmen h¨aufig im Hinblick auf eine effiziente Ausf¨ uhrbarkeit programmiert, und dabei wird die Transparenz des funktionalen und applikativen Programmierstils geopfert. Wenn man jedoch die speziellen Eigenschaften von funktionalen und applikativen Sprachen ausnutzt, kann man auch auf von Neumann-Rechnern zu effizienten Implementationstechniken gelangen (Gabriel 1982, Honschopp 1983, Felgentreu 1984). Es gibt verschiedene Ans¨ atze, die Ausf¨ uhrung von FP-Programmen bzw. allgemeiner von funktionalen und applikativen Programmen so effizient zu gestalten, daß sie mit der von imperativen Programmen vergleichbar wird. Allein durch Programmtransformationen auf der Ebene der FP-Funktionen kann man bereits f¨ ur große Klassen von Programmen Effizienz gewinnen. Da-
86
3 Programmiersprachen
bei strebt man vor allem g¨ unstigere Rekursionsschemata f¨ ur FP-Programme an (Backus 1981, Williams 1982, Kieburtz 1981), aber man kann auch gezielt die Anzahl von Zwischenergebnissen bei der Ausf¨ uhrung minimieren (Bellegarde 1984). Ein vielversprechender Weg zu gr¨ oßerer Effizienz ist der Einsatz von neuen Rechnerarchitekturen, die auf funktionale und applikative Programmiersprachen ¨ ahnlich gut abgestimmt sind wie imperative Sprachen auf von NeumannRechnern. Im Vorgriff auf Kapitel 4.3, sind hier als Beispiel die Datenflußmaschinen (Dennis 1975, Keller 1979), die Reduktionsmaschinen (Berkling 1978, Treleaven 1980) oder die zellularen, baumartigen Rechnernetze (Mago 1979) zu nennen. 3.1.5 Die Algebra der FP-Programme Jedem FP-System l¨ aßt sich eine Algebra der Programme zuordnen, das ist ein algebraischer Kalk¨ ul zur Argumentation mit FP-Programmen. Wir beschr¨ anken uns hier auf eine knappe Einf¨ uhrung in diesen Themenkreis. Eine ausf¨ uhrliche Darstellung findet man in Backus (Backus 1978), Kieburtz (Kieburtz 1981) und Williams (Williams 1982). ¨ In der Algebra der Programme werden Aquivalenzen zwischen FP-Funktionen angegeben, mit deren Hilfe Programmtransformationen durchgef¨ uhrt werden k¨ onnen. Damit lassen sich auf dem Niveau von Programmen selbst Eigenschaften untersuchen, wie z.B. Korrektheit und Terminierung. F¨ ur Argumentationen dieser Art benutzt man u ul, ¨blicherweise einen anderen Kalk¨ z.B. den der Hoare’schen Zusicherungen und nicht die Programmiersprache selbst. Eine Algebra der Programme besteht aus Gesetzen und Theoremen. Die Gesetze geben bez¨ uglich eines semantischen Modells zul¨assige Transformationen zwischen Funktionstermen an. So besagt etwa das Gesetz [f, g] ◦ h = [f ◦ h, g ◦ h], daß f¨ ur alle Funktionen f, g, h | O → O die Konstruktion von f und g komponiert mit h die gleiche Funktion ergibt, wie die Konstruktion von f , komponiert mit h, und g, komponiert mit h. F¨ ur alle Objekte x gilt ([f, g] ◦ h) : x = [f ◦ h, g ◦ h] : x. Dabei gilt als Gleichheitsbegriff f¨ ur FP-Funktionen: Definition 3.1-3 Zwei Funktionen f, g | O → O heißen gleich genau dann, wenn f¨ ur alle Objekte x ∈ O f :x=g:x gilt.
3.1 FP-systeme
87
Die G¨ ultigkeit des oben genannten Gesetzes kann man nun folgendermaßen beweisen: a) f : (h : x) = ⊥ ∧ ([f, g] ◦ h) : x = = = =
g : (h : x) = ⊥ [f, g] : (h : x) < f : (h : x), g : (h : x) > < (f ◦ h) : x, (g ◦ h) : x > [f ◦ h, g ◦ h] : x
b) f : (h : x) = ⊥ ∨ ([f, g] ◦ h) : x = = [f ◦ h, g ◦ h] : x = da oder
g : (h : x) = ⊥ (Komposition) [f, g] : (h : x) ⊥ (Konstruktion) ⊥ (Konstruktion) (f ◦ h) : x = f : (h : x) = ⊥ (g ◦ h) : x = g : (h : x) = ⊥
(Komposition) (Konstruktion) (Komposition) (Konstruktion)
Dieses Gesetz gilt f¨ ur alle Objekte x ∈ O. Es gibt auch Gesetze, die nur f¨ ur Teilmengen von O gelten. Beispielsweise gilt S1 ◦ [f, g] = f nicht f¨ ur Objekte x mit g : x =⊥ . In einem solchen Fall schr¨ ankt man den G¨ ultigkeitsbereich des Gesetzes durch ein Pr¨ adikat p ein und schreibt p →→ f = g, um anzuzeigen, daß f : x = g : x dann f¨ ur ein Objekt x gilt, wenn p : x = true ist. Ein eingeschr¨ ankt g¨ ultiges Gesetz ist true ◦ g →→ S1 ◦ [f, g] = f denn ( true ◦ g) : x = true : (g : x) = ((g : x) = ⊥→ ⊥; true). Die folgende Liste von Gesetzen ist ein Auszug aus Backus (Backus 1978) und bezieht sich auf das FP-System aus Kapitel 3.1.3. Bei anderen Atomen, Basisfunktionen und Funktionalen ergeben sich andere Gesetze. 1. Konstruktion und Komposition 1.1
[f1 , . . . , fn ] ◦ g = [f1 ◦ g, . . . , fn ◦ g]
1.2
(α f ) ◦ [g1 , . . . , gn ] = [f ◦ g1 , . . . , f ◦ gn ]
1.3
1.4
, (n ≥ 2) (/f ) ◦ [g1 , . . . , gn ] = f ◦ [g1 (/f ) ◦ [g2 , . . . , gn ]] = f ◦ [g1 , f ◦ [g2 , . . . , f ◦ [ gn−1 , gn ] . . . ]] (/f ) ◦ [g] = g f ◦ [x, g] = (bu f x) ◦ g
88
3 Programmiersprachen
1.5 Sei Sk ein Selektor mit k ≤ n : (∀i = k, 1 ≤ i ≤ n) : true ◦ fi → → Sk ◦ [f1 , . . . , fn ] = fk true ◦ f1 → → t1 ◦ [f1 ] = N il true ◦ f1 → → t1 ◦ [f1 , . . . , fn ] = [f2 , . . . , fn ]
1.6
1.7
distl ◦ [f, [g1 , . . . , gn ]] = [[f, g1 ], . . . , [f, gn ]] true ◦ f → → distl ◦ [N il] = N il
1.8
apndl ◦ [f, [g1 , . . . , gn ]] = [f, g1 , . . . , gn ] null ◦ g →→ apndl ◦ [f, g] = [f ] [f1 , . . . , fi−1 , ⊥, fi+1 , . . . , fn ] = ⊥
1.9 1.10
apndl ◦ [f ◦ g, (α f ) ◦ h] = (α f ) ◦ apndl ◦ [g.h]
1.11
(/f ) ◦ apndl ◦ [g.h] = f ◦ [g, (/f ) ◦ h]
2.
, (n ≥ 2) , (n ≥ 1)
, (n ≥ 1)
Komposition und Bedingung
2.1
(p → f ; g) ◦ h = p ◦ h → f ◦ h; g ◦ h
2.2
h ◦ (p → f ; g) = p → h ◦ f ; h ◦ g or ◦ [p, not ◦ q] →→ and ◦ [p, q] → f ; and ◦ [ p, not ◦ q] → g; h = p → (q → f ; g); h
2.3
2.3.1 3.
p → (p → f ; g); h = p → f ; h Komposition allgemein true ◦ f →→ x ◦ f = x
3.1 3.1.1
⊥◦f =f ◦⊥ =⊥
3.2
f ◦ id = id ◦ f = f
3.3 3.4 4
α(f ◦ g) = (α f ) ◦ (α g) null ◦ g →→ (α f ) ◦ g = N il Konstruktion und Bedingung
4.1 [f1 , . . . , (p → g; h), . . . , fn ] = p → [f1 , . . . , g, . . . fn ]; [f1 , . . . , h, . . . fn ] , (n ≥ 1)
3.1 FP-systeme
89
4.1.1 [f1 , . . . , (p1 → g1 ; . . . ; pn → gn ; h), . . . , fm ] = p1 → [f1 , . . . , g1 , . . . fm ]; . . . ; pn → [f1 , . . . , gn , . . . fm ]; [f1 , . . . , hn , . . . fm ], (n ≥ 1) Anhand von 1.10 soll beispielhaft gezeigt werden, wie man die G¨ ultigkeit von Gesetzen beweist. apndl ◦ [f ◦ g, (α f ) ◦ h] = (α f ) ◦ apndl ◦ [g, h] Zu zeigen ist, daß bei Anwendung auf ein beliebiges Objekt x beide Seiten das gleiche Ergebnis liefern. Wir betrachten die verschiedenen M¨oglichkeiten f¨ ur x. Fall 1 h : x ∈ A ∪ {⊥}\{N il}. Dann liefern beide Seiten ⊥. Fall 2 h : x = N il, f : (g : x) = ⊥ apndl ◦ [f ◦ g, (α f ) ◦ h] : x = apndl :< f ◦ g : x, (αf ) ◦ h : x >= apndl :< f ◦ g : x, N il > = < f : (g : x) > (α f ) ◦ apndl ◦ [g, h] : x = (α f ) ◦ apndl : < g : x, h : x > = (α f ) ◦ apndl : < g : x, N il > = < f : (g : x) > F¨ ur f : (g : x) =⊥ liefern beide Seiten ⊥. Fall 3 h : x = < y1 , . . . , yn >, f : (g : x) = ⊥ und f : yi = ⊥ f¨ ur 1 ≤ i ≤ n apndl ◦ [f ◦ g, (α f ) ◦ h] : x = apndl : < f ◦ g : x, (α f ) : < y1 , . . . , yn >> = < f : (g : x), f : y1 , . . . , f : yn > (α f ) ◦ apndl ◦ [g, h] : x = (α f ) ◦ apndl : < g : x, < y1 , . . . , yn >> = (α f ) : < g : x, y1 , . . . , yn > = < f : (g : x), f : y1 , . . . , f : yn > F¨ ur f : (g : x) =⊥ liefern beide Seiten ⊥, oder wenn es ein k gibt mit f : yk =⊥, 1 ≤ k ≤ n. Zur Illustration der Handhabung dieses Gesetzes sei das Programm 1) aus Kapitel 3.1.4 genommen: def l¨ ange(/+) ◦ (α 1)
90
3 Programmiersprachen
Es soll gezeigt werden, daß sich beim Verl¨ angern einer Folge um ein Element ihre L¨ ange um 1 erh¨ oht, d.h. es ist die Korrektheit der Gleichung l¨ ange ◦ apndl ◦ [f, g] = + ◦ [1, l¨ ange ◦ g] zu zeigen unter den Annahmen, daß f : x = ⊥ ist und daß g : x eine Folge ist. l¨ ange ◦ apndl ◦ [f, g] ≡ (/+) ◦ (α1) ◦ apndl ◦ [f, g] = (/+) ◦ apndl ◦ [1 ◦ f, (α 1) ◦ g] = (/+) ◦ apndl ◦ [1, (α 1) ◦ g] = +◦ [1, / + ◦ (α 1) ◦ g] = +◦ [1, l¨ ange ◦ g]
(nach (nach (nach (nach (nach
Def. von l¨ange) 1.10) 3.1) 1.11) Def. von l¨ange)
Da die Gesetze nur ein einfaches Hantieren mit Zeichenreihen erlauben, werden sie allein z.B. bei rekursiven Programmen in der Regel nicht ausreichen, die Korrektheit eines Programms zu zeigen. Hierzu dienen in der Algebra der Programme die Theoreme, mit deren Hilfe man spezielle Typen von rekursiven Gleichungen def f = τ [f ] l¨ osen kann. Das folgende Theorem stammt aus Backus (Backus 1978): Rekursions-Theorem Sei die Gleichung def f ≡ p → g; Q(f ) gegeben. Seien p,g,h,i,j gegebene Funktionen und Q(K) = h ◦ [i, K ◦ j] . Dann ist die L¨osung der Gleichung die Funktion def f ≡ p → g; p ◦ j → Q(g); . . . ; p ◦ j n → Qn (g); . . . mit Qn (g) = /h ◦ [i, i ◦ j, . . . , i ◦ j n−1 , g ◦ j n ] und j n = j ◦ j n−1
.
Anhand eines Korrektheitsbeweises f¨ ur das Fakult¨atsprogramm def f ≡ eqO → 1; ∗◦ [id, f ◦ s] def s ≡ −◦ [id, 1] (Subtraktion um 1) soll die Anwendung des Rekursions-Theorems demonstriert werden.
3.1 FP-systeme
91
Zu zeigen ist:
x! f¨ ur x nicht-negative ganze Zahlen f :x= ⊥ sonst . Die Voraussetzungen des Rekursions-Theorems sind erf¨ ullt mit den Zuordnungen: p g h i j
= ˆ eqO = ˆ 1 = ˆ ∗ = ˆ id = ˆ s = −◦ [id, 1]
Man erh¨ alt somit als L¨ osung: def f ≡ eqO → 1; eqO ◦ s → Q(1); . . . ; eqO ◦ sn → Qn (1); . . . mit Qn (1) = / ∗ ◦[id, id ◦ s, . . . , id ◦ S n−1 , 1 ◦ sn ]
.
Wir wenden die Gesetze der Algebra an: a)
= sk nach 3.2 id ◦ sk 1 true ◦ s →→ 1 ◦ s1 = 1 nach 3.1 eqO ◦ sn : x = true impliziert true ◦ sn : x = true
b) egO ◦ sn
→→ 1 ◦ sn = 1
Da egO ◦ sn : x = true genau dann gilt, wenn x = n ist, erh¨alt man mit a) und b): Qn (1) : n = / ∗ ◦ [id, id ◦ s, . . . , id ◦ sn−1 , 1 ◦ sn ] : n = /∗ : < id : n, id ◦ s : n, . . . , id ◦ sn−1 : n, 1 ◦ sn : n > = /∗ : < n, n − 1, . . . , n − (n − 1), 1 : n − n > = n ∗ (n − 1) ∗ · · · ∗ (n − (n − l)) ∗ (1 : n − n) = n ∗ (n − 1) ∗ · · · ∗ 1 ∗ 1 = n! . Setzen wir dies in die Gleichung f¨ ur die L¨ osung ein, so erhalten wir: f : x = x = 0 → 1; . . . ; x = n → n ∗ (n − 1) ∗ · · · ∗ 1 ∗ 1,. Somit haben wir gezeigt, daß f genau dann einen Funktionswert x! besitzt, wenn das Argument x eine nicht-negative ganze Zahl ist. In den anderen F¨allen ist f : x undefiniert.
92
3 Programmiersprachen
3.1.6 FFP-Systeme Die FFP-Systeme (Formal Functional Programming Systems) sind eine Verallgemeinerung der FP-Systeme, die urspr¨ unglich zur Definition einer operationalen Semantik eingef¨ uhrt wurden (Backus 1978, Williams 1981), da sie als eigenes Metasystem ungeeignet sind. Man kann n¨amlich die Funktionale aus F nicht im FP-System selbst ausdr¨ ucken, da alle Programme Funktionen sind. Generell kann man gewisse Funktionen in FP-Systemen nicht programmieren, da die Menge der Objekte und die Menge der Funktionen disjunkt sind; z.B. tritt in dem Funktional apply | (O → O) × O → O mit apply : < f, y >= f : y f als Objekt und zugleich als Funktion auf. Diese Einschr¨ ankungen von FP-Systemen werden in den FFP-Systemen u ¨berwunden, indem gewisse Objekte zur Darstellung von Funktionen benutzt werden. Die Basisfunktionen und Funktionen, die durch ein Gleichungssystem definiert sind, werden in FFP-Systemen durch atomare Objekte repr¨asentiert, w¨ahrend die Funktionale durch strukturierte Objekte dargestellt werden. Das erste Element f von < f, x1 , . . . , xn > ∈ 0 bestimmt das Funktional, welches durch das Objekt dargestellt wird, und die u ¨brigen Elemente x1 , . . . , xn sind Argumente f¨ ur f . Jedes FP-System kann nun in ein zugeh¨ origes FFP-System eingebettet werden (Robinet 1980 b, Williams 1981) und die Semantik von FFP-Systemen l¨aßt sich metazirkul¨ ar“ in FFP ausdr¨ ucken (Robinet 1980 a). Wir wollen hier ” eine andere interessante Eigenschaft betrachten: Man kann in FFP-Systemen gewisse Programme, die man u ¨blicherweise rekursiv programmiert, ohne syn¨ taktische Rekursion definieren. Ahnlich wie man den Y-Kombinator im λKalk¨ ul zu diesem Zweck benutzt (Kap.2.2.6), gibt es in FFP-Systemen die sogenannte Metakompositionsregel. FFP-Programme dieser Art stellen eine weitere Variante des funktionalen Programmierstils im engeren Sinne dar. Definition 3.1-4 Ein FFP-System (A, F, :) besteht aus den folgenden Komponenten: 1. A ist eine Menge von atomaren Objekten, definiert wie bei FP-Systemen (Def. 3.1-1). Zus¨ atzlich gilt # ∈ A als Darstellung von ERROR. Die Menge der Objekte O ist induktiv definiert durch O := A ∪ {⊥} ∪ {< x1 , . . . , xn > | n ≥ 1 und xi ∈ O\ {⊥}}
.
Die Bemerkung u ¨ber Objekte bei FP gelten entsprechend. Unter den atomaren Objekten befinden sich Bezeichnungen f¨ ur die Basisfunktionen aus F und f¨ ur benutzerdefinierte Funktionen.
3.1 FP-systeme
93
2. F ist eine endliche Menge von Basisfunktionen f | O → O . Alle Basisfunktionen sind total und strikt. 3. Die Applikation einer Funktion f | O → O auf ein Objekt x ∈ O wird mit f : x bezeichnet. 4. Die Menge E aller Ausdr¨ ucke ist induktiv definiert durch E := O ∪ {x : y | x, y ∈E}∪ {< e1 , . . . , en > | n ≥ 1 ∧ ei ∈ E} Ein Objekt ist also ein Ausdruck, in dem kein Teilausdruck eine Applikation ist. Ein FFP-Programm ist ein Ausdruck, der eine Funktion O → O bezeichnet. 5. Die Bedeutung einer Applikation x : y mit x, y ∈ E ist durch eine Evaluationsfunktion μ und eine Repr¨ asentationsfunktion ρ gegeben: μ:E→O ρ : O → (O → E)
.
Es gilt: falls x ∈ O
μ(x)
=x
μ(y)
= < μ(y1 ), . . . , μ(yn ) > falls y = < y1 , . . . , yn >, yi ∈ E
μ(x : y) = μ(ρ(x) : μ(y))
falls x, y ∈ E
ρ(x)
= ρ(μ(x))
falls x ∈ E \ O
ρ(x)
= fx
falls x ∈ A und x bezeichnet fx : O → E
ρ(x)
=⊥
falls x ∈ A und x bezeichnet keine Funktion.
asentierte Funktion wird definiert Die durch eine Folge < f, x1 , . . . , xn > repr¨ durch die Metakompositionsregel :
ρ(< f, x1 , . . . , xn >) : y = ρ(f ) :<< f, x1 , . . . , xn >, y > f¨ ur f, x1 , . . . , xn , y ∈ O \ {⊥} ⊥ sonst. Durch ρ(f ) wird also bestimmt, welches Funktional durch das gesamte Objekt dargestellt wird, wenn ein strukturiertes Objekt aus O \ A vorliegt. Atomare Objekte bezeichnen Basisfunktionen bzw. benutzerdefinierte Funktionen, wobei noch angegeben werden muß, wie solche Definitionen in FFP
94
3 Programmiersprachen
konkret aussehen. Die Bedeutung einer Applikation x : y mit x, y ∈ E erh¨alt man, indem man die durch x repr¨ asentierte Funktion auf den Wert von y anwendet und dann die Bedeutung des Resultats bestimmt. Benutzerdefinierte Funktionen werden in FFP-Systemen durch sogenannte Zellen“ dargestellt. Jede Zelle ist ein Tripel < CELL, f, of >, wobei ” CELL ∈ A ein ausgezeichnetes Atom ist, f ∈ A das Atom, das die zu definierende Funktion bezeichnet und of ∈ O das Objekt ist, das diejenige Funktion repr¨ asentiert, die durch f bezeichnet wird. In FP entspricht diese Definition der Gleichung def f ≡ ρ(of ), wenn man of hier als Einbettung von FFP-Zellen in FP betrachtet. 3.1.7 Beispiele fu ¨ r FFP-Programme 1. Sei die FP-Funktion null durch N U LL ∈ A repr¨asentiert, dann hat in FFP die Applikation N U LL : A mit A ∈ A die Bedeutung μ(N U LL : A) = μ(ρ(N U LL) : μ(A)) = μ(null : A) = μ(f alse) = f alse. 2. Da Basisfunktionen FFP-Ausdr¨ ucke als Wert haben k¨onnen, kann man eine Funktion apply : < x, y > = (x : y) definieren mit ρ(AP P LY ) = apply. Die Bedeutung von AP P LY : < N U LL; A >) ist μ(AP P LY : < N ull, A >) = μ(ρ(AP P LY ) : μ(< N U LL, A >)) = μ(apply : μ < N U LL, A >) = μ(N ull : A) = f alse. 3. Sei CON ST ∈ A Repr¨ asentant von S2 ◦ S1, dann repr¨asentiert das Obur y = ⊥ gilt jekt < CON ST, x > f¨ ur x ∈ O das FP-Funktional x, denn f¨ mit der Metakompositionsregel: μ(< CON ST, x > : y) = μ(ρ(< CON ST, x >) : μ(y)) = μ(ρ(CON ST ) : << CON ST, x >, y >) = μ(S2 ◦ S1 : << CON ST, x >, y >) = μ(x) = x F¨ ur y = ⊥ gilt: μ(ρ(< CON ST, x >) : ⊥ = μ(⊥) = ⊥. F¨ ur x = ⊥ gilt: μ(ρ(< CON ST, ⊥ >) : μ(y) = μ(⊥) = ⊥. 4. Die Metakompositionsregel gestattet die Definition aller FP-Funktionale durch einfache Funktionen u ¨ber Objekten, vorausgesetzt, man hat geeigne¨ te Basisfunktionen gew¨ ahlt. Uber den Kern der Basisfunktionen von FP hinaus ben¨ otigt man nur noch die Funktion apply (Robinet 1980 b). Sei ρ(CON S) = α apply ◦ tl ◦ distr, dann wird durch < CON S, f1 , . . . , fn >∈ O das FP-Funktional Konstruktion, d.h. [ρ(f1 ), . . . , ρ(fn )] repr¨asentiert:
3.1 FP-systeme
95
μ(< CON S, f1 , . . . , fn > : x (Definition von μ) = μ(ρ(< CON S, f1 , . . . , fn >) : μ(x)) (Metakomposition) = μ(ρ(CON S) : << CON S, f1 , . . . , fn >, x >) = μ(α apply ◦ tl ◦ distr : << CON S, f1 , . . . , fn >, x >) (Definition von ρ(CON S)) (tl ◦ distr) = μ(α apply : << f1 , x >, . . . , < fn , x >>) (α apply) = μ(< apply : < f1 , x >, . . . , apply : < fn , x >>) (Definition von μ) = μ(< f1 : x, . . . , fn : x >) = < ρ(f1 ) : x, . . . , ρ(fn ) : x >= [ρ(f1 ), . . . , ρ(fn )] : x . 5. Sei COM P ∈ A mit ρ(< COM P, f1 , . . . , fn >) : x = f1 : (f2 : (. . . : (fn : x) . . . )) f¨ ur n ≥ 1, d.h. COM P repr¨ asentiert die gew¨ohnliche Komposition von Funktionen. Dann gilt: < COM P, f, g > << COM P, S2, Sl >, x > << COM P, S2 >>
repr¨ asentiert f ◦ g repr¨ asentiert x repr¨ asentiert id
Die Darstellung durch Zellen in FFP ist: < CELL, COM P F G < CELL, CON ST AN T X < CELL, CON ST < CELL, ID
, < COM P, f, g >> , << COM P, S2, S1 >, x >> , < COM P, S2, S1 >> , < COM P, S2 >>> .
6. Die wesentliche Eigenschaft der Metakompositionsregel ist jedoch, daß man mit ihr rekursive Programme schreiben kann, ohne daß die Rekursion explizit im Programmtext auftritt (syntaktische Rekursion). Hier liegt ein enger Bezug zum Y-Kombinator im λ-Kalk¨ ul vor. Wir betrachten die Funktion last in rekursiver Definition in FP: def last ≡ null ◦ tl → S1; last ◦ tl. Sei in FFP ρ(LAST ) = null ◦ tl ◦ S2 → S1 ◦ S2; apply ◦ [S1, tl ◦ S2], dann repr¨ asentiert < LAST > die Funktion last ohne Benutzung von syntaktischer Rekursion. alt man: Sei < x1 , . . . , xn >∈ O, dann erh¨ μ(< LAST >: < x1 , . . . , xn >) = μ(ρ(< LAST >) : < x1 , . . . , xn >) = μ(ρ(LAST ) : << LAST >, < x1 , . . . , xn >>)
(Definition von μ) (Metakomposition)
= μ(apply ◦ [S1, tl ◦ S2] : << LAST >, < x1 , . . . , xn >>) (null ◦ tl ◦ S2 sei false, d.h. n ≥ 2)
96
3 Programmiersprachen
= μ(apply : << LAST >, < x2 , . . . , xn >>) = μ(< LAST >: < x2 , . . . , xn >) = μ(< LAST >: < xn >)
(Induktion)
= μ(ρ(< LAST >) : < xn >)
(Definition von μ)
= μ(ρ(< LAST >) : << LAST >, < xn >>) = μ(S1 ◦ S2 : << LAST >, < xn >>)
(Metakomposition) (null ◦ tl ◦ S2 ist true)
= μ(xn ) = xn . Als weiteres Beispiel betrachten wir die n-fache Iteration einer Funktion f , d.h. ρ(< IT >) : < n, f, x, > = f n : x, wobei f O = id und f n+1 = f ◦ f n ist. Sei ρ(IT ) = eqO ◦ S1 ◦ S2 → S3 ◦ S2; apply ◦ [S2 ◦ S2, apply ◦ [S1, [M 1 ◦ S1, S2, S3] ◦ S2]], wobei eqO wie in Kapitel 3.1.3 definiert ist und M 1 die Differenz − ◦ [id.1] bezeichnet. Man erh¨ alt: μ(< IT > : < n, f, x >) = μ(ρ(< IT >: < n, f, x >)) = μ(ρ(IT ) : << IT ><, n, f, x >>) = μ(apply ◦ [S2 ◦ S2, apply ◦ [S1, [M 1 ◦ S1, S2, S3] ◦ S2]] : << IT >, < n, f, x >>) = μ(apply : < S2 ◦ S2 : << IT >, < n, f, x >>, apply ◦ [S1, [M 1 ◦ S1, S2, S3] ◦ S2 : << IT >, n, f, x >>>) = μ(apply : < f, apply : << IT >, < n − 1, f, x >>>) = μ(apply : < f, ρ(IT ) : << IT >, < n − 1, f, x >>>) .. . = μ(apply
:< f, apply :< f, . . . , apply :< f , x > · · · >>) n-mal f
= μ(f : (f : . . . . : (f : x) . . . ))
.
Die rekursive Version der n-fachen Iteration lautet ρ(ITr ) = eqO ◦ S1 → S3; apply ◦ [S2, ITr ◦ [M 1 ◦ Sl, S2, S3]]. Es sei dahingestellt, ob die Version < IT > ohne syntaktische Rekursion eleganter und u ¨bersichtlicher ist als das Programm ITr !
3.1 FP-systeme
97
3.1.8 FP-Programme als Kombinatoren Die kombinatorische Logik stellt ein nat¨ urliches Hilfsmittel zur Beschreibung von variablenfreien Programmiersprachen dar, wie z.B. FP. Jedes FPProgramm definiert eine partiell rekursive Funktion u ¨ber einem effektiven Bereich (Def. 2.1-3, Def. 2.1-6, Bsp. 2.1-4) und ist somit durch einen kombinatorischen Term definierbar (Satz 2.3-7). Wir sind jedoch an einer solchen generellen Beziehung weniger interessiert und betrachten statt dessen ¨ ein spezielles, syntaxorientiertes Ubersetzungsverfahren von FP-Programme in kombinatorische Terme nach Robinet (Robinet 1980 b). Dadurch wird eine Semantik f¨ ur FP-Programme aus der kombinatorischen Logik heraus induziert, die mit den in der Metanotation von FP ausgedr¨ uckten Intentionen u ¨bereinstimmt. Dieses Ziel allein ließe sich mit anderen Hilfsmitteln weniger aufwendig erreichen. Es soll jedoch aufgezeigt werden, daß die kombinatorische Logik eine wichtige Funktion als Zwischensprache bei der Implementation funktionaler Sprachen einnehmen kann. Sie ist sowohl ein Bindeglied zu alternativen Rechnerarchitekturen als auch eine algebraische Struktur zum gegebenen¨ falls maschinellen Beweisen von funktionalen Programmen. Das Ubersetzen von funktionalen Programmiersprachen in kombinatorische Terme mit einem ad¨ aquaten Satz von Basiskombinatoren (Turner 1979, Turner 1979 a, B¨ohm 1982, Gibert 1983, Oberhauser 1984) und die Reduktion auf neue Rechnerarchitekturen (Clarke 1980, Turner 1979, Ackermann 1984) oder auf von Neumann Rechnern (Muchnik 1982, Gibert 1983) ist praktikabel und vom Prinzip her nicht unbedingt kompliziert oder ineffizient. Die Integration von maschi¨ nellen Beweissystemen und Ubersetzern bzw. Interpretierern befindet sich erst in den Anf¨ angen. Hughes betrachtet eine Generalisierung der kombinatorischen Logik und u ¨bersetzt Programme aus einer funktionalen Sprache, die im wesentlichen auf dem λ-Kalk¨ ul beruht, in Superkombinatoren“ (Hughes 1982). Die Basiskom” binatoren eines Superkombinators werden jeweils aus dem zu u ¨bersetzenden Programm abgeleitet. Die Menge aller Basiskombinatoren bildet eine unendliche Menge; u ¨blich ist es, einer Implementation eine endliche Menge von Basiskombinatoren zugrunde zu legen. ¨ Die kombinatorische Logik spielt auch bei der Ubersetzung von konventionellen Programmiersprachen eine gewisse Rolle: Wand hat gezeigt, daß man eine Programmiersprache, die in einer Variante der denotationellen Semantik definiert ist, systematisch in kombinatorische Terme u ¨bersetzen kann (Wand 1982). Zur Darstellung von FP in der kombinatorischen Logik werden wir die folgenden Kombinatoren ben¨ otigen: 1. I, K, S als Basiskombinatoren 2. B ≡ S(KS)K 3. C ≡ S(BBS)(KK)
98
3 Programmiersprachen
4. W ≡ SS(KI) . 5. C∗ ≡ CI Diese Kombinatoren gen¨ ugen f¨ ur X, Y, Z ∈ CT den Gleichungen: 1.
2. 3. 4. 5.
IX KXY SXY Z BXY Z CXY Z W XY C∗ XY
=X =X = XZ(Y Z) = X(Y Z) = XZY = XY Y =YX .
Weiterhin werden folgende abk¨ urzende Notationen benutzt: Sei n ∈ N0 . = KI 0 ur n ≥ 0 n + 1 = SBn f¨
.
Durch n wird ein Iterationsoperator“ dargestellt: ” n M N = M (M (. . . (M N ) . . . )) . n−mal
Nach Church werden geordnete Paare M, N von Termen ≤ M, N ≥∈ CT dargestellt durch: ≤ M, N ≥ ≡ [z]zM N. Die Abstraktion kombinatorischer Terme ist in den Definitionen 2.3-5 und 2.3-6 eingef¨ uhrt worden. Selektoren auf geordneten Paaren sind (≤ M, N (≤ M, N (≤ M, N (≤ M, N
≥)1 ≥)2 ≥)1 ≥)2
≡ ≡ ≡ ≡
≤ M, N ≤ M, N ≤ M, N ≤ M, N
≥ K und ≥ (KI), denn ≥ K ≡ ([z]zM N )K = KM N = M und ≥ (KI) ≡ ([z]zM N )(KI) = KIM N = IN = N
Wir behandeln zun¨ achst spezielle Aspekte der Darstellung des FP-Systems aus Kapitel 3.1.3 und fassen dann die Teilergebnisse zu einer Abbildung aller Komponenten dieses Systems in die kombinatorische Logik zusammen. 1. Atomare Objekte Die Menge der Atome ist A = {true, f alse} ∪ {N il} ∪ Idf ∪ Z. Die Wahrheitswerte werden in Analogie zu Definition 2.2-10 dargestellt durch die Kombinatoren T = K und F = KI.
3.1 FP-systeme
99
Das Atom Nil ist in FP mit der leeren Folge < > identifiziert. Wir werden zwischen dem Identifikator N il ∈ Idf und < > unterscheiden. Die literalen Atome Idf und die ganzen Zahlen Z werden durch die gleichnamigen Konstanten in der kombinatorischen Logik dargestellt, d.h. (Idf ∪ Z) ⊂ CT . Eine spezielle Kodierung von Z durch Kombinatoren. in Anlehnung an Kapitel 2.2.4 ben¨ otigen wir nicht, da arithmetische Operationen als δRegeln (Kap. 2.2.8) eingef¨ uhrt werden. 2. Folgen und ihre Operationen Das total undefinierte FP-Objekt ⊥ wird dargestellt unter Verwendung eines Fixpunktkombinators YF (Bsp. 2.3-1). Definition 3.1-5 Die Darstellung von ⊥ lautet: ⊥≡ YF K Es gilt f¨ ur X ∈ CT : ⊥ X ≡ YF KX = K(YF K)X = YF K ≡ ⊥ . Nun k¨ onnen FP-Folgen und die zugeh¨ origen Basisfunktionen durch kombinatorische Terme ausgedr¨ uckt werden. Definition 3.1-6 Die Darstellung von Folgen ist gegeben durch PO ≡ K ⊥ ur n ≥ 1 Pn ≡ [a1 , . . . , an ] ≤ a1 , Pn−1 a2 . . . an ≥ f¨ Der Kombinator PO ist die Darstellung der leeren Folge < >. Sei < x1 , . . . , xn > eine FP-Folge und seien X1 , . . . , Xn Darstellungen der Folgenelemente in der kombinatorischen Logik, dann ist Pn X1 . . . Xn die Darstellung von < x1 , . . . , xn >. Pn X1 . . . Xn ≡ [a1 , . . . , an ] ≤ a1 , Pn−1 a2 . . . an ≥ X1 . . . Xn = ≤ X1 , ≤ X2 , . . . , ≤ Xn , PO ≥ · · · ≥≥ In graphischer Darstellung: <>
x1
x2
xn
100
3 Programmiersprachen
Definition 3.1-7 Die Basisoperationen tl und hd auf Listen sind gegeben durch tl ≡ C∗ (KI) hd ≡ C∗ K Diese Kombinatoren sind die Darstellungen der FP-Basisfunktionen hd und tl (Def.3.1-2). Sie entsprechen den in Metanotation gegebenen Spezifikationen. tl PO ≡ C∗ (KI)PO ≡ C∗ (KI)(K ⊥) = K ⊥ (KI) = ⊥ tl (Pn X1 . . . Xn ) ≡ C∗ (KI)(Pn X1 . . . Xn ) ≡ C∗ (KI)([a1 , . . . , an ] ≤ a1 , Pn−1 a2 . . . an ≥ X1 . . . Xn = C∗ (KI) ≤ X1 , Pn−1 X2 . . . Xn ≥ = (≤ X1 , Pn−1 X2 . . . Xn ≥)2 = Pn−1 X2 . . . Xn hd PO ≡ C∗ KPO ≡ C∗ K(K ⊥) = K ⊥ K = ⊥ hd (Pn X1 . . . Xn ) ≡ C∗ K(Pn X1 . . . Xn ) ≡ C∗ K([a1 , . . . , an ] ≤ a1 , Pn−1 a2 . . . an ≥ X1 . . . Xn = C∗ K ≤ X1 , Pn−1 X2 . . . Xn ≥ = (≤ X1 , Pn−1 X2 . . . Xn ≥)1 = X1 Die Selektoren S1, S2, . . . , Sm, . . . lassen sich auf die (m-1)-fache Iteration von tl und anschließendem hd zur¨ uckf¨ uhren. B sorgt f¨ ur die Rechtsklammerung. Definition 3.1-8 Sm ≡ B hd(m − 1 tl), m ≥ 1 Es gilt f¨ ur n ≥ 0:
Xm Sm(Pn X1 . . . Xn ) = ⊥
falls m ≤ n sonst.
1. Fall: m=1,n=0 S1PO ≡ B hd(0 tl)PO = hd(0 tl PO ) = hd(PO ) = ⊥ m=1,n≥1 S1(Pn X1 . . . Xn ) ≡ B hd (0 tl)(Pn X1 . . . Xn ) = hd(Pn X1 . . . Xn ) = X1
3.1 FP-systeme
101
2. Fall: m ≥ 2,
n=0
m ≥ 2,
n=1 Sm(Pn X1 . . . Xn ) ≡ B hd(m − 1 tl)(Pn X1 . . . Xn ) = hd (m − 1 tl (Pn X1 . . . Xn ))
SmPO ≡ B hd(m − 1 tl)PO = hd(m − 1 tl PO ) ,m − 1 ≥ 1 = hd(⊥) = ⊥ tl ⊥ = C∗ (KI) ⊥ = ⊥ (KI) = ⊥ hd ⊥ = C∗ K ⊥ = ⊥ K = ⊥
Falls m ≤ n Sm(Pn X1 . . . Xn ) = hd (Pn−m+1 Xm . . . Xn ) = Xm Falls m = n + 1 Sm(Pn X1 . . . Xn ) = hd (n tl (Pn X1 . . . Xn )) = hd PO = ⊥ Falls m ≥ n + 2 Sm(Pn X1 . . . Xn ) = hd (m − 1 tl (Pn X1 . . . Xn )) = hd (m − 1 − n tl ⊥) = hd(⊥) = ⊥ 3. Operationen auf Folgenelemente Man ben¨ otigt einen Kombinator Ext zur Ausdehnung von zweistelligen Operationen in Pr¨ afixnotation, bezeichnet mit ⊕, auf Listen von Paaren: Ext ⊕ (P2 n m) = ⊕ n m
.
Sei M ≡ P2 n m. Nach der in Beispiel 2.3-1 skizzierten Methode erh¨alt man einen Kombinator Ext durch: Ext ⊕ M = ⊕ (S1 M ) (S2 M ) = B ⊕ S1 M (S2 M ) = S (B ⊕ S1) S2 M = C S S2 (B ⊕ S1) M = C S S2 (C B S1⊕) M = B (C S S2) (C B S1) ⊕ M Ext ≡ B (C S S2) (C B S1) . 4. Logische Operationen Die logischen Operationen Negation, Konjunktion und Disjunktion lassen sich durch die folgenden Terme darstellen:
102
3 Programmiersprachen
Satz 3.1-1 1. not ≡ C(C∗ F )T 2. and ≡ SCI 3. or ≡ SII mit T ≡ K und F ≡ KI. Beweis: zu 1. not X ≡ C(C∗ F )T X = C∗ F XT = XF T not T = T F T = F und not F = F F T = T zu 2. and X Y ≡ SCIXY = CX(IX)Y = XY X and T T = T T T = T und and T F = T F T = F and F T = F T F = F und and F F = F F F = F zu 3. or X Y ≡ SIIXY = IX(IX)Y = XXY or T T = T T T = T und or T F = T T F = T or F T = F F T = T und or F F = F F F = F 5. Pr¨ adikate Die numerischen Pr¨ adikate wie z.B. eq0 werden u uhrt. ¨ber δ-Regeln eingef¨ Es bleibt die Darstellung von eq, atom und null. Grundlage daf¨ ur ist der Kombinator Ident, der durch die Gleichheit von Normalformen kombinatorischer Terme definiert ist (Satz 3.3-5). ⎧ ⎪ ⎨T Ident X Y = ⊥ ⎪ ⎩ F
falls X, Y in Normalform und X ≡ Y falls X = Y =⊥ sonst.
Ident ist ein Kombinator mit einer speziellen Reduktionsstrategie; seine Argumente m¨ ussen erst in Normalform reduziert werden. Die Erweiterung der kombinatorischen Logik um Ident ist konsistent (Curry 1958), da es sich um eine δ-Regel handelt. Definition 3.1-9 Die Pr¨ adikate eq und null sind gegeben durch eq ≡ Ext Ident null ≡ Ident PO
3.1 FP-systeme
103
Es ist sehr aufwendig, den Kombinator Atom mit der Eigenschaft: ⎧ T falls x ∈ Idf ∪ Z ⎪ ⎪ ⎪ ⎨T falls x ∈ {T, F, N il} Atom X = ⎪ F falls x eine nichtleere Folge ist ⎪ ⎪ ⎩ ⊥ sonst. auf Ident und andere Basiskombinatoren zur¨ uckzuf¨ uhren (Robinet 1980 b) 6. Funktionale Komposition Die Darstellung von f ◦ g ist Bfg . Konstruktion Die Darstellung von [ ] ist die der konstanten Funktion N il. Die Darstellung von [f1 , . . . , fn ] mit n ≥ 1 ist An f1 . . . fn mit An ≡ B Sn−1 (B Pn ) S0 ≡ B I . Sn+1 ≡ B (B Sn ) S
,n − 1
Wir zeigen zun¨ achst, daß Sn eine Verallgemeinerung von S ist: Sn f g1 . . . gn x = f x(g1 x) . . . (gn x). Beweis durch Induktion u ¨ber n: a) n = 0 S0 f x ≡ BIf x = I(f x) = f x b) Die Behauptung gelte f¨ ur n < p. c) n = p : Sn f g1 . . . gn x ≡ B (BSn−1 ) Sf g1 . . . gn x = BSn−1 (Sf )g1 . . . gn x = Sn−1 ((Sf )g1 )g2 . . . gn x (Induktionsvoraussetzung) = Sf g1 x(g2 x) . . . (gn x) = f x(g1 x)(g2 x) . . . (gn x) Ferner gilt: An f1 . . . fn x ≡ BSn−1 (BPn ) f1 . . . fn x = Sn−1 (BPn f1 )f2 . . . fn x = BPn f1 (f2 x) . . . (fn x) = Pn (f1 x)(f2 x) . . . (fn x).
104
3 Programmiersprachen
Bedingung Die Darstellung von (p → f ; g) ist Cond pf g mit Cond = B(BS)S
.
Es gilt: Cond pf g ≡ B (BS) Spf g = (BS) (SP ) f g = S (Spf ) g . Damit ergibt sich bei Applikation auf ein Argument x: Cond pf gx = S(Spf ) gx = Spf x (gx)
⎧ ⎪ ⎨f x falls px = T = px(f x)(gx) = gx falls px = F ⎪ ⎩ ⊥ sonst .
Konstante Funktion Die Darstellung von x ist Cond Atom (Kx) (Kx). Bemerkung: Kx als Darstellung w¨ are eine nichtstrikte Funktion! Reduktion Wir folgen Backus urspr¨ unglicher Intuition, daß /f die Erweiterung einer zweistelligen, arithmetischen bzw. logischen Basisfunktion auf Folgen bezeichnet und schließen den nach Definition 3.1-2 m¨oglichen Fall aus, daß f eine beliebige FP-Funktion ist. Dann wird /f dargestellt durch Rn f mit R1 ≡ K hd, f¨ ur n ≥ 2, und Rn ≡ n − 1 Q R1 Q ≡ B(S(BS(C B hd)))(B(C B tl))
.
Wir zeigen durch Induktion u ¨ber n ∈ N : Rn f (Pn x1 . . . xn ) = f x1 (f x2 (. . . (f xn−1 xn ) . . . )) a) n = 1 R1 f (P1 x1 ) ≡ K hd f (P1 x1 ) = hd (P1 x1 ) = x1 b) Die Behauptung gelte f¨ ur n < p, p ≥ 1
3.1 FP-systeme
105
c) n = p : Rn f (Pn x1 . . . xn ) ≡ n − 1 Q R1 f (Pn x1 . . . xn ) = Q (n − 2 Q R1 ) f (Pn x1 . . . xn ) ≡ Q Rn−1 f (Pn x1 . . . xn ) = f x1 (Rn−1 f (Pn−1 x2 . . . xn )) Sei D ≡ (Pn x1 . . . xn ). Q Rn−1 f D ≡ B (S (B S (C B hd))) (B (C B tl)) Rn−1 f D = S (B S (C B hd)) (B (C B tl) Rn−1 ) f D = B S (C B hd) f (B (C B tl) Rn−1 f ) D = S (C B hd f ) (C B tl (Rn−1 f )) D = C B hd f D (C B tl (Rn−1 f ) D) = B f hd D (B (Rn−1 f ) tl D) = f (hd D) (Rn−1 f (tl D)) = f x1 (Rn−1 f (Pn−1 x2 . . . xn )) Anwendung auf alle Wir stellen αf durch einen Kombinator αn dar, f¨ ur den gilt: αn f (Pn x1 . . . xn ) = P (f x1 ) . . . (f xn ) Die Definition von α ergibt sich aus der folgenden Rechnung: Sei Π = [xyz]zxy ein Kombinator zur Paarbildung, d.h. ΠN M = [z]zN M =≤ N, M ≥ und sei D = (Pn x1 . . . xn ). αn f D = Pn (f x1 ) . . . (f xn ) = ≤ (f x1 ), Pn−1 (f x2 ) . . . (f xn ) ≥ = Π (f (x1 ) (Pn−1 (f x2 ) . . . (f xn )) = Π (f (hd D))(αn−1 f (tl D)) = B Π (B f hd) D (B (αn−1 f ) tl D) = S (B Π (B f hd)) (B(αn−1 f ) tl) D = S (B (B Π) (C B hd) f ) (C (B B αn−1 )tl f ) D = B S (B (B Π) (C B hd)) f (C (B B αn−1 )tl f ) D = S (B S (B (B Π) (C B hd))) (C B B αn−1 )tl f ) D = S (B S (B(B Π) (C B hd))) (B (C C tl) (B B)αn−1 ) f D = B (S (B S (B(B Π) (C B hd))) (B (C C tl) (B B))) αn−1 ) f D Wegen K I f PO = PO setzt man αO ≡ KI, und aus der Rechnung ergibt sich f¨ ur n ≥ 1 αn ≡ Hαn−1 = nHαO mit H ≡ B (S (B S (B (B Π) (C B hd))) (B (C C tl) (BB)))
.
106
3 Programmiersprachen
Bin¨ ar nach Un¨ ar Sei f eine arithmetische bzw. logische Basisfunktion. Dann wird (bu f x) durch bu f x definiert mit bu ≡ B (B (B (C∗ P2 ) B) B) Ext
.
Es gilt: bu f x y ≡ B (B (B (C∗ P2 ) B) B) Ext f x y = B (B (C∗ P2 ) B) B (Ext f ) x y = B (C∗ P2 ) B (B (Ext f )) x y = C∗ P2 (B (B (Ext f ))) x y = B (B (Ext f )) P2 x y = B (Ext f ) (P2 x) y = Ext f (P2 x y)
.
Bedingte Wiederholung Wir definieren while p f u ¨ber einen Fixpunktkombinator YF : YF ([t][y] Cond (py) (t(f y)) y) . 7. Darstellung von Definitionen Eine rekursive Definition def f ≡ τ wird dargestellt durch YF [f ]τ , wobei ¨ von τ in einen Kombinator ist. Die Darstellung von f τ die Ubersetzung wird f¨ ur alle u ¨brigen, angewandten Vorkommen von f substituiert. Wir fassen nun zusammen und geben als Semantik eine Abbildung σ von FPProgrammen in die kombinatorische Logik an: FP-Objekte: σ(x) σ(true) σ(⊥) σ(< >) σ(< x1 , .., xn >)
=x f¨ ur x ∈ Idf ∪ Z = T ≡ K, σ(f alse) = F ≡ K = YF K = PO = Pn σ(x1 ) . . . σ, (xn ), n ≥ 1
Basisfunktionen: σ(id) = I ˜ σ(hd) = C∗ K ≡ hd ˜ σ(tl) = C∗ (KI) ≡ tl ˜ ˜ ,m ≥ 1 σ(Sm) = B hd (m − 1 tl) σ(not) = C (C∗ F )T σ(and) = SCI σ(or) = SII
3.1 FP-systeme
σ(⊕)
= Ext⊕
107
wobei ⊕ f¨ ur eine zweistellige Operation in Pr¨ afixnotation steht
σ(eq) = Ext Ident σ(null) = Ident PO σ(atom) = Atom Die u ¨brigen in Definition 3.1-2 genannten Basisfunktionen lassen sich in F P aquivalent umformen: ¨ σ(f ◦ g) σ([f1 , . . . , fn ]) σ(p → f ; g) σ(/⊕) σ(α f ) σ(while p f ) σ(x) σ((bu f x)) σ(def f = τ )
= B σ(f ) σ(g) = An σ(f1 ) . . . σ(fn ) = Cond σ(p) σ(f ) σ(g) , wobei n die L¨ ange des Arguments ist = Rn ⊕ , wobei n die L¨ange des Arguments ist = αn f = YF ([t][y] Cond (σ(p)y) (t(σ(f )y))y) = Kx = B (B (B (C∗ .P2 ) B) B) Ext f x = YF [f ]σ(τ ) und σ(f ) = σ(def f = τ ) f¨ ur alle u ¨brigen Vorkommen von f
Die Abbildung σ ist vertr¨ aglich mit der Applikation in FP und in der kombinatorischen Logik (Robinet 1980 b): σ(f : x) = σ(f ) σ(x)
.
Als Konsequenz der Einbettung von FP in die kombinatorische Logik lassen sich die S¨ atze aus der kombinatorischen Logik u ¨bertragen, z.B. der Satz 2.3-5: Satz 3.1-2 FP-Systeme besitzen die Church-Rosser-Eigenschaft. Dieser Satz ist wichtig, weil er die Grundlage f¨ ur das parallele Reduzieren von FP-Programmen ist, in dem mehrere Applikationen gleichzeitig reduziert werden. Das kann in den Programmbeispielen 3) und 4) aus Kapitel 3.1-4 recht effizient eingesetzt werden. Wir wollen mit einem Ausblick auf FFP schließen. Im wesentlichen ben¨otigt man Darstellungen von apply und von der Metakompositionsregel in der kombinatorischen Logik(Robinet 1980b)): 1. Sei apply = S hd (B hd tl) , dann gilt: apply(P2 xy) = xy 2. ρ(< f, x1 , . . . , xn > : y) = ρ(f ) : << f, x1 , . . . , xn >, y > wird in eine Gleichung u ¨bertragen: Rho ≤ f, x1 , . . . , xn ≥ y = Rho f (P2 ≤ f, Pn x1 . . . xn ≥)y mit Rho ≡ YF (B (B (B (C S P2 ) (C B hd)) (B B))).
108
3 Programmiersprachen
Die Elimination von syntaktischen Rekursionen in FFP ist damit gekl¨art; sie beruht auf dem Fixpunktkombinator YF .
3.2 LISP 3.2.1 Einleitung LISP ist die Abk¨ urzung f¨ ur List Processing Language“ und geh¨ort zur glei” chen Generation von Programmiersprachen wie FORTRAN oder ALGOL 60 und geh¨ ort damit zu den ¨ altesten h¨ oheren Programmiersprachen. Dennoch unterscheidet sich LISP schon im ¨ außeren Bild wesentlich von diesen beiden Sprachen. Als ihr Vater gilt John McCarthy. Einige Grundideen von LISP stammen aus der Sprache IPL (Information uckgeht, und Processing Language), die auf A. Newell und H.A. Simon zur¨ bereits im Sommer 1956 soweit entwickelt war, daß sie von den Autoren auf Konferenzen und Tagungen pr¨ asentiert werden konnte. Wegen ihres maschinennahen Niveaus geriet sie jedoch mit dem Aufkommen der h¨oheren Programmiersprachen schnell in Vergessenheit. Die erste vollst¨andige, aber noch vorl¨ aufige Beschreibung von LISP verfaßte P.A. Fox 1960 im ’LISP-1 Programmers Manual’. Die endg¨ ultige Beschreibung, die f¨ ur lange Zeit Quasi-Standard war, ist das 1962 fertiggestellte LISP-1.5 Programmers Manual“ (McCarthy ” 1962). Einzelheiten der recht interessanten, fr¨ uhen Geschichte von LISP findet man in McCarthy (McCarthy 1977) und Stoyan (Stoyan 1980, Stoyan 1984 a). W¨ ahrend die Beschreibung der Syntax von LISP auf konventionelle Weise durch Angabe einer Grammatik erfolgte, beschritt McCarthy bei der Beschreibung der Semantik einen neuen Weg, indem er sie durch Angabe eines Interpretierers maschinenunabh¨ angig festlegte. Der Interpretierer selbst ist in einer einfachen Teilsprache von LISP geschrieben, f¨ ur die er ein intuitives Verst¨ andnis voraussetzt. Man spricht von einer ’metazirkul¨aren Definition’. Somit existieren zwei Darstellungsformen f¨ ur Programme: In der Form, in der u.a. der Interpretierer geschrieben ist (Meta-Sprache) und in der Form von Daten (S-Ausdruck), wie sie der Interpretierer als Eingabe erwartet. Der hier vorgestellte Sprachumfang wird als ’Pure-LISP’ bezeichnet und bildet den applikativen Kern von LISP. Er entspricht dem Sprachumfang der Seiten 1-19 des LISP -1.5 Manuals, erweitert um funktionale Argumente und funktionale Resultate (FUNARG-Konzept). Pure-LISP ist nicht als Programmiersprache f¨ ur die Praxis zu verstehen. Die etwas st¨ arkere Fokussierung im Rahmen dieses Buches auf Pure-LISP ist darin begr¨ undet, daß hiermit eine kompakte Kernsprache zur Verf¨ ugung steht, die eine einfache Einf¨ uhrung in die Konzepte der (im strengen Sinne) applikativen Programmiersprachen erlaubt. Auch die Zusammenh¨ange mit dem λKalk¨ uhl und die grundlegenden Techniken der Implementierung applikativer Sprachen k¨ onnen auf der Basis von Pure-LISP ohne den f¨ ur eine komfortable Programmierumgebung notwendigen Overhead“ unmittelbar vermittelt ” werden.
3.2 LISP
109
Schon das von McCarthy entwickelte LISP-1.5 System war im Hinblick auf die praktischen Anwendungen wesentlich reichhaltiger ausgestattet, z.B. mit zus¨ atzlichen Datenstrukturen und Funktionen. Ausgehend von LISP-1.5 sind eine Reihe von großen LISP-Systemen entstanden, wie z.B. INTERLISP, MACLISP, CommonLISP. Die Erweiterungen lassen sich grob in folgende Kategorien einteilen: -
-
Erweiterungen um zus¨ atzliche (atomare) Datentypen. Erweiterungen um zus¨ atzliche Basisfunktionen (mehrere hundert !!!), teils in Machinencode, teils in LISP programmiert. Konzeptionelle Erweiterungen die, z.T. allerdings nicht mehr im strengen Sinne applikativ sind, wie PROG, SETQ, objektorientierte Programmierung, spezielle Auswertungsstrategien f¨ ur Argumente. Erweiterungen des Systems zu einer integrierten Programmierumgebung mit Editoren, Debugger, Tracer, Interfaces zu anderen Sprachen usw.. LISP-Compiler.
Mit ’CommonLISP’ und ’EU-LIST’ wurden zwei Versuche unternommen, eine Standardisierung zu erreichen. Diese Versuche waren allerdings nur beschr¨ankt erfolgreich. Im Ausbildungsbereich setzte sich als Nachfolgesprache SCHEME durch. Breitere Anwendung fand LISP u.a. in Gebieten wie K¨ unstliche Intelligenz, Robotersteuerung sowie als Anfragesprache an Datenbanken. Der Erfolg von LISP beruht auf dem einfachen applikativen Grundkonzept, der homogenen Datenstruktur, der leichten Anpassungsf¨ahigkeit an spezielle Benutzeranforderungen und der Entwicklung einer umfangreichen und komfortablen, interaktiven Programmierumgebung. Es hat sich gezeigt, daß LISP in besonderer Weise sowohl f¨ ur die Erstellung von Prototypen von Programmen geeignet ist als auch f¨ ur die Entwicklung und Wartung sehr umfangreicher Programme. Es gibt auch immer wieder Bestrebungen, Elemente der pr¨adikativen Programmierung in LISP-Systeme einzubeziehen. Eines der ersten war das LOGLISP-System von Robinson (Robinson 1982). 3.2.2 Pure-LISP 3.2.2.1 Daten In Pure-LISP gibt es nur einen Datentyp, die S-Ausdr¨ ucke (symbolische Ausdr¨ ucke). Sie unterteilen sich in atomare und nichtatomare S-Ausdr¨ ucke. Ein Atom ist eine Zeichenreihe, die wie ein Identifikator aufgebaut ist. Beispiele 3.2-1 A, BIRNE, A4BSXYZ Ein nichtatomarer S-Ausdruck ist ein endlicher bin¨arer Baum, dessen Bl¨atter mit Atomen belegt sind.
110
3 Programmiersprachen
A
B
C
D
Die Darstellung als Zeichenreihe erfolgt in der Punktschreibweise (dot notation): ((A.B).(C.D)) Die Punkte entsprechen den Knoten des Baumes. Ein nichtatomarer SAusdruck (α.β) wird durch den Punkt in einen linken Teil α und einen rechten Teil β unterteilt, entsprechend dem linken und rechten Unterbaum. Definition 3.2-1 < S − Ausdruck >::=< Atom > |(< Ausdruck > . < S − Ausdruck >) Beispiele 3.2-2 (A.(B.(C.D))):
(A.B): A
B
A B C
D
¨ Uberwiegend tritt die folgende spezielle Baumstruktur auf
α1 α2 αn
Ω
wobei α1 , . . . , αn S-Ausdr¨ ucke sind und Ω eine Endmarkierung darstellt. Man kann sie als eine Auflistung α1 , α2 . . . , αn , Ω deuten. Zur vereinfachten Darstellung dieser Baumstruktur dient die Listenschreibweise (α1 α2 . . . αn ) als Abk¨ urzung f¨ ur (α1 .(α2 .(. . . (αn .N il) . . . )))
.
Die Endmarkierung Ω wird in LISP durch das Spezialatom N IL dargestellt. N IL dient auch als Repr¨ asentant f¨ ur die leere Liste. Da Listen also spezielle
3.2 LISP
111
S-Ausdr¨ ucke sind, lassen sie sich in die Punktschreibweise umformen. Jedoch kann nicht jeder S-Ausdruck in Punktschreibweise als Liste dargestellt werden. Definition 3.2-2 < Liste >::= () |N IL| (< Element > · · · < Element >) < Element >::= < S − Ausdruck > | < Liste > 3.2.2.2 Basisfunktionen zur Verarbeitung von S-Ausdru ¨ cken 1. cons cons ist eine Funktion, die aus zwei S-Ausdr¨ ucken einen neuen S-Ausdruck konstruiert. Das funktionale Verhalten von cons ist gegeben durch: Definition 3.2-3 cons[α; β] = (α.β) Beispiele 3.2-3 cons[A; B] = (A.B) cons[(A.B); C] = ((A.B).C) cons[cons[A; B]; C] = ((A.B).C) 2. car und cdr Die Basisfunktionen car und cdr dienen zur Selektion des S-Ausdrucks α bzw. β aus einem S-Ausdruck (α.β). Das funktionale Verhalten von car und cdr ist gegeben durch: Definition 3.2-4
α1 car[α] = undef iniert
α2 cdr[α] = undef iniert
falls α = (α1 .α2 ) sonst . falls α = (α1 .α2 ) sonst .
F¨ ur eine Liste. (α1 . . . αn ) gilt dann: car[(α1 . . . αn )] = α1 cdr[(α1 . . . αn )] = (α2 . . . αn ) Beispiele 3.2-4 car[((A1.A2).B)] = (A1.A2) cdr[((A1.A2).B)] = B
112
3 Programmiersprachen
car[(A B C)] = A cdr[(A B C)] = (B C) car[(A)] = A cdr[(A)] = ( ) car[A] ist undefiniert 3.2.2.3 Vereinfachte Darstellung aufeinanderfolgender car‘s und cdr‘s Folgen von car’s und cdr’s k¨ onnen durch eine vereinfachte Darstellung repr¨ asentiert werden. Der Name einer solchen Funktion wird gebildet aus den Buchstaben c am Anfang und r am Ende. Dazwischen steht eine Folge von a’s (f¨ ur car) und d’s (f¨ ur cdr). Beispiele 3.2-5 cadr[(A B C)] ≡ car[cdr[(A B C)]] = B caddr[(A B C)] ≡ car[cdr[cdr[(A B C)]]] = C 3.2.2.4 Basispr¨ adikate fu ¨ r S-Ausdru ¨ cke Funktionen, deren Wert entweder true oder f alse sind, heißen Pr¨adikate. In LISP werden die Werte true und f alse durch die Atome T und F repr¨asentiert. 1. atom Die Funktion atom dient zur Unterscheidung von atomaren und nichtatomaren S-Ausdr¨ ucken. Definition 3.2-5
T falls S ein Atom atom[S] = F sonst Beispiele 3.2-6 atom[(U.V )] = F
atom[car[(U.V )]] = T
2. eq ¨ Die Funktion eq dient zur Uberpr¨ ufung der Gleichheit von atomaren SAusdr¨ ucken; f¨ ur nichtatomare S-Ausdr¨ ucke muß eine entsprechende Funktion (equal) programmiert werden.
3.2 LISP
113
Definition 3.2-6 ⎧ ⎪ falls S1, S2 Atome sind und S1 = S2 gilt ⎨T eq[S1 ; S2 ] = F falls S1, S2 Atome sind und S1 = S2 gilt ⎪ ⎩ undef iniert sonst In vielen LISP-Implementierungen ist eq eine total definierte Funktion, deren Wert sich im Falle von nichtatomaren Argumenten aus dem Vergleich der Speicheradressen von S1 und S2 ergibt, d.h. es gilt eq[S1 ; S2 ] = T , wenn S1 und S2 physikalisch identische S-Ausdr¨ ucke sind. Beispiele 3.2-7: eq[A; A] = T eq[A; B] = F eq[A; (A.B)] ist undefiniert 3.2.2.5 Bedingte Ausdru ¨ cke Bedingte Ausdr¨ ucke besitzen die allgemeine Form [p1 → e1 ; p2 → e2 ; . . . ; pn → en ], adikate und die ei Ausdr¨ ucke sind. Ein Paar (pi , ei ) heißt wobei die pi Pr¨ Klausel (clause). Die Klauseln werden von links nach rechts solange ausgewertet, bis das erste pi mit dem Wert T gefunden wird. Der Wert des Ausdrucks ist dann ei . Wird kein wahres Pr¨ adikat gefunden, so ist der Wert des bedingten Ausdrucks undefiniert. Bedingte Ausdr¨ ucke k¨ onnen auch geschachtelt auftreten. Beispiel 3.2-8 [eq[car[(A.B)]; A] → Y ES; T → N O] 3.2.2.6 Abstraktionen und Applikationen Die Abstraktion eines Ausdrucks E nach den Variablen x1 , . . . , xn erfolgt in LISP durch Bildung eines Lambda-Ausdrucks der Form λ[[x1 ; . . . ; xn ]; E]. Hierbei heißt [x1 , . . . , xn ] Variablenliste, E heißt Form, und die xi heißen Lambda-Variablen. Damit man sie nicht mit gleichnamigen Atomen verwechselt, werden sie mit Kleinbuchstaben bezeichnet.
114
3 Programmiersprachen
Beispiele 3.2-9 λ[[u; v]; cons[u; v]] λ[[x]; [atom[x] → x; T → cadr[x]]] Die Applikation eines Lambda-Ausdrucks bzw. einer Basisfunktion auf die Arubergabe gumente a1 , . . . , an wird mit f [a1 ; . . . ; an ] bezeichnet. Die Parameter¨ erfolgt gem¨ aß ’call by value’. Beispiele 3.2-10 1. λ[[u; v]; cons[u; v]][T ASCHEN ; RECHN ER] → (T ASCHEN.RECHN ER) 2. λ[[u; v]; λ[Cx; y]; cons[x; y]][car[v]; cdr[u]]][(A); (B)] → [λ[x; y]; cons[x : y]][B; ( )] → (B) 3.2.2.7 Rekursive Lambda-Ausdru ¨ cke Lambda-Ausdr¨ ucke k¨ onnen durch den Operator ’label’ mit einem (tempor¨ aren) Namen, bestehend aus Kleinbuchstaben, benannt werden. Dies erm¨oglicht die Definition von rekursiven Lambda-Ausdr¨ ucken ohne Benutzung eines Fixpunktkombinators. Beispiele 3.2-11 label [f f ; λ[[x]; [atom[x] → x; T → f f [car[x]]]]] 3.2.2.8 Funktionale Mit den bisher genannten Sprachkonzepten ist man in der Lage, Funktionen mit S-Ausdr¨ ucken als Argument und Funktionswert im applikativen Stil zu programmieren. LISP erm¨ oglicht jedoch auch Programmieren im funktionalen Stil, da man Funktionen programmieren kann, die funktionale Argumente oder funktionale Ergebnisse besitzen. Wir beschr¨anken uns hier auf zwei Beispiele: Der Lambda-Ausdruck λ[[f ]; f [f [(A B C)]]] kann auf die Basisfunktion cdr angewandt werden. Man erh¨alt das Ergebnis λ[[f ]; f [f [(A B C)]]][cdr] → cdr[cdr[(A B C)]] → (C)
3.2 LISP
115
Der Lambda-Ausdruck λ[[x]; λ[[y]; cdr[cdr[cons[x; y]]]]] ist eine Funktion, deren Ergebnis der Lambda-Ausdruck (Funktion) λ[[y]; cdr[cdr[cons[x; y]]]] ist. Zur vollst¨ andigen Auswertung m¨ ussen daher zwei Argumentgruppen zur Verf¨ ugung stehen, z.B. liefert die Auswertung von λ[[x]; λ[[y]; cdr[cdr[cons[x; y]]]]][A][(B C)] zun¨ achst λ[[y]; cdr[cdr[cons[A; y]]]][(B C)] und weiter cdr[cdr[cons[A; (B C)]]], und schließlich erh¨ alt man nach Auswertung der Basisfunktionen als Ergebnis (C). ur eine Funktion mit sowohl funktionalen Argumenten als auch Ein Beispiel f¨ funktionalen Ergebnissen ist λ[[f ]; λ[[x]; f [f [x]]]]. Die Anwendung auf die beiden Argumentgruppen [cdr] und [(A B C)] liefert λ[[f ]; λ[[x]; f [f [x]]]][cdr][(A B C)] → λ[[x]; cdr[cdr[x]]][(A B C)] → cdr[cdr[(A B C)]] → (C) 3.2.2.9 Zusammenfassung der Syntax 1. S-Ausdr¨ ucke < LET T ER > ::= A|B|C| . . . |Z < digit > ::= 0|1|2| . . . |9 < atomic symbol > ::= < LET T ER > < atom part > < atom part > ::= < empty > | < LET T ER > < atom part > | < digit >< atom part >
116
3 Programmiersprachen
< S − expression> ::= < atomic symbol > | (< S − expression > . < S − expression >) | (< S − expression> · · · < S − expression>) | ( ) 2. M-Sprache ::= 0|1| . . . |9 ::= a|b| . . . |z ::= < letter > < id part > ::= < empty > | < letter > < id part > | < digit > < id part > < f unctional f orm > ::= < f unction > < f orm > ::= < constant > | < variable > | < f unction f orm > | < f unction > [< argument >; . . . ; < argument >]| < f orm > [< argument >; . . . ; < argument >]| [< f orm > → < f orm >; . . . ; < f orm > → < f orm >] < constant > ::= < S − expression> < variable > ::= < identif ier > < var list > ::= [< variable >; . . . ; < variable >] < argument > ::= < f orm > | < f unction > < f unction > ::= < identif ier > | λ[< var list >; < f orm >]| label [< identif ier > ; < f unction >] < digit > < letter > < identif ier > < id part >
¨ 3.2.2.10 Ubersetzung von Programmen der M-Sprache in S-Ausdru ¨ cke Da der Interpretierer ein LISP-Programm ist, das als Eingabe S-Ausdr¨ ucke verlangt, m¨ ussen die zu interpretierenden Funktionen zun¨achst in einen SAusdruck transformiert werden. Dies geschieht folgendermaßen: 1. Eine Form f [arg1 ; . . . ; argn ] wird u ¨bersetzt in (f ∗ arg1∗ . . . argn∗ ). ∗ ¨ von x. Hierbei ist x die Ubersetzung ¨ 2. Ist E ein S-Ausdruck, dann ist (QU OT E E) das Ubersetzungsresultat. ¨ Ublich ist auch die Konvention (QU OT E E) ≡ E 3. Namen von Funktionen oder Pr¨ adikaten und Variablen werden in die gleichnamigen Atome u ¨bersetzt. 4. Ein bedingter Ausdruck [p1 → e1 ; . . . ; pn → en ] wird in (CON D(p∗1 e∗1 ) . . . (p∗n e∗n )) u ¨bersetzt. 5. Ein Lambda-Ausdruck λ[[x1 ; . . . ; xn ]; E] wird in (LAM BDA(x∗1 . . . x∗n )E ∗ ) u ¨bersetzt.
3.2 LISP
117
6. Eine Label-Deklaration label [f ; e] wird in (LABEL f ∗ e∗ ) u ¨bersetzt. 7. Ein funktionales Ergebnis f oder funktionales Argument wird in (F U N CT ION f ∗ ) u ¨bersetzt. Beispiele 3.2-12 M-Ausdr¨ ucke car[x] T
S-Ausdr¨ ucke (CAR X) (QU OT E T )
label [f f ; λ[[x]; [atom[x] → x; (LABEL F F (LAM BDA(X) T → car[x]]]] (CON D ((AT OM X)X) ((QU OT E T ) (CAR X))))) f f [car[x]] atom[x] → x; T → f f [car[x]]
λ[[f ]; λ[[x]; f [f [x]]]]
(F F (CAR X)) (CON D ((AT OM X) X)) ((QU OT E T ) (F F (CAR X))) (LAM BDA (F ) (F U N CT ION (LAM BDA (X) (F (F X)))))
3.2.2.11 Beispiele In diesem Abschnitt werden eine Reihe von Beispielen f¨ ur LISP-Programme angegeben. Sie dienen gleichzeitig als Hilfsfunktionen f¨ ur den Interpretierer und erwarten daher stets eine korrekte Eingabe. 1. equal [x; y] Als Ergebnis dieser Funktion wird T abgeliefert, falls die beiden Argumente identische S-Ausdr¨ ucke sind, und F sonst (zur Erinnerung: Das Pr¨ adikat eq war nur f¨ ur Atome definiert). Die Definition von equal ist ein Beispiel f¨ ur eine geschachtelte Verwendung eines bedingten Ausdrucks.
118
3 Programmiersprachen
label [equal; λ[ [x; y]; [atom[x] → [atom[y] → eq[x; y]; T →F ]; atom[y] → F ; equal[car[x]; car[y]] → equal[cdr[x]; cdr[y]]; T →F ]]] ¨ Zur Demonstration sei noch einmal die Ubersetzung in einen S-Ausdruck angegeben: (LABEL EQU AL (LAM BDA (X Y ) (CON D ((AT OM X) (CON D ((AT OM Y )(EQ X Y )) ((QU OT E T )(QU OT E F )))) ((AT OM Y ) (QU OT E F )) ((EQU AL (CAR X)(CAR Y )) (EQU AL( CDR X) (CDR Y ))) ((QU OT E T ) (QU OT E F ))) 2. subst [x; y; z] Die Funktion subst liefert als Ergebnis einen S-Ausdruck z, der aus z dadurch entsteht, daß jedes Vorkommen von y in z durch x ersetzt wird. label [subst; λ[[x; y; z]; [equal[y; z] → x; atom[z] → z; T → cons[subst[x; y; car[z]]; subst[x; y; cdr[z]] ]]]] Beispiel subst[(X.A); B; ((A.B).C)] = ((A.(X.A)).C) 3. null [x] Die Funktion null liefert als Ergebnis T , falls das Argument N IL ist und F sonst. λ[[x]; [eq[x; N IL] → T ; T → F ]] 4. append [x; y] Die Funktion append verbindet ihre beiden Argumente zu einer Liste.
3.2 LISP
119
label[append; λ[[x, y]; [null[x] → y; T → cons[car[x]; append[cdr[x]; y]] ]]] Beispiel append[(A B); (C D E)] = (A B C D E) Man betrachte hierbei den Unterschied zu cons[x; y] cons[(A B); (C D)] = ((A B).(C D E)) = ((A B) C D E) 5. member [x; y] Die Funktion member liefert als Ergebnis T , falls der S-Ausdruck x ein Element der Liste y ist, und F sonst. label[member; λ[[x; y]; [null[y] → F ; equal[x; car[y]] → T ; T → member[x; cdr[y]] ]]] Beispiel member[C; (A B C)] liefert zun¨ achst member[C; (B C)], danach member[C; (C)] und sodann T . 6. pairlis [x; y; a] Die Funktion pairlis faßt jeweils das 1., 2., usw. Element von x bzw. y zu einem Paar zusammen und h¨ angt diese Paare vor die Liste a. label[pairlis; λ[[x; y; a]; [null[x] → a; T → cons[cons[car[x]; car[y]]; pairlis[cdr[x]; cdr[y]; a] ]]]] Beispiel pairlis[(A B C); (U V W ); ((D.X)(E.Y ))] = ((A.U ) (B.V ) (C.W ) (D.X) (E.Y )) Der Interpretierer erzeugt mit Hilfe der Funktion pairlis eine Liste von Paaren, entsprechend einer Tabelle mit zwei Spalten. Diese Tabelle wird ’Assoziierungs-Liste’ (A-Liste) genannt. Die A-Liste besteht aus S-Ausdr¨ ucken, die aus dem Namen der Variablen und ihrem Wert gebildet
120
3 Programmiersprachen
sind. Sie dient dazu, beim Auftreten einer Variablen ihren aktuellen Wert zu finden. 7. assoc [x; a] Bei den Ausf¨ uhrungen zu 6. wurde erl¨ autert, daß die A-Liste von dem Interpretierer benutzt wird, um den aktuellen Wert einer Variablen zu finden. Dieser Suchprozess wird mit Hilfe der Funktion assoc durchgef¨ uhrt. Die Funktion assoc sucht die A-Liste a von links nach rechts durch, bis zum ersten Mal ein Element gefunden wird, dessen erste Komponente x ist. Dieses Element ist das Ergebnis der Funktion assoc. label[assoc; λ[[x; a]; [equal[caar[a]; x] → car[a]; T → assoc[x; cdr[a]]]]] Beispiel assoc[B; ((A.(M N )) (B.(CAR X)) (C.(QU OT E M )) (B.(CDR X)))] = (B.(CAR X)) 8. sublis [a; y] F¨ ur die Parameter von sublis wird vorausgesetzt, daß a eine A-Liste der Art ((α1 .β1 ) . . . (αn .βn )) ist, wobei die αi Atome sind, und daß y ein beliebiger S-Ausdruck ist. Die Funktion sublis ersetzt jedes αi in y durch das entsprechende βi . Um sublis zu definieren, ben¨otigen wir noch eine Hilfsfunktion: label[sub2; λ[[a; z]; [null[a] → z; eq[caar[a]; z] → cdar[a]; T → sub2[cdr[a]; z] ]]] label[sublis; λ[[a; y]; [atom[y] → sub2[a; y]; T → cons[sublis[a; car[y]]; sublis[a; cdr[y]] ] ]]] Beispiel sublis[((X.GOET HE) (Y.F AU ST )); (X SCHRIEB DEN Y )] = (GOET HE SCHRIEB DEN F AU ST )
3.2 LISP
121
3.2.2.12 Der Interpretierer ¨ Zur besseren Ubersichtlichkeit ist der nachfolgende Interpretierer in einzelne Module aufgespalten worden. Um ein syntaktisch korrektes LISP-Programm zu erhalten, m¨ ussen die einzelnen Module und die Hilfsfunktionen geeignet ineinander verschachtelt werden bzw. mit der hier nicht vorgestellten Pseudofunktion ’define’ deklariert werden (s. Kap. 3.2.3.2). evalquote = λ[[f n; x]; apply[f n; x; N IL]] apply = λ[[f n; x; a]; [null[f n] → error[A10]; atom[f n] → eq[f n; CAR] → caar[x]; eq[f n; CDR] → cdar[x]; eq[f n; AT OM ] → atom[car[x]]; eq[f n; CON S] → cons[car[x]; cadr[x]]; eq[f n; EQ] → eq[car[x]; cadr[x]]; T → apply[cdr[sassoc[f n; a; λ[[]; error[A2]]]]; x; a]]; eq[car[f n]; LAM BDA] → eval[caddr[f n]; pairlis[cadr[f n]; x; a]]; eq[car[f n]; LABEL] → apply[caddr[f n]; x; cons[cons[cadr[f n]; caddr[f n]]; a]]; 10 eq[car[f n]; F U N ARG] → apply[cadr[f n]; x; caddr[f n]]; 11 T → apply[eval[f n; a]; x; a]]]
1 2 3 4 5 6 7 8 9
12 13 14 15 16 17 18 19 20
eval = λ[[e; a]; [null[e] → N IL; atom[e] → [eq[e; T ] → T ; eq[e; F ] → F ; T → cdr[sassoc[e; a; λ[[]; error[A8]]]]]; atom[car[e]] → [eq[car[e]; QU OT E] → cadr[e]; eq[car[e]; CON D] → evcon[cdr[e]; a]; eq[car[e]; F U N CT ION ] → list[F U N ARG; cadr[e]; a]; T → apply[car[e]; evlis[cdr[e]; a]; a]]; T → apply[car[e]; evlis[cdr[e]; a]; a]]] evcon = λ[[l; a]; [null[l] → error[A3]; eval[caar[l]; a] → eval[cadar[l]; a]; T → evcon[cdr[l]; a]]] evlis = λ[[m; a]; [null[m] → N IL; T → cons[eval[car[m]; a]; evlis[cdr[m]; a]]]] list = λ[[x; y; z]; cons[x; cons[y; cons[z; N IL]]]]
122
3 Programmiersprachen
sassoc = λ[[x; y; u]; [null[y] → u[ ]; eq[caar[y]; x] → car[y]; T → sassoc[x; cdr[y]; u]]] Die Funktion sassoc unterscheidet sich von assoc durch eine dritte Variable u, deren Wert eine nullstellige Funktion, z.B. eine Fehlermeldung sein muß. Sie wird aufgerufen, wenn der Identifikator x nicht in der A-Liste y gefunden wird (Z.7 und Z.15). pairlis = λ[[x; y; a]; [null[x] → [null[y] → a T → error[F 2]]; null[y] → error[F 3]; T → cons[cons[car[x]; car[y]]; pairlis[cdr[x]; cdr[y]; a]]]] Diese Version der Funktion pairlis ist gegen¨ uber der alten Version lediglich um die Fehlerbehandlung erweitert worden. Bedeutung der Fehlermeldungen A2 A3 A8 F2 F3 A10
: : : : : :
Funktion undefiniert Kein Pr¨ adikat erf¨ ullt evcon Variable hat keinen Wert Erste Argument-Liste zu kurz in pairlis Zweite Argument-Liste zu kurz in pairlis Keine Funktion auf Funktionsposition
Erl¨ auterungen Der LISP-Interpretierer besteht aus den beiden Hauptfunktionen eval und apply und einer Reihe von Hilfsfunktionen. Die Funktion apply behandelt die Applikation von Funktionen auf ihre Argumente, w¨ahrend eval den Wert von Formen berechnet. A. Der Start des Interpretierers erfolgt durch den Aufruf von evalquote mit einer Funktion f n (als S-Ausdruck) und ihren Argumenten x B. Die Funktion evalquote ruft apply auf, u ¨bergibt dabei f n und x und initiiert die A-Liste mit N IL. Mit Hilfe dieser A-Liste (Assoziierungsliste) werden die Bindungen von Lambda-Variablen an Argumente implementiert. Sie besteht aus S-Ausdr¨ ucken, deren linker Teil der Name einer Variablen und deren rechter Teil ihren Wert darstellt. Wird der Wert einer Variablen ben¨ otigt, so sucht der Interpretierer die A-Liste von links nach rechts durch, bis zum ersten Mal ein S-Ausdruck gefunden wird, dessen linker Teil der Name der Variablen ist. Der zugeh¨orende rechte Teil ist der gesuchte Wert.
3.2 LISP
123
Funktionale erfordern eine spezielle Behandlung: Trifft eval auf F U N CT ION , so legt eval in der A-Liste ein Tripel an. Das erste Element ist das Sonderatom F U N ARG, das zweite Element ist die nachfolgende Funktion selbst, und das dritte Argument ist die momentane A-Liste. Hierdurch werden die beim Auftreten von F U N CT ION g¨ ultigen Bindungen von Variablen und Werten f¨ ur eine sp¨atere Benutzung bei Aufrufen der Funktion aufbewahrt. Trifft apply auf F U N ARG, so nimmt apply die Funktion aus dem zweiten Element des Tripels und die ’alte’ A-Liste, das dritte Element des Tripels, in der die Werte der globalen Gr¨ oßen bestimmt werden. Das AListen-Konzept ist in modernen LISP-Systemen durch effizientere Techniken ersetzt worden, die sich in dem hier vorgestellten Pure-LISP schwer ausdr¨ ucken lassen. Die Prinzipien einiger dieser Techniken sind jedoch in Kapitel 4.1 erl¨ autert. C. In der Funktion apply werden die folgenden F¨alle unterschieden: 1. f n ist ein Atom (Z.2) Dann gibt es zwei M¨ oglichkeiten: i. Ist f n eine Basisfunktion, so wird sie unmittelbar auf die Argumente angewandt (Z.2-Z.6). ii. Ist f n keine Basisfunktion, so wird ihre Bedeutung aus der A-Liste ermittelt (Z.7). 2. f n beginnt mit LAM BDA (Z.8). Die Argumente werden gem¨ aß dem A-Listen-Konzept an die entsprechenden Variablen gebunden, und die Form des Lambda-Ausdrucks wird zwecks Auswertung an eval u ¨bergeben. 3. f n beginnt mit LABEL (Z.9). Der Name der Funktion und ihre Definition werden in der A-Liste abgelegt. Die Funktionsdefinition wird durch apply ausgewertet. 4. f n ist eine Funktion, die mit F U N ARG markiert ist (Z.10). An apply werden zur Auswertung u ¨bergeben: Die Funktion selbst, die Argumente und die im F U N ARG-Tripel als 3. Komponente enthaltene ’alte’ A-Liste. 5. f n ist ein Ausdruck, dessen funktionaler Wert durch eval ermittelt wird und durch apply auf die Argumente x angewandt wird (Z.11). D. Das erste Argument e von ’eval’ ist eine Form; es werden die folgenden F¨ alle unterschieden:
124
3 Programmiersprachen
1. Eine leere Liste e hat sich selbst zum Wert (Z.12). 2. e ist ein Atom (Z.13-Z.l5). Ist e eine Variable, so wird ihr Wert aus der A-Liste ermittelt (Z.l5); die Sonderatome T und F haben sich stets selbst als Wert. 3. e beginnt mit QU OT E (Z.16). ¨ e ist die Ubersetzung eines S-Ausdrucks, den man durch cadr[e] als Wert von e erh¨ alt. 4. e beginnt mit CON D (Z.17). Es liegt ein bedingter Ausdruck vor. ’evcon’ wertet sukzessive die Pr¨ adikate aus, bis sich zum ersten Mal T ergibt und u ¨bergibt die zu diesem Pr¨ adikat geh¨ orende Form an eval. 5. e beginnt mit F U N CT ION (Z.18). Es wird ein F U N ARG-Tripel erstellt, bestehend aus dem Sonderatom F U N ARG, der nach F U N CT ION folgenden Funktion selbst und der momentanen A-Liste. 6. e ist die Applikation einer Funktion auf unausgewertete Argumente (Z.19 u. Z.20). Die Liste der Argumente wird durch evlis ausgewertet und zusammen mit der Funktion an apply u ¨bergeben. 3.2.2.13 Interpretation eines Beispiels Zu beachten ist stets, daß die universelle LISP-Funktion auf S-Ausdr¨ ucken operiert. Liegt z.B. das LISP-Programm λ[[x; y]; cons[car[x]; y]][(A B); (C D)] vor, so erfolgt der Aufruf von ’evalquote’ durch evalquote[(LAM BDA (X Y ) (CON S (CAR X) Y )); ((A B) (C D))]. 1. Der Aufruf evalquote [(LAM BDA (X Y ) (CON S (CAR X) Y )); ((A B) (C D))] f¨ uhrt zu dem Aufruf apply[f n; x; a] ≡ apply [(LAM BDA (X Y ) (CON S (CAR X) Y )); ((A B) (C D)); N IL] Durch ≡ soll die Zuordnung von Variablen und Argumenten verdeutlicht werden.
3.2 LISP
125
2. Da car[f n] = LAM BDA, erfolgt eval[caddr[f n]; pairlis [cadr[f n]; x; a]] ≡ eval[ caddr [(LAM BDA (X Y ) (CON S (CAR X) Y ))]; pairlis[ cadr[ (LAM BDA (X Y ) (CON S (CAR X) Y ))]; ((A B) (C D)); N IL]] → eval[(CON S(CAR X)Y ); pairlis[(X Y ); ((A B)(C D)); N IL]] Da die Auswertung von pairlis[(X Y ); ((A B) (C D)); N IL] die A-Liste ((X.(A B)) (Y.(C D))) liefert, erh¨ alt man eval[e; .a] ≡ eval[(CON S (CAR X) Y ); ((X.(A B)) (Y.(C D)))] 3. Da car[e] das Atom CON S ist, erfolgt apply[car[e]; evlis[cdr[e]; a]; a] (*) ≡ apply [ car[(CON S (CAR X) Y )]; evlis[cdr[(CON S (CAR X) Y )]; a]; a] 3.1 Wir betrachten zun¨ achst den Aufruf von ’evlis’: evlis[cdr[(CON S (CAR X) Y )]; a] → evlis[((CAR X) Y ); a] → cons[eval[car[((CAR X) Y )]; a]; evlis[cdr[((CAR X) Y )]; a]] → cons[eval[(CAR X); a]; evlis[(Y ); a]] (**) Zu betrachten sind nun eval[(CAR X); a] und evlis[(Y ); a] : 3.1.1 eval[(CAR X); a] → apply[ car[(CAR X)]; evlis[cdr[(CAR X)]; a]; a] → apply[ CAR; evlis[(X); a]; a] → apply[ CAR; cons[eval[car[(X)]; a]; evlis[cdr[(X)]; a]]; a] → apply[ CAR; cons[cdr[sassoc[X; a; λ[[] . . . ]]]; N IL]; a]
126
3 Programmiersprachen
≡ apply[ CAR; cons[cdr[sassoc[X; ((X.(A B))(Y.(C D))); λ[[] . . . ]]]; N IL]; a] → apply[ CAR; cons[cdr[(X.(A B))]; N IL]; a] → apply[ CAR; cons[(A B); N IL]; a] → apply[ CAR; ((A B)); a] → caar [((A B))] →A 3.1.2 evlis[(Y ); a] → cons [eval[car[(Y )]; a]; evlis[cdr[(Y )]; a]] → cons [eval[Y ; a]; evlis[N IL; a]] → cons [eval[Y ; a]; N IL] → cons [cdr[sassoc[Y ; a; . . . ]]; N IL] → cons [cdr[sassoc[Y ; ((X.(A B)) (Y.(C D))); . . . ]]; N IL] → cons [cdr[(Y.(C D))]; N IL] → cons [(C D); N IL] → ((C D)) Durch Einsetzen in (**) erh¨ alt man cons[A; ((C D))] → (A (C D)) Durch Einsetzen in (*) erh¨alt man apply[ car[(CON S (CAR X) Y )]; (A (C D)); a] → apply[CON S; (A (C D)); a] → cons[car[(A (C D))]; cadr[(A (C D))]] → cons[A; (C D)] → (A C D) 3.2.3 LISP-Programmiersysteme Obwohl Pure-LISP eine universelle Programmiersprache ist, bleiben vielf¨ altige W¨ unsche nach Komfort und Effizienz offen. Beispielsweise ist die Arithmetik mit λ-Termen, wie sie im Kapitel 2.2.4 vorgestellt wurde, offensichtlich sehr ineffizient. Man w¨ unscht sich weiterhin M¨oglichkeiten, Funktionsdefinitionen permanent zu speichern oder die Ein-/Ausgabe von Daten auf Dateien zu programmieren. Weiterhin ist der Ausbau zu einem interaktiven
3.2 LISP
127
Programmiersystem naheliegend wegen der interpretativen Ausf¨ uhrung von LISP-Programmen. Die heute gebr¨ auchlichen, großen LISP-Programmiersysteme sind so komplex und umfangreich, daß sie in allen Einzelheiten kaum noch u ¨berschaubar sind und auch untereinander teilweise schwer vergleichbar sind. Hier liegen erhebliche Probleme f¨ ur einen Anf¨ anger, und nicht nur f¨ ur ihn. Kenntnisse des Quasi-Standards“ von LISP, wie er im LISP-1.5 Manual (McCarthy ” 1962) definiert worden ist, alleine reichen heute nicht mehr aus, um in einem zur Verf¨ ugung stehenden LISP-Dialekt zu programmieren. Es ist allerdings abzuwarten, ob sich die aktuellen Standardisierungsbem¨ uhungen gegen¨ uber der Tradition durchsetzen werden, LISP-Programmiersysteme an die speziellen Gegebenheiten einer Anwendung zu adaptieren. Es bereitet in der Regel keine Probleme, neue Features“ einzubauen bzw. vorhandene abzu¨andern, ” denn die meisten Teile von LISP-Programmiersystemen sind in LISP selbst programmiert. Gemeinsam ist jedoch allen Systemen als Kern die Sprache Pure-LISP mit dem Interpretierer. Von hier aus erh¨ alt man einen Zugang zu den unterschiedlichen Komponenten eines LISP-Programmiersystems, seien sie nun in LISP selbst implementiert oder als Erweiterung des Interpretierers. Stellvertretend f¨ ur die Familie der im Detail doch recht unterschiedlichen LISPProgrammiersysteme und auch f¨ ur die moderneren Nachfolgesysteme und sprachen von LISP wollen wir im folgenden das INTERLISP-System der Firma SIEMENS (INTERLISP 1985) betrachten, welches urspr¨ unglich bei Bolt, Beranek und Newman Inc. f¨ ur den Rechner DEC PDP-l0 entwickelt wurde. Das Pr¨ afix INTER“ soll an interactive“ erinnern. Anhand von einigen aus” ” gew¨ ahlten Konzepten, die mit Varianten in vielen LISP-Systemen auftreten, soll aufgezeigt werden, durch welche Maßnahmen Pure-LISP zu einem komfortablen und effizienten Programmiersystem erweitert werden kann. 3.2.3.1 Datenstrukturen Eine erste wesentliche Erweiterung von Pure-LISP entsteht durch die Hinzunahme von neuen Atomen, n¨ amlich von Zahlen, Zeichenketten (strings) und Feldern (arrays), sowie der zugeh¨ origen Basisfunktionen bzw. Pr¨adikate. Damit verbunden ist ein Typenkonzept f¨ ur diese Datenstrukturen. Zur Darstellung greifen wir auf die graphische Notation von S-Ausdr¨ ucken (Kap.3.2.2.1) zur¨ uck und f¨ ugen maschinennahe Einzelheiten der INTERLISP-Darstellung von S-Ausdr¨ ucken hinzu. Im LISP-Jargon heißen die Knoten LISP-Zellen“ ” und die Kanten Pointer“. Zur Differenzierung heißen die in Kapitel 3.2.2.1 ” eingef¨ uhrten Atome literale Atome“. Pointer werden systemintern durch ” Speicherworte zu 4 Bytes dargestellt.
V irtuelle Adresse
Datentypkennzeichnung
128
3 Programmiersprachen
Die virtuelle Adresse wird im Betriebssystem durch Adressumsetzung in eine reelle Hauptspeicheradresse u uhrt. Die Datentypkennzeichnung gibt an, ¨berf¨ welchen Datentyp der S-Ausdruck besitzt, auf den der Verweis zeigt. Hier ein Auszug: Datentyp
Kennzeichnung (hexadezimal)
LISP-Zellen kleine ganze Zahlen große ganze Zahlen Gleitkommazahlen doppelt genaue Gleitkommazahlen literale Atome Felder Zeichenketten
15 16 17 1A 1B 1E 1F 23
Die Datentypenkennzeichung kann f¨ ur jeden S-Ausdruck abgefragt werden. Im einzelnen haben die verschiedenen S-Ausdr¨ ucke folgende interne Darstellung: LISP-Zellen Eine LISP-Zelle wird in einem Doppelwort dargestellt entsprechend der graphischen Notation f¨ ur S-Ausdr¨ ucke
V irtuelle Adresse
V irtuelle Adresse
der CAR − Komponente der CDR − Komponente Datentypkennzeichnung Kleine-ganze-Zahlen Die Bin¨ arverschl¨ usselung dieser ganzen Zahlen ist in drei Bytes darstellbar und wird direkt in dem Pointer gespeichert. 16
Bin¨ arverschl¨ usselung
Große-ganze-Zahlen Die virtuelle Adresse im Pointer verweist auf ein Speicherwort, das die Bin¨ arverschl¨ usselung der ganzen Zahl enth¨ alt.
3.2 LISP
17
129
Bin¨ arverschl¨ usselung
Gleitkommazahlen Die virtuelle Adresse im Pointer verweist auf ein Speicherwort, das Vorzeichen, Charakteristik und Mantisse der Gleitkommazahl enth¨alt. 1A
Mantisse Charakteristik V orzeichenbits
Doppelt genaue Gleitkommazahl Die Mantisse ist um 4 Bytes verl¨ angert. 1B
Mantisse Charakteristik V orzeichenbits
Literale Atome Die virtuelle Adresse im Pointer verweist auf das zweite von vier zusammenh¨ angenden Speicherw¨ ortern. Jedes literale Atom wird h¨ochstens einmal abgespeichert. Deshalb k¨ onnen sie eindeutig an dem Pointer identifiziert werden, was z.B. bei der Implementation von EQ ausgenutzt wird. Erzeugt werden literale Atome beim Einlesen von S-Ausdr¨ ucken. 1E
Kennzeichnung
L¨ ange des print − name print − name
globaler W ert
P roperty−Liste
F unktionsdef inition
130
3 Programmiersprachen
Als print-name“ ist die Zeichenfolge gespeichert, die das literale Atom be” zeichnet. Jedes literale Atom hat einen globalen Wert, auf den explizit mit GETTOPVAL bzw. SETTOPVAL zugegriffen werden kann. Es gibt spezielle Variablen, d.h. literale Atome des INTERLISP-Systems, die einen globalen Wert besitzen, um bestimmte Interaktionen zu steuern. Normalerweise werden jedoch Variablenbindungen mit Hilfe eines Kellers gespeichert. Nur f¨ ur den Fall, daß im Keller keine Bindung existiert, wird auf den globalen Wert der betreffenden Variablen zur¨ uckgegriffen. In Kapitel 4.1.2 wird eine Implementierungstechnik f¨ ur Variablenbindungen vorgestellt, die auf dem Vorhandensein von globalen Werten beruht. Man nennt sie shallow binding“. In der Property Liste“ eines literalen Atoms ” ” k¨ onnen weitere global g¨ ultige Werte abgespeichert werden (GETPROP, PUTPROP). Wenn das literale Atom eine Funktion bezeichnet, so ist die Funktionsdefinition im letzten Wert gespeichert. Das Atom CONS besitzt hier die Kennzeichnung ’00’ und die Startadresse des Unterprogramms f¨ ur cons. Felder In INTERLISP sind Felder eindimensional. Sie besitzen dynamische Indexgrenzen und enthalten als Elemente Pointer, die allerdings unterschiedliche Datentypkennzeichnungen tragen k¨ onnen. Das Anlegen eines Feldes erfolgt u ¨ber eine Spezialfunktion genauso wie der Zugriff auf Elemente. Systemintern ist ein Feld ein zusammenh¨ angender Speicherbereich folgender Art: 1F
⎫ ⎪ ⎪ ⎬ Informationen u ¨ber ⎪ das Feld ⎪ ⎭ ⎫ ⎪ ⎪ ⎪ ⎪ ⎬Feldelemente als Pointer ⎪ ⎪ ⎪ ⎪ ⎭
Zeichenketten Die virtuelle Adresse im Pointer verweist indirekt auf die Folge der Zeichen 23
n
z1
z2
z3
zn
3.2 LISP
131
Die Erweiterung von Pure-LISP um neue Atome macht es erforderlich, u ¨ber die 5 in Kapitel 3.2.2.2 definierten Basisfunktionen hinaus weitere einzuf¨ uhren. Als Beispiel betrachten wir einige arithmetische Operationen: iplus[x1 ; x2 ; . . . ; xn ] = x1 + x2 + · · · + xn idif f erence[x; y] = x − y itimes[x1 ; x2 ; . . . ; xn ] = x1 ∗ x2 ∗ · · · ∗ xn iquotient[x; y] = x ÷ y
T igreaterp[x; y] = F
falls x > y sonst
zerop[x] = egp[x; 0] Diese Funktionen bzw. Pr¨ adikate sind nur f¨ ur ganzzahlige Argumente definiert. Bei nichtnumerischen Argumenten erfolgt eine Fehlermeldung. F¨ ur Gleitkommazahlen benutzt man sinngem¨ aß f plus, f dif f erence, f times, f greaterp und egp[x; 0]. Folgende Funktionen bzw. Pr¨adikate f¨ uhren eine implizite Typanpassung bei numerischen Argumenten durch; sie sind jedoch f¨ ur nichtnumerische Argumente undefiniert: plus, dif f erence, times, quotient, greaterp. Der Vergleich von Zahlen n, m erfolgt durch eqp[n; m] = T , falls n = m. Durch die Pr¨ adikate f ixp[n] und f loatp[n] kann man feststellen, ob n ganzzahlig bzw. eine Gleitkommazahl ist. In ¨ ahnlicher Weise wie bei den arithmetischen Operationen sind auch die ur die u Basisfunktionen bzw. Pr¨adikate f¨ ¨brigen atomaren Datenstrukturen nur f¨ ur Argumente des korrekten Typs definiert. Man kann also in INTERLISP mit diesen Datenstrukturen getypt programmieren, wenn man in jede benutzerdefinierte Funktion die diesbez¨ uglichen Typ¨ uberpr¨ ufungen einbezieht. H¨ohere funktionale Typen gibt es jedoch in INTERLISP nicht. Im Vergleich zum ungetypten λ-Kalk¨ ul und zu Pure-LISP liegt also ein eingeschr¨anktes Typkonzept f¨ ur atomare Datenstrukturen vor. 3.2.3.2 Pseudofunktionen Im LISP-Jargon werden Basisfunktionen, bei deren Aufruf es weniger auf den Funktionswert als auf einen Nebeneffekt ankommt, als Pseudofunktionen“ ” bezeichnet. Man benutzt sie zur Implementierung von Konzepten, die sich in Pure-LISP nur umst¨ andlich ausdr¨ ucken lassen. Als Beispiele betrachten wir Pseudofunktionen zur Ein-/Ausgabe von S-Ausdr¨ ucken und zur Vereinbarung von global g¨ ultigen Funktionsdefinitionen.
132
3 Programmiersprachen
Sei dl eine Liste mit der speziellen Struktur dl = ((f1 d1 ) . . . (fn dn )), wobei fj ein literales Atom ist und dj eine Lambda-Funktion in Datensprache. Dann ist die Pseudofunktion define gegeben durch
N IL falls x = dl def ine[x] = undef iniert sonst Als Nebeneffekt werden die Funktionsdefinitionen d1 , . . . , dn unter den Naugbar gemen f1 , . . . , fn abgespeichert und im INTERLISP-System global verf¨ macht. Man kann also mit der Pseudofunktion define einem LISP-Programm einen Deklarationsteil voranstellen, in dem h¨ aufig benutzte Funktionen vereinbart werden. Damit umgeht man die etwas umst¨andliche Programmierung mit LABEL. Beispielsweise kann man die Funktion null[x] folgendermaßen vereinbaren: DEF IN E(((N U LL (LAM BDA(X) (EQ X N IL))) )) f1
d1
dl Den Interpretierer des INTERLISP-Systems kann man sowohl, wie in Kapitel 3.2.5 beschrieben, u ¨ber die Funktion evalquote aufrufen als auch u ¨ber die Funktion eval. Das obige Beispiel ist eine Eingabe f¨ ur evalquote. In der Implementierung von def ine wird der Nebeneffekt jeweils durch die vierte Komponente der literalen Atome f1 , . . . , fn erreicht, in der die Funktionsdefinitionen d1 , . . . , dn als Pointer abgespeichert werden. In dem zur Typkennzeichnung vorgesehenen Byte wird durch ’04’ bis ’07’ kodiert, daß es sich um eine in Listenform vorliegende Funktion handelt und wie die Parameter¨ ubergabe zu erfolgen hat (siehe Kap. 3.2.3.4). Wenn man sich auf den Standpunkt stellt, daß Funktionsvereinbarungen bei den systemintern eindeutig dargestellten literalen Atomen ad¨ aquat abgespeichert sind, dann erstreckt sich der G¨ ultigkeitsbereich einer Funktionsvereinbarung u ¨ber das Programm hinaus, in dem die Vereinbarung vorkommt. Literale Atome werden n¨amlich unabh¨ angig von einzelnen LISP-Programmen verwaltet! Komfortabler als define ist die Pseudofunktion de: ⎧ ⎪ falls f Atom, args = (a1 , . . . , an ) ⎨N IL de[f ; args; body] = und body ein Ausdruck ist ⎪ ⎩ undef iniert sonst Die Argumente von de werden nicht evaluiert. Der Nebeneffekt besteht in der Vereinbarung der Funktionsdefinition λ[[a1 ; . . . ; an ]; body] unter dem Namen f analog zu def ine. Die Vereinbarung der Funktion null[x] verk¨ urzt sich als
3.2 LISP
133
Eingabe f¨ ur eval zu N IL)) (DE N LL (X) (EQ X U . f args body Die Ein-/Ausgabe von S-Ausdr¨ ucken beschr¨ ankt sich bei Pure-LISP auf das Notwendige. Eine flexible Programmierung der Ein-/Ausgabe ist in INTERLISP durch eine Vielzahl von Pseudofunktionen erm¨oglicht. Hier eine kleine Auswahl: input[f ile] = f ile Funktionswert ist die vorhergehende Prim¨ areingabedatei f ile . Als Nebeneffekt wird f ile die g¨ ultige Prim¨ areingabedatei. read[f ile] = e Der Funktionswert e ist ein von der Datei f ile gelesener S-Ausdruck. print[x; f ile] = x Als Nebeneffekt wird der S-Ausdruck x auf die Datei f ile ausgegeben. close all[] = (f ile1 , . . . , f ilen ) Funktionswert ist die Liste aller geschlossenen Dateien; Nebeneffekt ist das Schließen dieser Dateien. 3.2.3.3 Standardfunktionen Im LISP-Jargon versteht man unter dem diffusen Begriff Standardfunktion“ ” eine Sammelbezeichnung f¨ ur alle diejenigen Funktionen f n, die man in einem Programm unter dem Identifikator f n aufrufen kann, ohne daß in dem Programm selbst eine Vereinbarung einer Funktionsdefinition f¨ ur f n steht. In diesem Sinne haben wir bereits Standardfunktionen kennengelernt, n¨amlich Basisfunktionen, Pseudofunktionen, sowie Funktionen, die mit def ine vordefiniert wurden. Im INTERLISP-System gibt es mehrere hundert Standardfunktionen, die in unterschiedlicher Weise implementiert sind. Ganz grob kann man drei Arten unterscheiden: In LISP selbst programmierte, in Maschinencode compilierte und im Maschinencode von Hand programmierte Standardfunktionen. Einem Benutzer ist es im Prinzip m¨oglich, durch die Definition von neuen Standardfunktionen bzw. durch die Modifikation von vorhandenen, das INTERLISP-System ganz individuell an ein Anwendungsgebiet anzupassen. Bequemer ist es jedoch, benutzerspezifische Standardfunktionen auf einer speziellen Datei zu speichern, die beim Starten des INTERLISP-Systems eingelesen wird. Weitere M¨ oglichkeiten zur Benutzung von Standardfunktionen er¨offnet das INTERLISP-Dateisystem (Kap. 3.2.3.5).
134
3 Programmiersprachen
3.2.3.4 Konzeptionelle Erweiterungen Es darf nicht verschwiegen werden, daß fast alle LISP-Systeme Konzepte enthalten, die im Widerspruch zur reinen Lehre von der funktionalen und applikativen Programmierung stehen. Betrachten wir zun¨achst typische prozedurale Sprachkonzepte in INTERLISP. Der Rumpf einer LAMBDA-Funktion kann aus einer Folge von Ausdr¨ ucken bestehen λ[[x1 ; . . . ; xn ]; e1 ; . . . ; ek ]
,
die bei einem Funktionsaufruf sequentiell von links nach rechts evaluiert werden, und wobei der Wert von ek der Funktionswert ist. Die Bedeutung der Evaluation von e1 , . . . , ek−1 liegt in Seiteneffekten, von denen einige im folgenden angesprochen werden. Entsprechend sind die Klauseln in bedingten Ausdr¨ ucken definiert: . . . ; pi → ei1 ; . . . ; eij ; pi+1 → . . . wobei im Falle von pi = T der Wert von eij der Wert des bedingten Ausdrucks ist. Die Analogie zur sequentiellen Ausf¨ uhrung von compound statements“ ” ALGOLartiger Programmiersprachen liegt auf der Hand. Die Funktion prog gestattet es, Bl¨ocke“ (mit lokalen Variablen!) zu programmieren: ” prog[lokal; e1 ; . . . ; ek ] Das erste Argument von prog ist die Liste lokal = ((v1 a1 ) . . . (vn an )) der lokal ucken a1 , . . . , an f¨ ur e1 , . . . , ek vereinbarten Variablen v1 , . . . , vn mit den Ausdr¨ zur Initialisierung. Ein atomarer Ausdruck ej dient speziell als Sprungmarucke werden von links ke f¨ ur einen Sprungbefehl go[ej ]. Nichtatomare Ausdr¨ nach rechts evaluiert, bis ein Befehl return[x] ausgef¨ uhrt wird; die sequentielle Ausf¨ uhrung von prog wird unmittelbar abgebrochen und prog mit dem Wert von x als Funktionswert verlassen. Die Pseudofunktion setq[vi ; expr] = expr wirkt wie eine Wertzuweisung vi := expr an die lokale Variable vi . Der globale Wert eines literalen Atoms atm, die 2. Komponente, kann aber auch unabh¨ angig von irgendwelchen Bindungsverh¨altnissen durch rpaq[atm; expr] den Wert von expr zugewiesen bekommen. Diese unter Umst¨ anden gef¨ ahrliche Art von Wertzuweisung ist in ALGOL ausgeschlossen, ebenso wie das hier nicht angesprochene Programmieren von Property ” lists“, die in der 3. Komponente von literalen Atomen gespeichert werden. Kommen wir nun zu konzeptionellen Erweiterungen der S-Ausdr¨ ucke. Bei der Interpretation eines Pure-LISP-Programms treten in allen anfallenden SAusdr¨ ucken nur Atome auf, die in der Eingabe vorgekommen sind, die aus der Kodierung des Programms in Datensprache stammen oder die die Standarda-
3.2 LISP
135
tome N IL, T, F sind. In INTERLISP kann man nun bei der Interpretation auch zus¨ atzlich literale Atome erzeugen: n ∈ {0, . . . , 9}
gensym[c] = cnnnn
Das Resultat von gensym ist ein literales Atom, dessen Name sich aus dem Buchstaben c und einer fortlaufenden vierstelligen Zahl nnnn zusammensetzt. Die folgenden Funktionen m¨ ussen mit großer Vorsicht benutzt werden, da sie zur Berechnung ihres Wertes die Struktur bestehender S-Ausdr¨ ucke ver¨ andern, anstatt einen neuen S-Ausdruck durch cons zu generieren. Es k¨onnen Objekte entstehen, die gem¨ aß der strengen Definition aus Kapitel 3.2.2.1 keine S-Ausdr¨ ucke sind, da sie in der graphischen Notation z.B. Zyklen enthalten oder identische Teilstrukturen besitzen. A
B
A C
N IL
B Wir betrachten diese Objekte unter Vorbehalt jedoch auch als S-Ausdr¨ ucke. Es muß im Einzelfall vom Programmierer entschieden werden, ob ihre Verwendung zu undefinierten Zust¨ anden f¨ uhrt, wie z.B. bei der Ausgabe von zyklischen S-Ausdr¨ ucken. rplaca[x; y] = x rplacd[x; y] = x In dem nichtatomaren S-Ausdruck x wird der Verweis auf car[x] bzw. cdr[x] durch einen Verweis auf y ersetzt. Der Funktionswert ist der modifizierte SAusdruck x bzw. x . Die alte Struktur von x ist zerst¨ort. Beispiele 3.2-13 rplaca[(A.B); (C.D)] = ((C.D).B) rplacd[(A.B); (C.D)] = (A.(C.D)) −→ A
B }x =⇒ rplaca
−→ C
⎫ B ⎬
−→
D }y =⇒ rplacd
C
D
⎭
−→ A
⎫ ⎬
C
⎭
D
x
x
136
3 Programmiersprachen
Diese und andere strukturver¨ andernde Funktionen setzt man ein, wenn Funktionen zur Manipulation von S-Ausdr¨ ucken besonders schnell und mit wenig Speicherbedarf programmiert werden sollen. Betrachten wir nun abschließend eine Erweiterung des Konzepts einer LISP-Funktion. Mit plus und times haben wir Funktionen kennengelernt, die mit einer beliebigen, endlichen Anzahl von Argumenten aufgerufen werden k¨ onnen, und de ist ein Beispiel f¨ ur eine Funktion, deren Argumente nicht evaluiert werden. Diese Erweiterung von Pure-LISP dient einmal dazu, eine knappe, u ¨bersichtliche Notation zu erreichen, und weiterhin wird durch die Typ¨ uberpr¨ ufungen“ bei Funktionsaufrufen eine gr¨oßere Zuverl¨assigkeit der ” Programme erreicht. Jede INTERLISP-Funktion erf¨ ullt jeweils eine der folgenden, voneinander unabh¨ angigen Bedingungen: a) ihre Argumente werden evaluiert oder nicht (call by value bzw. call by name!), b) die Funktion besitzt eine feste oder eine beliebige Anzahl von Argumenten c) die Funktion ist definiert durch einen S-Ausdruck, durch das Ergebnis der Compilation eines S-Ausdrucks oder durch eine Folge von Maschinenbefehlen. Daraus ergeben sich die folgenden verschiedenen Typen von INTERLISPFunktionen:
Funktionstyp[5]
Arg. evaluiert
Anzahl d. Arg. fest
EXPR[1] FEXPR[2] EXPR∗[3] FEXPR∗[3] CEXPR CFEXPR CFXPR* CFEXPR* SUBR FSUBR SUBR* FSUBR*
ja nein ja nein ja nein ja nein ja nein ja nein
ja ja nein nein ja ja nein nein ja ja nein nein
Syntaktische Kennzeichnung
Beispiele
(LAM BDA(X1 . . . Xn ) . . . ) (N LAM BDA(X1 . . . Xn ) . . . ) (LAM BDA X . . . ) (N LAM BDA X . . . ) Compilierte Version von EXPR - FEXPR∗ Typ syntaktisch nicht erkennbar
B1 B2 B3 B4
Funktionen in Maschinencode, Typ manchmal am Funktionsnamen erkennbar
cons, cdr de, setq plus times cond[4] , prog
Bemerkungen [1] call by value“, Typ der Funktionen in Pure LISP mit Ausnahme ” von COND und QUOTE [2] call by name“ ” [3] Kommt in ALGOL-artigen Programmiersprachen nicht vor [4] Abweichung von Pure LISP [5] Der Typ einer Funktion fn l¨ aßt sich durch fntyp[fn] abfragen.
3.2 LISP
137
Beispiele 3.2-14 B1 : ((LAM BDA
(X) X) (QU OT E B)) → B
B2 : ((N LAM BDA (X) X) (QU OT E B)) → (QU OT E B) X X) ´B ´C ´D)
→ (B C D)
B4 : ((N LAM BDA X X) ´B ´C ´D)
→ (´B ´C ´D)
B3 : ((LAM BDA
In INTERLISP gibt es also ein Typkonzept; wenn auch nicht in dem mathematischen Sinne des getypten λ-Kalk¨ uls (Barendregt 1981) oder im Sinne streng getypter Programmiersprachen wie ALGOL 68 (Wijngaarden 1976), ML (Milner 1984) oder LISP/N (Lippe 1979). Es bleibt zu untersuchen, wie sich dieses pragmatische Typkonzept in die mathematische Theorie einordnen l¨aßt. 3.2.3.5 Die INTERLISP-Programmierumgebung Der Komfort beim interaktiven Programmieren mit INTERLISP beruht auf einer LISP-spezifischen Programmierumgebung, die alle g¨angigen Dienstleistungsprogramme als LISP-Funktionen bereith¨alt. Einige dieser Programme wickeln einen eigenen Dialog mit dem Benutzer ab. Damit entf¨allt die Notwendigkeit, st¨ andig zwischen INTERLISP und den verschiedenen Dienstleistungsprogrammen des Betriebssystems der Rechenanlage hin und her wechseln zu m¨ ussen. Die sprachspezifische Programmierumgebung schirmt gegen die komplexe, undurchsichtige Welt der Betriebssystemkommandos ab. Im Prinzip gen¨ ugt es, wenn man von dem Betriebssystem das LOGON-Kommando und das Kommando zum Starten von INTERLISP kennt. Im Rahmen der INTERLISP-Programmierumgebung arbeitet der Programmierer mit Dienstleistungsprogrammen, die auf die ganz spezielle Situation beim interaktiven Erstellen von LISP-Programmen abgestimmt sind. Im folgenden sollen diese Programme in Stichworten vorgestellt werden. Der INTERLISP-Editor In ihrer Arbeitsweise sind die meisten Editoren zeilenorientiert, oder sie arbeiten mit einem Sichtfenster. Diese Abh¨ angigkeit von einer physikalischen Struktur, in der Daten intern dargestellt werden, ist im INTERLISP-Editor vermieden worden, indem syntaxorientiert anhand der Struktur von S-Ausdr¨ ucken editiert wird. Mit Ausnahme von Basisfunktionen und compilierten Funktionen kann man alle Objekte, die in INTERLISP-Programmen auftreten, in einheitlicher Weise editieren. Der Editor ist selbst ein interaktives Programm, das u ¨ber einen Funktionsaufruf gestartet wird. Der Aufruf editf [atom] er¨ offnet die Funktionsdefinition mit dem Namen atom“ zum Editieren oder editv[atom] er¨offnet den globalen Wert von ” atom“zum Editieren. Danach erwartet der Editor als Eingabe Kommandos, ”
138
3 Programmiersprachen
u oschen, Einf¨ ugen, Ersetzen und Drucken von S-Ausdr¨ ucken in ¨ber die das L¨ dem zu editierenden S-Ausdruck gesteuert wird. Dabei spielt der sogenannte aktuelle S-Ausdruck“ eine besondere Rolle; die durch Kommandos verursach” ten Effekte, z.B. das L¨ oschen, beziehen sich auf den aktuellen S-Ausdruck. Das Kommando 2P bewirkt z.B. die Ausgabe des 2. Listenelements des aktuellen S-Ausdrucks und macht diesen Teilausdruck zum neuen aktuellen S-Ausdruck. Mit OP kann man auf den urspr¨ unglichen, aktuellen S-Ausdruck zur¨ ucksetzen. Da das Durchsuchen eines S-Ausdrucks mit diesen primitiven Hilfsmitteln oft zu langwierig ist, gibt es ausgefeilte Kommandos, die ein gezieltes Suchen nach Teilausdr¨ ucken erm¨ oglichen. Dabei reicht es, das Muster des gesuchten S-Ausdrucks anzugeben. Abschließend sei angemerkt, daß es in anderen LISP-Systemen Editoren wie z.B. den EMACS (Stallmann 1981) gibt, die mit ihren M¨oglichkeiten dem INTERLISP-Editor weit u ¨berlegen sind. Programme zur Dateiverwaltung Das interaktive Programmieren mit LISP-Systemen zeichnet sich dadurch aus, daß man den gesamten Zustand des Systems, wie er sich z.B. am Ende einer Sitzung am Terminal ergibt, auf einer Datei abspeichern kann und daß man zu einem sp¨ ateren Zeitpunkt mit diesem Zustand weiter arbeiten kann. Man kann sich ein individuell adaptiertes LISP-System f¨ ur einen bestimmten Aufgabenkreis schaffen, in dem vereinbarte Funktionen wie Standardfunktionen benutzt werden k¨ onnen und in dem gewisse Variablen mit einem globalen Wert vorbesetzt sind. Dadurch k¨ onnen LISP-Programme, die sich auf so einen speziellen Systemzustand beziehen, relativ kurz werden. Weiterhin bietet INTERLISP ein Programmpaket zur Dateiverwaltung an, mit dessen Hilfe man gr¨ oßere Mengen von Funktionsvereinbarungen und Vorbesetzungen von Variablen mit einem globalen Wert gezielt auf Dateien sichern kann. Im Programmpaket sind die internen Zust¨ande aller betroffenen ¨ Dateien bekannt, und alle Anderungen werden protokolliert. Aufgrund dieser ¨ Informationen wird der Benutzer u unterrichtet, u ¨ber Anderungen ¨ber bereits compilierte oder gedruckte Dateien informiert, und offensichtlich notwendige Aktionen werden automatisch ohne R¨ uckfrage beim Benutzer erledigt. Interaktive Fehlerbehandlung Die Programme des INTERLISP-Systems zur Fehlerbehandlung garantieren im Falle eines Fehlers bei der Evaluation eines Ausdrucks die Aufrechterhaltung des Dialogs mit dem Benutzer. Dar¨ uber hinaus befindet sich das System in einem speziellen Fehlerstatus (BREAK-Modus), der zus¨atzlich zur Fehlermeldung erm¨ oglicht, interaktiv den Fehler zu lokalisieren und zu beseitigen und dann mit dem Programm fortzufahren, ohne evtl. umfangreiche Vorarbeiten wiederholen zu m¨ ussen. Im Sinne der Programmierung von Ausnahmesituationen stehen Pseudofunktionen zur Verf¨ ugung, mit denen man in einem
3.2 LISP
139
LISP-Programm Fehler generieren und entsprechende Meldungen ausgeben kann (exception raising). Mit anderen Pseudofunktionen lassen sich Fehler abfangen, analysieren und korrigieren, wobei gegebenenfalls auch der normale Kontrollfluß im Programm unterbrochen werden kann (exception handling). Die Ausf¨ uhrung eines LISP-Programms l¨aßt sich jederzeit vom Bildschirm aus durch verschiedene Arten von Eingriffen (interrupts) unterbrechen bzw. abbrechen. Im Falle einer Unterbrechung befindet sich das System im BREAK-Modus und erlaubt Modifikationen der normalen Ausf¨ uhrung des LISP-Programms. Modifikation von Funktionen Mit dem Programmpaket ADVISE steht in INTERLISP eine weitere Un¨ terst¨ utzung bei der Programmentwicklung zur Verf¨ ugung. Ublicherweise ¨andert man eine Funktion, indem man mit dem Editor den Text der Funktionsvereinbarung entsprechend bearbeitet. Mit ADVISE kann man jedoch Funktionen modifizieren, ohne dabei den Rumpf der originalen Funktion zu ver¨andern. ¨ Die notwendigen Anderungen erfolgen in Form von ADVICES“ als Vor- bzw. ” Nachspann zu dem Rumpf der originalen Funktion, der wie eine black box“ ” angesehen wird. Das folgende Diagramm stellt die Struktur einer Funktion dar, die mehrmals durch ADVISE modifiziert wurde. lokale Variablen V1 , · · · , Vk
ADVICE BIND
advice1 ⎪ ⎭ ⎫ ⎪ ⎬
sequentiell zu berechnende Ausdr¨ ucke
ADVICE BEFORE advicen originaleF unktion
advice1 ⎪ ⎭ ⎫ ⎪ ⎬
sequentiell zu berechnende Ausdr¨ ucke
advicem
Modifizierte Funktion
ADVICE AFTER
140
3 Programmiersprachen
Diese Betrachtungsweise einer Funktion als black box“ ist vorteilhaft bei ” Funktionen, die in Maschinencode implementiert sind und f¨ ur die in der Regel ein Rumpf in editierbarer Form gar nicht vorliegt. Die Modifikation einer bereits compilierten Funktion mit ADVISE kann schneller sein als das Editieren mit anschließender erneuter Compilation. Man kann ADVISE auch als ein Hilfsmittel zur schichtenweisen“ Strukturierung von Funktionsr¨ umpfen anse” hen, wobei Implementationsdetails innerer Schichten nach außen abgeschirmt sind. Kehren wir zu dem oben angegebenen Diagramm zur¨ uck. Eine Funktion λ[[x1 ; . . . ; xn ]; body] wird durch ADVISE schrittweise modifiziert zu: λ[ [x1 ; . . . ; xn ] (∗ADV ICE BIN D) prog[ (/V ALU E, V1 , . . . , Vk ); setq[/V ALU E; prog[( ); advice1 ; (∗ADV ICE BEF ORE) .. .
advice1 .. .
advicen ; return[body]]]; (∗ADV ICE AF T ER)
advicem return[/V ALU E]]] Betrachten wir z.B. eine Modifikation der Array-Zugriffsfunktion elt[u; v], die das v-te Element des Arrays u als Wert hat. Durch arglist[elt] = (U V ) ist ¨ bekannt, daß die Variablen von elt u und v heißen. Eine Uberpr¨ ufung der Einhaltung der Indexgrenzen des Arrays u vor der Ausf¨ uhrung von elt kann erreicht werden durch: advise[elt; bef ore; [or [ilessp[v; l]; igreaterp[v; arraysize[u]] → error [ ARRAY − IN DEX F ALSCH“; v]; ” T → T ]]. Compilation von Funktionen ¨ Die Ubersetzung von LISP-Funktionen in Maschinenbefehle dient dazu, die Laufzeiteffizienz zu verbessern. Der Compiler von INTERLISP ist analog zu den anderen Programmpaketen in das Gesamtsystem integriert und kann jederzeit aufgerufen werden, ohne den Dialog beenden zu m¨ ussen. Der Compi¨ ler erm¨ oglicht sowohl das inkrementelle Ubersetzen von Funktionen als auch ¨ das Ubersetzen von Funktionspaketen und Dateien mit Funktionen. F¨ ur das Resultat eines LISP-Programms ist es in der Regel irrelevant, ob einzelne Funktionen compiliert sind oder nicht. In ganz speziellen Situationen kann es
3.2 LISP
141
jedoch zu Unterschieden kommen, die sich mit der Technik des inkrementellen Compilierens erkl¨ aren lassen. Funktionen, die vor ihrer Verwendung auf jeden Fall compiliert werden, d¨ urfen sogenannte ASSEMBLER-Ausdr¨ ucke enthalten. Das sind Folgen von Maschinenbefehlen in der syntaktischen Gestalt eines S-Ausdrucks, die vom Compiler erkannt werden und in die u ugt werden. ¨bersetzte Funktion eingef¨ Es ist typisch f¨ ur LISP-Systeme, daß man bei Bedarf sogar auf dem Niveau von Maschinenbefehlen programmieren kann. Aktive Hilfen zur Dialogabwicklung Wir wollen abschließend zwei ungew¨ ohnliche Programmpakete des INTERLISP-Systems ansprechen. Do What I Mean“ (DWIM) dient zur automa” tischen Korrektur von Syntaxfehlern, wie z.B. falsch geschriebenen Atomen oder bestimmten Arten von Klammerfehlern. Dazu werden verschiedene Informationsquellen benutzt: Der Kontext des Fehlers im Programm, fr¨ uhere Aktionen innerhalb des laufenden Dialogs mit INTERLISP und die Tastaturanordnung des benutzten Terminals. Wenn DWIM eine Korrekturm¨oglichkeit ermittelt hat, wird das Programm entsprechend verbessert, der Benutzer wird dar¨ uber informiert und die Verarbeitung wird so fortgesetzt, als sei kein Fehler aufgetreten. Konnte keine Korrektur durchgef¨ uhrt werden, erfolgt eine Unterbrechung (BREAK) der Programmausf¨ uhrung. Entspricht ein Korrekturvorschlag nicht den Vorstellungen des Benutzers, dann kann dieser gezielte Korrekturen vornehmen und sogar in einigen F¨allen nachtr¨aglich bereits erfolgte Korrekturen r¨ uckg¨ angig machen. Das zweite ungew¨ ohnliche Programmpaket ist der Programmer’s Assis” tent“ (PA). Es dient dazu, die Abwicklung eines Dialogs mit INTERLISP weiter zu vereinfachen und Routinearbeiten vom Benutzer fernzuhalten. Auf der Basis eines Protokolls aller erfolgten Eingaben des Benutzers ist PA in der Lage, zur¨ uckliegende Teile des Dialogs noch einmal zu zeigen, einzelne eingegebene Funktionsaufrufe bzw. Gruppen davon noch einmal zu wiederholen und bei einer Wiederholung kleinere Modifikationen einzuf¨ ugen. PA ist in der Lage, Seiteneffekte einiger spezieller Funktionen des INTERLISP-Systems r¨ uckg¨ angig zu machen. Diese Eigenschaft (UNDO) wird auch im Editor und in DWIM ausgenutzt. Die hier skizzierten M¨ oglichkeiten von DWIM und PA sind in anderen LISP-Systemen, wie z.B. im LISP-Machine-LISP perfektioniert worden und bilden dort zentrale Komponenten der LISP-Programmierumgebung. 3.2.4 Kuriosit¨ aten Zur Abrundung des Eindrucks von LISP sollen zwei Merkw¨ urdigkeiten vorgestellt werden, die zwar aus der Programmierung in Assemblersprachen bekannt sind, aber in h¨ oheren Programmiersprachen nicht vorkommen. Es handelt sich um Programme mit implizit definierten Funktionen und um Pro-
142
3 Programmiersprachen
gramme, die sich selbst modifizieren. Beide Merkw¨ urdigkeiten lassen sich beim Bau von LISP-Interpretierern nur relativ schwierig ausschließen (Simon 1978, Bauchrowitz 1980). Da bei LISP-Programmen Funktionen als Argumente zugelassen sind und andererseits die Eingabe f¨ ur den Interpretierer die Kodierung eines LISP-Programms in einem S-Ausdruck darstellt, ergibt sich folgende Komplikation: In einem Programm in Datensprache darf an jeder Stelle, an der vom Interpretierer eine Funktion erwartet wird, im Prinzip auch ein S-Ausdruck stehen, dessen Auswertung zu einem S-Ausdruck f¨ uhrt, der als Kodierung einer Funktion interpretierbar ist. Es folgt ein Programm mit einer solchen impliziten Definition der Funktion λ[[x]; eq[x; A]] . ((LAM BDA (F ) (F A)) (LIST ’LAM BDA (LIST X) (LIST ’EQ ’X (LIST ’QU OT E ’A)))) → ((LAM BDA (X) (EQ X ’A)) ’A) →T Die Standardfunktion list ist vom Typ EXP R∗, und es gilt: list[x1 ; . . . ; xn ] = (x1 x2 . . . xn ). Zur Erinnerung: ’A bedeutet (QU OT E A). Mit Hilfe von Funktionen, die die Struktur von S-Ausdr¨ ucken ver¨andern, wie z.B. rplaca und rplacd (Kap. 3.2.3.4), kann man in der Datensprache Programme schreiben, die sich selbst modifizieren. Die besondere Problematik solcher Programme ist in J¨ urgensen (J¨ urgensen 1974) untersucht worden; insbesondere sind sie nur unter ganz speziellen Bedingungen compilierbar. Als kurzes Beispiel f¨ ur eine sich selbst modifizierende Funktion betrachten wir ein etwas umst¨ andliches Programm f¨ ur λ[[x]; x]. Von praktischer Relevanz k¨onnen selbstmodifizierende Funktionen unter Umst¨ anden bei der Optimierung von Programmen sein. Sie sind ferner f¨ ur KI-Anwendungen relevant. ( (LABEL F (LAM BDA (X) (F (RP LACA (CDDR F ) (LIST ’QU OT E X))))) ’A) Zu Beginn der Auswertung des Rumpfes des LAMBDA liegt als A-Liste vor: ( (X.A) (F.(LAM BDA (X) (F (RP LACA (CDDR F ) (LIST QU OT E X))))) ) Betrachten wir die wesentlichen Schritte der Auswertung. Die Argumente von RPLACA sind (CDDDR F ) = ((F (RP LACA (CDDR F ) (LIST ’QU OT E X)))) und
3.2 LISP
143
(LIST ’QU OT E X) = (QU OT E A). Das Ergebnis von RP LACA ist ((QU OT E A)) mit einem Nebeneffekt auf der A-Liste: ((X.A) (F.(LAM BDA (X) (QU OT E A))) )
.
Nun ist folgender Aufruf von F zu betrachten: (F ((QU OT E A))), wobei ((QU OT E A)) ein S-Ausdruck ist. Da F sich zu der konstanten Funktion mit dem Funktionswert A modifiziert hat, ist A das Ergebnis der gesamten Auswertung. 3.2.5 Die Beziehung zum λ-Kalku ¨l John McCarthy hat in den Entwurf von LISP die wesentlichen Konzepte des λKalk¨ uls einfließen lassen. Es gibt jedoch im LISP-Interpretierer auch Abweichungen von der reinen Reduktionssemantik f¨ ur λ-Terme (kurz: λ-Semantik), die in diesem Abschnitt dargelegt werden sollen. Wir gehen von einem angewandten λ-Kalk¨ ul (Kap. 3.2.8) aus mit den S-Ausdr¨ ucken von LISP als Konstanten und den Basisfunktionen von LISP als δ-Regeln. Daraus wird schrittweise der in Kapitel 3.2.2.12 angegebene LISP-Interpretierer entwickelt. Anhand der Spezifikationen f¨ ur die Zwischenstufen des Interpretierers wird deutlich, wo in LISP die Unterschiede zur Reduktionssemantik f¨ ur λ-Terme liegen (Greussay 1977, Perrot 1979, Simon 1980). Man kann die Inkonsistenzen von LISP aber auch auf anderem Wege aufkl¨ aren, indem man LISP als einen getypten, angewandten λ-Kalk¨ ul definiert und dessen operationalisierte, denotationelle Semantik mit McCarthy’s Interpretierer vergleicht (Fehr 1982, Eick 1983). 3.2.5.1 LISP als angewandter λ-Kalku ¨l Durch die Definition von geeigneten Konstanten, von δ-Regeln und einer Einschr¨ ankung der Reduktionssemantik wird ein angewandter λ-Kalk¨ ul ΛL definiert, aus dem sich die λ-Semantik f¨ ur LISP ergibt. Die Menge C der Konstanten ist die Vereinigung von drei paarweise disjunkten Mengen C = E ∪ B ∪ {ω}, wobei E die Menge der S-Ausdr¨ ucke (Kap. 3.2.2.9) und B die Menge der Basisfunktionen von LISP ist (Kap. 3.2.2.2, 3.2.2.4). Die Konstante ω steht f¨ ur den undefinierten S-Ausdruck. Die Definitionen der Basisfunktionen werden auf die folgenden δ-Regeln u ¨bertragen:
144
3 Programmiersprachen
e1 car e → δ ω
falls e = (e1 .e2 ) sonst
e2 falls e = (e1 .e2 ) cdr e → δ ω sonst cons e1 e2 → (e1 .e2 ) δ
T atom e → δ F
falls e atomar sonst
⎧ ⎪ ⎨T eq e1 e2 → F δ ⎪ ⎩ ω
falls e1 , e2 atomer und e1 = e2 falls e1 , e2 atomer und e1 = e2 sonst .
Die λ-Semantik eines LISP-Programms Π ∈ ΛL mit den Eingaben e1 , . . . , en ∈ E ist eine partielle Funktion fΠ : En → E fΠ [e1 , . . . , en ] = e. Der Funktionswert e ist dabei die βδ-Normalform von Πe1 . . . en , falls diese existiert. Durch die Einschr¨ ankung der Argumente und des Resultats auf SAusdr¨ ucke wird McCarthy’s Absicht dargestellt, daß LISP ein Formalismus zur Manipulation von S-Ausdr¨ ucken ist. H¨ oher getypte λ-Terme der Art λxM k¨onnen deshalb in LISP h¨ ochstens als Zwischenergebnis bei der Reduktion eines Programms mit Eingabedaten auf die βδ-Normalform auftreten. 3.2.5.2 Der Interpretierer eval1 In Kapitel 2.2.7 sind mit den Definitionen der Funktionen evalV bzw. evalN Strategien f¨ ur die Reduktion von λ-Termen auf β-Normalform angegeben worden. Die λ-Semantik eines Programms mit Eingabedaten erh¨alt man durch eval gem¨ aß call by name“, w¨ ahrend die wesentlich effizientere Strategie evalV ” gem¨ aß call by value“ das nicht immer leistet. Dennoch hat man sich bei ” LISP f¨ ur call by value“ entschieden, zumal dann auch eine Kombination der ” Anwendung von β-Reduktionen und δ-Reduktionen zul¨assig ist. Aus heutiger Betrachtungsweise h¨ atte man f¨ ur LISP die verz¨ogerte Auswertung gem¨aß call by need“ vorsehen sollen. Der Interpretierer eval1 beruht auf evalV und ” enth¨ alt als wesentliche Abweichungen von der λ-Semantik: 1. Call by value, 2. Kombination von δ-Reduktionen und β-Reduktionen.
3.2 LISP
145
Zur Definition der Interpretierer benutzen wir eine ALGOL-artige Notation, angereichert mit etwas λ-Kalk¨ ul, und unterstellen daf¨ ur eine mathematische Semantik, die hier jedoch nicht n¨ aher dargelegt wird. Kommentare sind durch { und } gekennzeichnet bzw. durch Marken wie z.B. [1], wenn der Kommentar im fortlaufenden Text steht. Zur Abk¨ urzung wird die Menge A = C ∪ V der Atome eingef¨ uhrt, die aus den Konstanten und den Variablen von ΛL besteht. Zur Vereinfachung betrachten wir im Moment auch nur monadische Basisfunktionen aus der Menge B1 . Definition 3.2-7 eval1 : ΛL → ΛL eval1[E] := E ∈ A then E if else if E = λxM then λx.eval1[M ] else {E = (M N )} Arg := eval1[N ]; {δ-Regel anwenden} if (M ∈ B1 ) ∧ (Arg ∈ E) then M [Arg] [1] else if M = λxM ´ then eval1[SubxArg [M ’]] {β-Reduktion} [2] else eval1[(eval1[M ] Arg)] f j f i fj fi Bemerkungen [1] F¨ ur die Substitution in λ-Termen wird die Definition 2.2.4 unterstellt, damit keine parasit¨ aren Variablenbindungen entstehen. [2] Da der Term E selbst kein Redex ist, wird angenommen, daß die Reduktion von M zu einem λ-Term M ’ f¨ uhrt, so daß (M ’Arg) ein Redex ist. Diese Spezialisierung der entsprechenden Zeilen von evalV (Kap. 2.2.7) ist auf die spezifische Situation bei der Reduktion von Termen aus ΛL abgestimmt, wo gem¨ aß der λ-Semantik βδ-Normalformen stets S-Ausdr¨ ucke sein m¨ ussen. Die Evaluation von M hat nur dann Sinn, wenn dadurch ein δ- bzw. β-Redex entsteht; sonst w¨ are die βδ-Normalform kein S-Ausdruck. Beispiel 3.2-15 Sei M ∈ ΛL ein Term mit T ∈ C als Normalform. Dann terminiert die Re¨ duktion von E nicht in Ubereinstimmung mit der λ-Semantik. eval1[(T Arg)] = eval1[(T Arg)] = . . . Bei der Reduktion mit evalV ergibt sich eine Applikation, aber kein SAusdruck. evalV [(T Arg)] = (T Arg)
146
3 Programmiersprachen
3.2.5.3 Der Interpretierer eval2 Dieser Interpretierer hat gegen¨ uber eval1 folgende wesentliche Ver¨anderungen, die jedoch die λ-Semantik nicht verf¨ alschen: 1. Keine partielle Reduktion der R¨ umpfe von λ-Abstraktionen, 2. Implementierung der Substitution durch Environments“. ” Aufgrund der λ-Semantik, die S-Ausdr¨ ucke als βδ-Normalformen verlangt, muß im Verlauf der Reduktion eines Terms E ∈ ΛL jede λ-Abstraktion M generell durch die Applikation (M Arg) auf ein Argument Arg verschwinden. Es ist also unn¨ otig, den Rumpf von M vor dieser Applikation zu reduzieren. M kann sich aber auch als u ussiges“ Argument erweisen, z.B. in (λx.yM ). ¨berfl¨ ” Da die Implementierung der textuellen Substitution in eval1 sehr aufwendig ist, werden Environments“ eingef¨ uhrt, um die Bindungen zwischen ” Variablen und zugeh¨ origen Werten zu verwalten. Ein Environment ist eine totale Abbildung ξ : A → E ∪ {ubv}, durch die jede Variable und jede Konstante an einen Wert gebunden ist. Konstanten, also S-Ausdr¨ ucke, sind an sich selbst gebunden; der Wert von Variablen ist das Spezialzeichen ubv (unbound variable), sofern nicht durch den Interpretierer eine andere Bindung etabliert ist. Wenn ξ ein Environment ist, dann bedeutet ξ’= ξ ∪ [id, V ] eine Modifikation der Abbildung ξ f¨ ur das Argument id auf den Funktionswert V . Aufgrund der Bindung von Variablen u ¨ber Environments ist der Interpretierer eval2 nun eingeschr¨ ankt auf die Reduktion von geschlossenen λ-Termen als Programme. Die Menge der geschlossenen λ-Terme aus ΛL sei mit ΛC L bezeichnet. In Anlehnung an den LISP-Jargon bezeichnen wir ein Paar < M, ξ >, bestehend aus einem λ-Term M ∈ ΛL und einem Environment ξ, das Bindungen f¨ ur Variablen aus M enth¨ alt, als ein Funarg (f unctional argument). Die Menge der Funargs sei mit F (ΛL ) bezeichnet. Definition 3.2-8 eval2
:
ΛC L → E ∪ {ω}
eval2[E] := if eval2’[E, ξO ] ∈ E then eval2 [E, ξO ] else ω f i Zu eval2 siehe Definition 3.2-9. ξO ist das Standardenvironment ξO = ξO ∪ {[T, T ], [F, F ], [N IL, N IL]}
mit den Bindungen f¨ ur die Standardatome T, F und N IL von LISP. Sei Env die Menge der Environments, wobei der Begriff um Funargs als Werte von Variablen erweitert wird:
3.2 LISP
147
ξ : A → E ∪ F(ΛL ) ∪ {ubv}. Sei ΛeL die Menge der erweiterten λ-Terme, in denen Funargs als Teilterme auftreten k¨ onnen. Definition 3.2-9 eval2’: ΛeL × Env → E ∪ F(ΛL ) ∪ {ubv} eval2’[E, ξ] := E ∈ A then ξ[E] if [3] else if E = λxM then < λxM, ξ > else {E = (M N )} Arg := eval2’[N, ξ]; if (M ∈ B1 ) ∧ (Arg ∈ E) then M [Arg] [2] else if M = λxM ’ then ξ’ := ξ ∪ [x, Arg]; eval2’[M ’, ξ’] ˜ , ξ˜ > then ξ’ := ξ˜ ∪ [x, Arg]; [4] else if M = < λxM ˜ , ξ] eval2’[M [1] else eval2’[(eval2’[M, ξ]Arg), ξ] f j f i f i fi fi Bemerkungen [1] Diese Zeile stammt aus eval1 unter Einf¨ ugung des Environments ξ. Im Falle Arg ∈ F(ΛL ) ist jedoch die wiederholte Reduktion von Arg durch eval2’ undefiniert. Da das u ussige, wiederholte Reduzieren in dieser ¨berfl¨ Situation im Interpretierer eval3 sowieso vermieden wird, wollen wir es in eval2’ dabei bewenden lassen. [2] Diese Modifikation des Environments ξ paßt unmittelbar zu der Definition der naiven Substitution“ Sub , bei der keine systematischen Umbenen” nungen stattfinden (Bem. zu Def. 2.2.4). Das Entstehen von parasit¨aren Variablenbindungen wird jedoch durch das Zusammenspiel der Zeilen [3] und [4] vermieden. Damit wird insgesamt eine Implementation von Sub erreicht, ohne explizite Umbenennungen in λ-Termen vornehmen zu m¨ ussen. Den Beweis findet man in Bauchrowitz (Bauchrowitz 1980). [3] Die freien Variablen von λxM werden durch ξ gebunden, da λxM nicht unmittelbar auf ein Argument appliziert wird. [4] Das Environment ξ enth¨ alt in der Regel nicht die korrekten Bindungen ˜ . Deshalb wird auf das Environment aus f¨ ur die freien Variablen von λxM dem Funarg zur¨ uckgegriffen.
148
3 Programmiersprachen
3.2.5.4 Statische und dynamische Bindung von Variablen Da im λ-Kalk¨ ul die Bindung eines angewandten Vorkommens einer Variablen x an das zugeh¨ orige definierende Vorkommen λx statisch anhand der Syntax f¨ ur λ-Terme und der G¨ ultigkeitsbereiche f¨ ur Variablen definiert ist, spricht man von statischer Variablenbindung. Beim Reduzieren von λ-Termen werden Variablenbindungen nicht verf¨ alscht, da die β-Reduktion mit Sub als Substitution definiert ist Beispiel 3.2-16
λ x (xxx λ y λ z ( y z ) →
β
→
β
λ y λ z (y z) λ y λ z (y z) λ z(λyλ z(yz)z)
→
β
λ zλ z (z z )
Anhand dieses Beispiels soll gezeigt werden, wie die statische Variablenbindung in eval2’ realisiert ist. eval2’[(λx(xx)λyλz(yz)), ξO ] = eval2’[(xx), ξ1 ] mit ξ1 = ξO ∪ [x, < λyλz(yz), ξO >] = eval2’[(eval2’[x, ξ1 ] < λyλz(yz), ξO >), ξ1 ] = eval2’[(< λyλz(yz), ξO >< λyλz(yz), ξO >), ξ1 ] Unter der Annahme, daß die Evaluation eines Funargs nicht n¨otig ist. = eval2’[λz(yz), ξ2 ] mit ξ2 = ξO ∪ [y, < λyλz(yz), ξO >] = < λz(yz), ξ2 > Hier endet die Reduktion, da eval2’ die R¨ umpfe von λ-Abstraktionen nicht reduziert. Die statische Variablenbindung findet man schon in FORTRAN, bei allen PASCALartigen Programmiersprachen und bei den modernen funktionalen Programmiersprachen. Eine Ausnahme bilden die LISP-Dialekte mit dem Konzept der dynamischen“ Variablenbindung, obwohl sich auch hier die sta” tische Variablenbindung durchzusetzen beginnt. Bei der dynamischen Variablenbindung ¨ andert sich im Verlauf der Reduktion eines λ-Terms die Bindungsrelation zwischen angewandten und definierenden Vorkommen von Variablen gem¨ aß der Definition der naiven Substitution Sub.
3.2 LISP
149
Beispiel 3.2-16 (Fortsetzung) Erst nach dem letzten Reduktionsschritt ergibt sich eine andere Bindungsrelation: λ z(λyλ z(yz)z) →
λ zλ z(z z)
Im Interpretierer eval2’ ließe sich durch zwei einfache Ver¨anderungen die dynamische Variablenbindung erreichen: 1. Ersetzen von < λxM, ξ > durch λxM in Zeile [3] und 2. Streichen von Zeile [4] und die Definition von Environments w¨ are daran anzupassen. Folglich w¨ urden dann die freien Variablen einer λ-Abstraktion λxM erst in demjenigen Environment gebunden, in dem λxM auf ein Argument appliziert wird. Man kann das Auftreten von dynamischen Bindungen als einen eigentlich unbeabsichtigten Seiteneffekt oder als Programmierfehler bei der Einf¨ uhrung von Environments anstelle der textuellen Substitution betrachten. Dynamische Bindungen findet man bereits in McCarthy’s Interpretierer f¨ ur Pure LISP (McCarthy 1962, p.13) und in der Folge hat es unter dem Begriff Funarg” Problem“ eine lange Kontroverse um den Sinn von dynamischen Variablenbindungen gegeben. Bemerkenswert ist weiterhin, daß dynamische Bindungen bereits durch das Vorhandensein von Environments selbst entstehen k¨onnen und nicht erst durch deren Implementierung als Keller, als A-Liste oder durch shallow bin” ding“ (Kap. 4.1.2). Deshalb erfolgt die Behandlung im Zusammenhang mit eval2’, obwohl man f¨ ur diesen Interpretierer kaum u ¨bersichtliche Beispiele hat, bei denen sich der Unterschied zur statischen Variablenbindung konkret zeigen l¨ aßt. 3.2.5.5 Der Interpretierer eval3 Die wesentlichen Ver¨ anderungen gegen¨ uber eval2 sind: 1. Aufteilung in eval3 und apply3. 2. Benennung von λ-Termen (syntaktische Rekursionen). 3. Mehrstellige λ-Abstraktionen und Grundfunktionen. Die klassische Unterteilung von LISP-Interpretierern in eine Funktion eval zur Evaluation von Ausdr¨ ucken und eine Funktion apply zur Applikation von Funktionen auf evaluierte Argumente hat zur Folge, daß die Probleme mit Zeile [1] von eval2’ gel¨ ost sind.
150
3 Programmiersprachen
Durch die Benennung von λ-Termen wird das explizite Programmieren von Fixpunktkombinatoren u ussig und es erfolgt ein wesentlicher Schritt ¨berfl¨ hin zu einer praktikablen Programmiersprache. Ebenso ist es unter pragmatischem Gesichtspunkt unerl¨aßlich, mit mehrstelligen Funktionen zu programmieren. Definition 3.2-10 eval3 : ΛeL → E ∪ {ω} eval3[E] := if eval3’[E, ξO ] ∈ E then eval3’[E, ξO ] else ω f i eval3’: ΛeL × Env → E ∪ F(ΛL )∪ {ubv} eval3’[E, ξ] := if E ∈ A then ξ[E] else if E = λx1 . . . xn .M then < λx1 . . . xn .M, ξ > else {E = (M N1 . . . Nn )} Args := evlis3[(N1 . . . Nn ), ξ]; apply3[M, Args, ξ] fi fi elvis3[(N1 . . . Nn ), ξ] := (eval3[N1 , ξ] . . . eval3[Nn , ξ]) apply3[M, Args, ξ] := [1] if (M ∈ B) ∧ (|Args| = ρ(M )) ∧ (Argi ∈ E) then M [Args] else if (M = λx1 . . . xk .M ’) ∧ (K = |Args|) then ξ’:= ξ ∪ {[x1 , Args1 ], . . . , [xk , Argsn ]}; eval3’[M ’, ξ’] [2] else if M = (id = M ”) then ξ’:= ξ ∪ [id, < M ”, ξ >]; apply3[M ”, Args, ξ’] ˜ , ξ˜ >) ∧ (K = |Args|) then else if (M = < λx1 . . . xk .M ˜ ξ’:= ξ ∪ {[x1 , Args1 ], . . . , [xk , Argsn ]}; ˜ ˜ , ξ] eval3’[M [3] else apply3[eval3’[M, ξ], Args, ξ] fi fi fi fi Bemerkungen [1] ρ(M ) ist die Stelligkeit der Basisfunktion M . [2] id = M ” heißt, daß die λ-Abstraktion M ” durch den Identifikator id bezeichnet ist. Die u ¨bliche LISP-Notation ist label[id; M ”]. Damit kann M ”
3.2 LISP
151
rekursiv benutzt werden. Man beachte, daß das F unarg < M ”, ξ > an id gebunden wird, damit auch hier statische Variablenbindungen entstehen. [3] Die erneute Reduktion der Argumente Args wird durch die Verwendung von apply vermieden. 3.2.5.6 Der Interpretierer eval4 Die wesentlichen Ver¨ anderungen gegen¨ uber eval3 sind: 1. Kodierung von LISP-Programmen in S-Ausdriicken 2. Definition des Interpretierers durch ein LISP-Programm ¨ Der Ubersichtlichkeit halber benutzen wir dennoch die eingef¨ uhrte ALGOLartige Notation weiter. Die Kommentare enthalten Zeilennummern des Interpretierers aus Kapitel 3.2.2.12 und sollen dadurch die Querbez¨ uge ausdr¨ ucken. Definition 3.2-11 Ein Environment ist in dem Interpretierer eval4 eine Abbildung ξ := A → E ∪ {ubv} eval4 : E → E ∪ {ω} eval4[E] := [1] if eval4’[E, ξO ] ∈ E then eval4’[E, ξO ] else ω f i eval4’: E × Env → E ∪ {ubv} eval4’[E, ξO ]. := {Z12} [2] if E = () then () else if E ∈ A then ξ[E] {Z13,Z14,Z15 auch Z12} else if (CAR E) ∈ A then [3] if (CAR E) = QU OT E then (CADR E) {Z16} [4] else if (CAR E) = CON D then evcon[(CDR E), ξ] {Z17} else if (CAR E) = F U N CT ION then list[’F U N ARG, {Z18} (CADR E), ξ] [5] else apply4[(CAR E), evlis[(CDR E), ξ], ξ] {Z19} fi fi fi else apply4[(CAR E), evlis[(CDR E), ξ], ξ] f i f i f i {Z20} evcon[c, ξ] := . . . {siehe Kap. 3.2.2.12} evlis[Args, ξ] := . . . {siehe elvis3} list[x, y, z] := (x, y, z) {siehe Kap. 3.2.2.12}
152
3 Programmiersprachen
apply4[M, Args, ξ] := {Z1, M ist keine Funktion!} [6] if M = () then error else if M ∈ A then [7] if (M ∈ B) ∧ (Argsi ∈ B) then M [Args] {Z2-Z6} else apply4[ξ[M ], Args, ξ] f i {Z7} else if ((CAR M ) = LAM BDA) ∧ (k = |Args|) then {M = λx1 . . . xk .M ’} [8] ξ’:= ξ ∪ {[x1 , Args1 ], . . . , [xk , Argsk ]}; eval4[((CADDR M ), ξ’)] {Z8} else if (CAR M ) = LABEL then {M = (LABEL id M ”)} [9] ξ’:= ξ ∪ [id, < M ”, ξ >]; apply4[(CADDR M ), Args, ξ’] {Z9} else if (CAR M ) = F U N ARG then {M = (F U N ARG F ξ’)} [10] apply4[(CADR M ), Args, ξ’] {10} else apply4[eval4[M, ξ], Args, ξ] {11} fi fi fi fi fi Bemerkungen Es wird nun die Beziehung zu dem Interpretierer evalquote aus Kapitel 3.2.2.12 diskutiert. Environments seien als A-Listen mit den Hilfsfunktionen sassoc und pairlis implementiert. [1] In evalquote wird unterstellt, daß E die Applikation eines LISP-Programms f n in Datensprache auf Argumente x1 , . . . , xn ∈ E ist. Das Standardenvironment ξO reduziert sich auf die leere Liste, da N IL, T und F in eval explizit evaluiert werden. In Abweichung von der λ-Semantik f¨ ur LISPProgramme fehlt der Test f n[x1 , . . . , xn ] ∈ E. [2] Da E in eval4’ ein S-Ausdruck ist, muß die Fallunterscheidung sich in dem bedingten Ausdruck an dieser Struktur orientieren. [3] a) QU OT E wird als spezielle Basisfunktion mit call by name“ als Pa” rameter¨ ubergabe implementiert und darf nicht als eine δ-Reduktion angesehen werden. b) Durch (QU OT E F N ) kann man in der Datensprache von LISP die dynamische Bindung der freien Variablen in der Funktion F N erreichen! [4] a) Da eval4, eval4’ und eval selbst LISP-Programme sind, m¨ ussen auch bedingte Ausdr¨ ucke als S-Ausdr¨ ucke kodiert werden. Das geschieht durch McCarthy’s bedingte Ausdr¨ ucke (CON D (p1 e1 ) . . . (pn pe )), die aquivalent sind zu ¨ if p1 then e1 else if . . . else if pn then en else ω f i . . . f i f i.
3.2 LISP
153
b) Wie QU OT E ist CON D als spezielle Basisfunktion mit call by na” me“ implementiert. [5] Im Unterschied zu eval3’ wird call by value“ hier nat¨ urlich im applikati” ven Programmierstil implementiert. [6] Die Analyse von M orientiert sich an der Struktur als S-Ausdruck. [7] F¨ alschlicherweise werden an dieser Stelle in apply die unerl¨aßlichen Tests ur BasisArgi ∈ E unterlassen, bei denen sich zeigt, ob die Argumente f¨ funktionen tats¨ achlich S-Ausdr¨ ucke aus einem LISP-Programm sind oder kodierte Funargs oder sogar Teile eines kodierten LISP-Programms. Wei¨ terhin fehlt dort die Uberpr¨ ufung |Args| = ρ(M ) auf korrekte Parameteranzahlen. Aus diesen beiden Fehlern k¨ onnen sich massive Abweichungen von der λ-Semantik ergeben (Bauchrowitz 1980). [8] a) Gegen¨ uber eval3’ beschr¨ ankt sich die syntaktische Analyse der LAM BDA-, LABEL- und F U N ARG-Funktionen auf die Abfragen des entsprechenden Schl¨ usselwortes und ist damit unvollst¨andig. Es k¨onnen S-Ausdr¨ ucke als Funktionen interpretiert werden, die gar keine LISPProgramme in Datensprache sind. b) Die korrekte Anzahl der Argumente wird in apply durch pairlis gepr¨ uft. [9] In apply erfolgt hier die Bindung ξ’:= ξ ∪ [id, M ”]. Das hat eine massive Verf¨ alschung der λ-Semantik zur Folge! Im Prinzip schleicht sich dynamische Bindung f¨ ur die freien Variablen von M ” ein. Das Problem l¨aßt sich vermeiden, wenn man alle Variablen in einem LISP-Programm paarweise verschieden benennt. Zur Verdeutlichung folgt ein Beispiel (Simon 1978) : Beispiel 3.2-17 Programm: λ[[xg]; label[f ; λ[[xf ]; ↓
[atom[car[xf ]] → λ[[xg]; T [cons[xg; A]]] Eingabe: A
↓
f [cons[xg; A]]][cons[xf ; A]]; → xg]]]
Resultat bei Interpretation durch evalquote ist ((A.A).A) . Ersetzt man an den durch Pfeile gekennzeichneten Stellen xg durch xh, dann erh¨alt man auch mit evalquote die λ-Semantik des Programms, das Resultat A.
154
3 Programmiersprachen
Dieses Programm ist deshalb bemerkenswert, weil es in aller Deutlichkeit zeigt, daß die dynamische Variablenbindung auch ohne Funargs entstehen kann! ¨ [10] Durch diese Anderung gegen¨ uber apply3’ ist ber¨ ucksichtigt, daß in apply4’ auch LABEL-Funktionen als funktionale Argumente auftreten k¨onnen. Als ein wesentliches Ergebnis aus den obigen Betrachtungen u ¨ber LISPInterpretierer wollen wir f¨ ur das Kapitel 4 festhalten, daß die Art der Variablenbindung (dynamisch oder statisch) unabh¨angig ist von der Art der Implementierung von Environments. Die aufgezeigten Abweichungen von der λ-Semantik in evalquote schwanken zwischen prinzipiellen Unterschieden wie z.B. call by value“ und Pro” grammierfehlern, wie sie z.B. unter [7] aufgezeigt worden sind. In Simon (Simon 1978) und Bauchrowitz (Bauchrowitz 1980) ist deshalb ein Interpretierer entwickelt worden, der als einzige Abweichung von der λ-Semantik call by ” value“ als Reduktionsstrategie hat. Er beruht auf dem hier angegebenen Interpretierer eval4 und enth¨ alt dar¨ uber hinaus Erweiterungen, durch die die Probleme [1] - [10] vermieden werden. Außerdem m¨ ussen LISP-Programme nur noch eine leicht u ufbare Bedingung f¨ ur die standardm¨aßige Benut¨berpr¨ zung von Standardidentifikatoren wie z.B. QU OT E erf¨ ullen. Dennoch ist hier der Interpretierer evalquote diskutiert worden, da alle weitverbreiteten LISP-Systeme als Interpretierer ein an den erweiterten Sprachumfang angepaßtes evalquote bzw. eval besitzen. Wir wollen an dieser Stelle die Vorstellung der Programmiersprache LISP beenden. Viele Themenkreise konnten nur kurz gestreift werden bzw. sind hier gar nicht angesprochen worden, da es sich um eine Einf¨ uhrung handeln soll. Dem interessierten Leser empfehlen wir deshalb die Lekt¨ ure eines der einschl¨ agigen Lehrb¨ ucher u ¨ber LISP (Allen 1978, Stoyan 1984 b, Winston 1981).
3.3 Weitere Applikative Programmiersprachen 3.3.1 Einleitung Aus der Vielzahl von bemerkenswerten, neuen Entwicklungen sollen hier nun ausgew¨ ahlte Sprachen skizziert werden. Anhand von SASL und KRC l¨aßt sich zeigen, daß man durch syntaktische Ausgestaltung applikative Programmier¨ sprachen hinsichtlich der Ubersichtlichkeit von Programmen wesentlich verbessern kann gegen¨ uber Sprachen wie z.B. Pure-LISP, die stark an der Syntax von λ-Termen orientiert sind. Mit EFPL wird eine Fortentwicklung von FP vorgestellt, bei der es ebenfalls um mehr Transparenz in Programmen geht. BRL ist eine der wenigen Sprachen, die konsequent auf dem Prinzip der string ” reduction“ beruhen, d.h. ein BRL-Programm wird als Zeichenreihe gespeichert
3.3 Weitere Applikative Programmiersprachen
155
und die einzelnen Reduktionsschritte sind als Modifikationen dieser Zeichenreihe implementiert. Zur Ausf¨ uhrung von BRL-Programmen ist eine spezielle Rechnerarchitektur, die GMD-Reduktionsmaschine, entwickelt worden, die in Kapitel 4.3.2 vorgestellt wird. Daneben gibt es Sprachen mit unterschiedlichen Typkonzepten bzw. Kombinationen mit anderen Programmiersprachen. Ihre Beschreibung hat nicht den Zweck eines Handbuches, sondern an Hand charakteristischer Eigenschaften soll die Vielfalt der unterschiedlichsten Auspr¨ agungsformen der funktionalen und applikativen Sprachen gezeigt werden. Informationen u ¨ber weitere hier nicht behandelte Sprachen findet man z.B. in den einschl¨ agigen Tagungsb¨ anden bzw. im Internet. 3.3.2 SASL SASL ist die Abk¨ urzung f¨ ur St. Andrews Static Language“ und wurde ” von D.A. Turner zwischen 1972 und 1975 entwickelt (Turner 1976, 1980). Haupts¨ achliche Verwendung fand die Sprache bei der Ausbildung von Studenten. SASL kennt f¨ unf Arten von Objekten: Wahrheitswerte, Zahlen, alphanumerische Zeichen, Listen und Funktionen, die alle die folgenden Rechte“ be” sitzen: 1. 2. 3. 4. 5.
Jedes Jedes Jedes Jedes Jedes
Objekt Objekt Objekt Objekt Objekt
kann kann kann kann kann
benannt werden. Wert eines Ausdrucks sein. Komponente einer Liste sein. Argument einer Funktion sein. Ergebnis einer Funktion sein.
Der Typ eines Objekts kann mit Hilfe der Pr¨adikate logical, number, char, uft werden. list und function u ¨berpr¨ Die Darstellung der Wahrheitswerte erfolgt durch true bzw. f alse, die der Zahlen auf die u ¨bliche Weise, z.B. 0, 125, -13. Alphanumerische Zeichen werden durch das Sondersymbol % gekennzeichnet, z.B. %A, %+, %% . Eine ur ’new line’ und np f¨ ur ’new page’. Ausnahme bilden die Sonderzeichen nl f¨ F¨ ur Zahlen sind die folgenden Basisfunktionen in der u ¨blichen Weise erkl¨art: +, -, /, mod, *, <, <=, =, >=, > Beispiele 3.3-1 +x, x/y, x+y, x =y F¨ ur Wahrheitswerte hat man die logischen Funktionen , ε, , entsprechend ’Negation’, ’Und’ und ’Oder’. F¨ ur alphanumerische Zeichen hat man die Pr¨adikate letter und digit.
156
3 Programmiersprachen
F¨ ur Objekte vom Typ logical, number oder char existiert eine Funktion =, z.B. 7 = 3 + 4. Listen sind letztlich wie die S-Ausdr¨ ucke in LISP aufgebaut; man benutzt jedoch eine Notation, bei der viele Klammerpaare eingespart werden k¨onnen: 1. ( ) ist die leere Liste. 2. a, ist eine Liste der L¨ ange 1, deren einzige Komponente das Objekt a ist. 3. Ist a ein Objekt und x eine Liste der L¨ ange n, dann ist a,x eine Liste der L¨ ange n+1. 4. Bei geschachtelten Listen sind die Unterlisten in ( und ) einzuschließen. Zur Verarbeitung von Listen dienen die Basisfunktionen hd und tl; sie sind analog zu den Basisfunktionen head und tail bei dem Beispiel f¨ ur ein FPSystem definiert (Kap. 3.1.3). Daneben existiert noch die Abfrage auf Gleichheit, die gegeben ist durch ⎧ ⎪ ⎨true falls x = () und y = () x = y = true falls hd x = hd y und tl x = tl y ⎪ ⎩ f alse sonst. Zur Darstellung von Zeichenreihen kann entweder die Listennotation, z.B. %S, %A, %S, %L oder die abk¨ urzende Notation ’SASL“ benutzt werden. Durch die Verwendung von ’ zur Kennzeichnung des Anfangs bzw. von “ zur Kennzeichnung des Endes einer Zeichenreihe k¨onnen Zeichenreihen eindeutig geschachtelt werden. Beispiele 3.3-2 Die folgenden Ausdr¨ ucke besitzen alle den Wert true: hd a, = a tl a, = () tl tl ’SASL”=’SL” hd tl hd tl ((1, 2, ), (3, 4, 5, ), ) = 4 hd hd (’Katze”,’Hund”, ) = K Bedingte Ausdr¨ ucke werden ¨ ahnlich wie in LISP gebildet: Bedingung 1 → Obj1 ; Bedingung 2 → Obj2 ; .. . Bedingung n → Objn ; Objn+1 Beginnend mit Bedingung 1 werden die Bedingungen sukzessive ausgewertet. Wird hierbei zum ersten Mal eine Bedingung i gefunden, die den Wert true besitzt, so ist der Wert des bedingten Ausdrucks das Objekt Obji . Existiert keine Bedingung mit dem Wert true, so ist der Wert des bedingten Ausdrucks das Objekt Objn+1 .
3.3 Weitere Applikative Programmiersprachen
157
Durch Lambda-Abstraktion kann der Benutzer sich selbst Funktionen definieren. Diese Funktionen bilden stets ein Objekt in ein anderes Objekt ab. Da ein Objekt auch eine Funktion, sogar eine Liste von Funktionen sein kann, lassen sich beliebig hohe Funktionalit¨ aten programmieren. Die Applikation einer Funktion f auf ein Objekt x erfolgt durch f x. Beispiele 3.3-3 Es sollen Konventionen zur Definition und Applikation von Funktionen illustriert werden 1. lambda x.x + 1 Die Applikation auf das Objekt 4 ist lambda x.x + 1 4 und liefert als Ergebnis 5. 2. lambda x.hd x + hd tl x Die Applikation auf die Liste 2,3, ist lambda x.hd x + hd tl x (2, 3, ) und liefert als Ergebnis 5. Hierbei muß die Liste 2,3, in Klammern eingeschlossen werden, um klarzustellen, daß der aktuelle Wert von x die Liste 2,3, ist und nicht nur 2, . Aus diesem Grund besteht die Konvention, daß jedes Objekt in Klammern eingeschlossen werden darf, ohne daß sich seine Bedeutung ¨andert. 3. Beispielfunktion 2 ben¨ otigt als Argument eine Liste. Daher ist es erlaubt, als Namen f¨ ur die Variable auch eine Liste von Namen zu verwenden. Das obige Beispiel l¨ aßt sich damit auch schreiben: lambda (a, b, ).a + b Dadurch erreicht man eine automatische Zerlegung des Arguments in gew¨ unschte Komponenten, die dann unmittelbar u ¨ber die in der Variablenliste benutzten Namen zugreifbar sind. Eine explizite Selektion mit ussig. hd und tl wird somit u ¨berfl¨ 4. lambda ((a, b, ), (c, d, e, ), ).a, b, c, d, e, Diese Funktion bildet aus einer Liste mit einer zweielementigen und einer dreielementigen Unterliste eine f¨ unfelementige Liste. 5. lambda a. lambda b.a + b Dies ist eine Funktion, die als Ergebnis wieder eine Funktion abliefert. Die Applikation auf 2 liefert als Ergebnis lambda b.2 + b . Zur vollst¨andigen Auswertung werden zwei Applikationen ben¨otigt: lambda a. lambda b.a + b liefert als Ergebnis 5.
2 3
Es ist jedoch nicht notwendig, daß bei derartigen Funktionalen stets alle Argumente zur vollst¨ andigen Auswertung zur Verf¨ ugung stehen. So
158
3 Programmiersprachen
kann man z.B. die obige Funktion nur auf 2 applizieren und das Ergebnis ur eine andere Funktion verwenden. lambda b.2 + b als Argument f¨ Funktionen mit funktionalem Ergebnis lassen sich durch folgende Konvention vereinfacht darstellen: onnen Punkt und lambda weggelassen Folgt auf den Punkt ein lambda, so k¨ werden, z.B. lambda a b.a + b Jedes Objekt Obj1 kann durch einen Namen x benannt werden, unter dem es in einem Objekt Obj2 benutzt werden kann: let x = Obj1 in Obj2. Die G¨ ultigkeit der Benennung x f¨ angt bereits im let-Teil hinter = an und endet mit Obj2 . Der Wert eines let-Ausdrucks ist Obj2, wobei jedes Vorkommen von x textuell durch Obj1 ersetzt wird. Das ist nicht zu vergleichen mit der Wertzuweisung let x = . . . ; in BASIC. Beispiele 3.3-4 1. let a = x + 1 in a + y ∗ a bewirkt, daß a + y ∗ a dem Ausdruck x + 1 + y ∗ (x + 1) entspricht. 2. let a = 1 in (let a = 2 in a + a) + a ucke. Hierdurch erreicht man ist ein Beispiel f¨ ur geschachtelte let-Ausdr¨ ¨ eine Uberschattung der Definitionsbereiche wie man sie von Blockschachtelungen her kennt. Der Wert dieses Ausdrucks ist 2 + 2 + 1 = 5 . 3. Sollen in einem Ausdruck mehrere Objekte benannt werden, so werden die Benennungen im let-Teil durch and verbunden, z.B. let x = f1 and y = f2 in . . . 4. let Nachfolger = lambda x.x + 1 in . . . ist ein Beispiel f¨ ur die Benennung einer Funktion. Die Benennung von Funktionen l¨aßt sich auch abk¨ urzend vornehmen mit let Nachfolger x be x + 1 in . . . at n be n = 0 → 1; n × F akult¨ at(n − 1) 5. let Fakult¨ ist ein Beispiel, wie man durch Benennung eine rekursive Funktion definieren kann. Auch wechselweise Rekursionen der Art let f x = . . . g . . . and g x = . . . f . . . in . . . sind im Zusammenhang mit and erlaubt. 6. Der G¨ ultigkeitsbereich einer Benennung kann auf den in-Teil eingeschr¨ankt werden, indem anstelle von let das Symbol new gesetzt wird, z.B. bewirkt let x = 1 in new x = x + 1 in Obj ,
3.3 Weitere Applikative Programmiersprachen
159
daß es sich nicht um eine zirkul¨ are Definition handelt, sondern daß x in Obj das Objekt 1+1 bezeichnet. Wir schließen mit einer Angabe der Syntax von SASL. Expressions < program >::= < exp > < exp >::= < block > | < lambda − exp > | < conditional − exp > | < exp − 1 > < block >::= let < def s > in < exp > | new < def s > in < exp > < lambda − exp >::= lambda < f ormal > . < exp > < conditional − exp >::= < exp − 2 > → < exp > : < exp > < exp − 1 >::= < exp − 2 >, < exp − 1 > | < exp − 2 > , | < exp − 2 > < exp − 2 >::= < exp − 2 >< or − op >< exp − 3 > | < exp − 3 > < exp − 3 >::= < exp − 3 > & < exp − 4 > | < exp − 4 > < exp − 4 >::= < exp − 4 > | < exp − 5 >< rel − op >< exp − 5 > | < exp − 5 > < exp − 5 >::= < exp − 5 >< add − op >< exp − 6 > | < exp − 6 > < exp − 6 >::= < add − op >< exp − 6 > | < exp − 6 >< mult − op >< exp − 7 > | < exp − 7 > < exp − 7 >::= < ex − op >< exp − 7 > | < combination > < combination >::= < combination >< arg > | < arg > < arg >::= < name > | < constant > | (< exp >) Definitions < def s >::= < def > | < def > and < def s > < def >::= < namelist > = < exp > | < f unction − f orm > be < exp > < namelist >::= < f ormal > | < f ormal >, | < f ormal >, < namelist > < f ormal >::= < name > | (< namelist >) | () < f unction − f orm >::= < name >< f ormal > | < f unction − f orm >< f ormal > Various operators < or − op >::= | < rel − op >::= > | >= | = | = | <= | < < add − op >::= + | − < ex − op >::= hd | tl number | log ical | char | list | f unction | letter | digit | digitval Layout and basic symbols < constant >::= < numeral > | < logical − const > | < char − const > | < string > | () < numeral >::= < digit >< digit − sequence >
160
3 Programmiersprachen
< logical − const >::= true | f alse < char − const >::= % < any character > | nl | np < string >::= ’< any message not containing unmatched quotes >“ < name >::= < letter > | < name >< letter > | < name >< digit > < comment >::= | | < any message up to the end of the line > < ignorable >::= < space > | < newline > | < comment > < layout >::= < ignorable > | < layout >< ignorable > 3.3.3 KRC KRC ist die Abk¨ urzung f¨ ur “Kent Recursive Calculator“. Es handelt sich um die Nachfolgesprache f¨ ur SASL, die ebenfalls von D.A. Turner entwickelt wurde (Turner 1979, 1981 a, 1981 b). Gegen¨ uber SASL wurde der Program¨ mierkomfort wesentlich erh¨ oht, und es wurden Anderungen in der Notation vorgenommen. Einige Unterschiede werden im folgenden kurz vorgestellt: Zeichenreihen werden in “ eingeschlossen und Listen mit Hilfe der Klammern [ ] und Kommata gebildet, z.B. Wochentage = [ Montag“, Dienstag“, . . . , Sonntag“] . ” ” ” Auf die einzelnen Elemente kann durch Indizierung zugegriffen werden, z.B. liefert Wochentag 3 das Objekt Mittwoch“. ” Die Funktion # liefert die L¨ ange einer Liste, z.B. # Wochentage als Ergebnis 7. Die Funktion : h¨ angt linksseitig ein Element an eine Liste, z.B. liefert 0 : [1, 2, 3] die Liste [0, 1, 2, 3]. Die Funktion −− bildet die Differenz zweier Listen, z.B. [1, 2, 3, 4, 5]−−[1, 3, 5] liefert die Liste [2, 4]. Die Notation [1 .. 100] ist eine abk¨ urzende Schreibweise f¨ ur die Liste der ganzen Zahlen von 1 bis 100. Der Benutzer schreibt die von ihm definierten Funktionen in Form eines Gleichungssystems auf, z.B. F akult¨ at n = product[1 .. n]. Hierbei ist product eine Standardfunktion (s.Tabelle). Werden zur Definition einer Funktion Fallunterscheidungen ben¨ otigt, so werden die einzelnen F¨alle unter Verwendung des Gleichheitszeichens aufgelistet.
3.3 Weitere Applikative Programmiersprachen
161
Die Funktion zur Bestimmung des gr¨ oßten gemeinsamen Teilers definiert man z.B. durch: ggT a b = a ,a = b = ggT (a − b) b , a > b = ggT a (b − a) , b > a Die Ackermann-Funktion l¨ aßt sich folgendermaßen definieren: A a b =b+1 ,a = 0 = A (a − 1) 1 ,b = 0 = A (a − 1) (A a (b − 1)) , a = 0 & b = 0
.
Diese Notation der Abstraktion, ohne explizite Verwendung von lambda, entspricht weitgehend der in der Mathematik u ¨blichen Notation. Als Ausgabekommandos existieren ? und ! . Durch ? werden Objekte in einem Standardformat ausgegeben. Die Verwendung von ! bewirkt, daß Listen unformatiert, d.h. ohne Klammern und Kommata, ausgegeben werden. Spezielle Formatierungsm¨ oglichkeiten sind u.a. durch die Verwendung der Standardfunktion show gegeben (s. Tabelle). Beispiel f¨ ur ein Programm mit Ausgabeanweisung ist: twice f1 x = f1 (f1 x) sq x =x∗x f = twice sq f 2? Ausgegeben wird 16. Eine Besonderheit von KRC stellt die M¨ oglichkeit zur Bildung von Mengen dar. {f x | x ∈ S} bedeutet die Menge aller f x, wobei x ∈ S gilt und f eine Funktion ist. An Stelle von — verwendet KRC das Semikolon und an Stelle von ∈ den Pfeil. Mengen werden als Listen implementiert, die verm¨oge lazy ” evaluation“ auch eine unendliche L¨ ange besitzen k¨onnen. Die Mengenoperationen werden durch die entsprechenden Funktionen auf Listen programmiert. Beispiele 3.3-5 1. {x ∗ x; x ← [1 .. 100]}? Ausgegeben wird die Liste der Quadrate der Zahlen von 1 bis 100. 2. {[a, b, c]; a, b, c ← [1 .. 30]; a ∗ a + b ∗ b = c ∗ c}? Ausgegeben wird die Liste der Phytagoras-Zahlen ≤ 30. Es folgt eine Auflistung der Standardfunktionen in KRC:
162
3 Programmiersprachen
Tabelle Mit Name :- Kommentar ist ein Kommentar zu Name bezeichnet und mit . die Komposition von Funktionen. abs x = x, x >= 0 ng x = −x and [] = true and (a : x) = a & and x append [] = [] append (x : xx) = x ++ append xx bagdif f :-defines the action of the ”–” operator; bagdif f [ ] y = [] bagdif f x [] = x bagdif f (a : x) (b : y) = bagdif f x y, a = b = bagdif f (a : bagdif f x [b]) y char :- predicate, true an string of size one, false otherwise (defined in machine code); cjustif y n x = [cjustif y’ (n − size x) x] cjustif y’ n x = [spaces (n / 2), x, spaces ((n + 1) / 2)] code :- converts a character to its ascii code number (defined in machine code); concat :- takes a list of strings and concatenates them into one big string (defined in machine code); cons a x = a : x decode :- converts an integer to the character of that ascii number (defined in machine code); digit x = char x & ”0” <= x <= ”9” digitval x = code x − code ”0”, digit x drop 0 x = x drop n [] = [] drop n (a : x) = drop (n − 1) x even x = x % 2 = 0 explode :- explodes a string into a list of its constituent characters (defined in machine code); f alse = ”F ALSE” f ilter f [] = [] f ilter f (a : x) = a : f ilter f x, f a = f ilter f x f or a b f = map f [a .. b] f unction :- type testing predicate (defined in n/c code); hd (a : x) = a insert :- auxiliary function used by sort, inserts a number into a sorted list in the correct position;
3.3 Weitere Applikative Programmiersprachen
insert a [] = [a] inserta(b : x) = a : b : x, a <= b = b : insert a x intersection [x] = x intersection (x : xx) = f ilter (member x) (intersection xx) lay [] = [] lay (a : x) = show a : nl : lay x lay’ n[] = [] lay’ n(a : x) = rjustif y 4 n : ”)” : show a : nl : lay’ (n + 1) x lay n x = lay’ | x letter x = uppercase x : lowercase x list :- type testing predicate (defined in machine code); ljustif y n x = [x, spaces (n − size x)] lowercase x = char x ”a” <= x <=”z” map f [] = [] map f (a : x) = f a : map f x max [a] = a max[a, b] = a, a >= b =b max (a : x) = max [a, max x] member [] a = f alse member (a : x) b = a = b : member x b min = hd.sort mkset x = mkset’ x [] mkset’ [] y = [] mkset’ (a : x) y = mkset’ x y, member y a = a : mkset’ x(a : y) nl = decode 10 not x = \x np = decode 12 number :— type testing predicate (defined in machine code); odd x = \even x or [] = f alse or (a : x) = a | or x perms [] = [ [] ] perms x = {a : p; a < −x; p < −perms (x − −[a])} powerset [] = [ [] ] powerset (a : x) = powerset’ a (powerset x) powerset’ a p = p ++ map (cons a) p product [] = 1 product (a : x) = a ∗ product x quote = decode 34 read :- takes a file or device name and returns a list of characters (defined in machine code);
163
164
3 Programmiersprachen
reverse [] = [] reverse (a : x) = reversex ++ [a] rjustif y n x = spaces(n − size x), x show x =” < nl > ” , x = nl =” < np > ” , x = np =” < tab > ” , x = tab =” < vt > ” , x = vt = [quote, x, quote], string x = [”[”, show x,”]”], list x =x show’ [] = [] show’ [a] = [show a] show’ (a : x) = show a :”,”: show’ x size :- size x, f or any x, gives width of x an printing (with ”!” ) (defined in machine code); sort [] = [] sort (a : x) = insert a (sort x) sp = spaces 0 = [] spaces n = : spaces (n − 1) string :- type testing predicate (defined in machine code); sum [] = 0 sum (a : x) = a + sum x tab = decode 9 take 0 x = [] take n [] = [] take n(a : x) = a : take (n − 1) x tl (a : x) = x true =”T RU E” union = mkset.append update f x y z = y, z = x =f z uppercase x = char x & ”A” <= x <= ”Z” vt = decode 11 write :- used to mark items to be sent to a file an output, thus: write filename“ x where x is any KRC data item. ” (Defined in machine code); zip x = [], hd x = [] = map hd x : zip (map tl x) Wir beenden diesen Abschnitt u ¨ber KRC mit einem etwas umfangreicheren Programm, mit dem Turner die Eleganz der applikativen Programmierung“ ” demonstrieren wollte (Turner 1981 b).
3.3 Weitere Applikative Programmiersprachen
165
Beispiel 3.3-6 Es geht um eine Aufgabe aus der Chemie, n¨ amlich die Strukturformeln f¨ ur Paraffine und ihre Isomere anzugeben. Paraffine sind Kettenmolek¨ ule, die nur aus 4-wertigem Kohlenstoff und 1-wertigem Wasserstoff aufgebaut sind. Die chemische Summenformel f¨ ur ein Molek¨ ul der L¨ange n lautet Cn H2n+2 . Kennzeichnend ist, daß keine Doppelbindungen und keine Zyklen auftreten. Isomere sind Molek¨ ule mit der gleichen Summenformel, aber mit verschiedenen chemischen Strukturformeln. Die physikalischen und chemischen Eigenschaften von Isomeren sind im allgemeinen verschieden. Bei den Paraffinen treten Isomere ab n ≥ 4 auf: H H H H | | | | H—C—C—C—C—H | | | | H H H H Butan
H H | | H—C—C— | | H | H—C— | H
H | C—H | H H
Isobutan Da die 4 Bindungen eines Kohlenstoffatoms v¨ ollig gleichwertig sind, sind solche Paraffinmolek¨ ule, die durch Vertauschen der 4 Radikale eines Kohlenstoffatoms auseinander hervorgehen, keine Isomere! Wir wenden uns nun der Entwicklung eines Programms zu, das f¨ ur jeden Wert von n die Strukturformel des Paraffinmolek¨ uls und seiner Isomere ausgibt. Zun¨ achst wird mit Hilfe der Listen von KRC eine Datenstruktur zur Darstellung von Molek¨ ulen entwickelt. 1. Man w¨ ahlt ein beliebiges Kohlenstoffatom als Startatom“. ” 2. Zur Darstellung des Startatoms w¨ ahlt man eine 4-elementige Liste, wobei jedes Listenelement einem der 4 Radikale entspricht, die an das Startatom gebunden sind. 3. Jedes dieser Radikale wird entweder durch die Zeichenreihe H“ darge” stellt, falls es nur aus einem Wasserstoffatom besteht, oder durch eine 3-elementige Liste, falls es ein Kohlenstoffatom mit drei weiteren Radikalen ist. Beispiele:
Methan: [”H”, ”H”, ”H”, ”H”]
H | H—C—H | H
166
3 Programmiersprachen
H | Propan: H — C — | H
H H | | C1—C—H | | H H
[[[”H”, ”H” , ”H”] , [”H” , ”H” , ”H”] , ”H” ] , ”H”] Bei dieser Art der Darstellung hat man zwei Freiheitsgrade: 1. Die Wahl des Startatoms, 2. die Anordnung der Darstellung von Radikalen in der Liste. Darstellungen, die sich nur in diesen Freiheitsgraden unterscheiden, sind ¨aqui¨ valent. Die Aquivalenzrelation wird durch die folgenden drei Funktionen erzeugt: 1. invert [[a, b, c] d, e, f ] = [a, b, c [d, e, f ]] invert x = x, x 1 = H 2. rotate [a, b, c, d] = [b, c, d, a] 3. swap [a, b, c, d] = [b, a, c, d] Durch die gegebenenfalls wiederholte Anwendung von rotate und swap l¨aßt sich die Anordnung der Darstellung von Radikalen beliebig ver¨andern, und durch entsprechende Kombination mit Anwendungen von invert l¨aßt sich jede Darstellung eines Kohlenstoffatoms in die Startposition bringen. So erh¨alt man alle ¨ aquivalenten Darstellungen eines gegebenen Paraffinenmolek¨ uls. Beispiel: W¨ahlt man bei Isobutan das mittlere Kohlenstoff als Startatom, so ergibt sich die Darstellung: [[”H”, ”H”, ”H”] , ”H”, [”H”, ”H”, ”H”] , [”H”, ”H”, ”H”]] Die Anwendung von invert ergibt: [”H”, ”H”, ”H”, [”H” [”H”, ”H”, ”H”], [”H”, ”H”, ”H”]]], wobei das linke Kohlenstoffatom das Startatom ist. Mit rotate erh¨ alt man: [”H”, ”H” , [”H”, [”H”, ”H”, ”H”] , [”H” , ”H” , ”H”] ] , ”H”] Es folgt die Definition eines Pr¨ adikats
true falls a und b Darstellungen des gleichen Molek¨ uls equic a b = f alse sonst
3.3 Weitere Applikative Programmiersprachen
167
in KRC: equiv a b = member (equivclass a) b equivclass a = closure under laws [rotate, invert, swap] [a] closure under laws f s = s ++ closure’ f s s closure’ f s t = closure” f s (mkset{a|f ’← f ; a ← map f ’ t; \member s a}) ↑ N egation! closure” f s t = [], t = [] = t ++ closure’ f (s + t) t, t = []. Dabei sind member, map und mkset Standardfunktionen, und ++ ist die Konkatenation von Listen. Durch mkset werden nur mehrfach auftretende Listenelemente gel¨ oscht. Die Hauptfunktion ist closure under laws mit einer Liste von Funktionen und einer Liste von Objekten als Argumente. Resultat ist die H¨ ulle bez¨ uglich der angegebenen Funktionen. Jedes Paraffinmolek¨ ul muß mindestens ein Methan-Radikal ·CH3 enthalten. Die Funktion, die eine Liste erzeugt, in der alle Paraffinmolek¨ ule mit n Kohlenstoffatomen auftreten, wird in KRC definiert durch: paraf f in n = quotient equiv {[x,”H” ,”H” ,”H” ,”H”] | < x ← para (n − 1)} quotient f (a : x) = a : {b|b quotient f x; \ f a b} quotient f [] = []
.
Durch para (n−1) erh¨ alt man eine Liste mit allen Paraffinradikalen der L¨ange n − 1. ¨ Durch quotient erh¨ alt man aus einer Menge, deren Elemente in Aquivalenz¨ klassen eingeteilt sind, eine Menge, in der bez¨ uglich jeder Aquivalenzklasse nur ein Element auftritt. para 0 = [”H”] para n = { [a, b, c] | i, j, k ← [0 .. n − 1]; i ≤ j ≤ k; i + j + k = n − 1, a ← para i; b ← para j; c ← para k } Damit liefert der Aufruf output! mit output = layn(append(map paraf f in [1..]))
168
3 Programmiersprachen
die Liste aller Paraffinmolek¨ ule in aufsteigender Gr¨oße. Der Abbruch der Rechnung muß vom Benutzer durch einen Eingriff am Bildschirm erzwungen werden. Durch append wird eine Liste von Listen konkateniert und mit layn f¨ ur die Ausgabe formatiert. Die Ausgabe von unendlichen Listen ist als stream“ ” implementiert, d.h. es werden die Listenelemente kontinuierlich ausgegeben. Zum Schluß einige Bemerkungen zur Effizienz des Programms. Aus den folgenden Gr¨ unden ist das Programm recht langsam: 1. Die Auswahl von i, j und k bei para ist unwirtschaftlich. Man kann sie verbessern: i ← [0..(n − 1)/3] j ← [i..(n − 1 − i)/2] k ←n−1−i−j Beispiel: 0
i
1
2
j
0
1
2
3
1
2
2
k
6
5
4
3
4
3
2
Damit gilt nun para 0 =”H” paran = { [a, b, c] | i ← [0 .. (n − 1)/3]; j ← [i .. (n − 1 − i)/2]; a ← para i; b ← para j; c ← para (n − 1 − i − j) } 2. Eine Vielzahl von Berechnungen wird u ussigerweise mehrfach aus¨berfl¨ gef¨ uhrt, insbesondere para n. Es ist deshalb zweckm¨aßig, aus para eine memorized function“ zu machen, d.h. einmal berechnete Funktionswerte ” werden in einer Tabelle gespeichert und statt einer erneuten Berechnung aus dieser Tabelle ausgelesen. para 0 =”H” para n = paralist n paralist = map genpara [1..] genpara n = { [a, b, c] | i ← [0 .. (n − 1)/3], j ← [i .. (n − 1 − i)/2]; a ← para i; b ← para j; c ← para (n − 1 − i − j) }
3.3 Weitere Applikative Programmiersprachen
169
Die Tabelle wird durch die unendliche Liste paralist dargestellt. Durch genpara wird die Berechnung ausgef¨ uhrt; allerdings ist nun die Rekursion durch ein Inspizieren der Tabelle ersetzt worden. Die verz¨ogerte Auswertung (lazy evaluation) von unendlichen Listen sorgt daf¨ ur, daß bei nicht zugreifbaren Tabellenelementen die Evaluation der Tabelle bis zu dieser Stelle weiter vorangetrieben wird. 3.3.4 EFPL Die Sprache EFPL (Easy Functional Programming Language) wurde ca. 1980 von Chr. Gram und E.I. Organick entwickelt (Gram 1980a, 1980b). Sie ist im wesentlichen das spezielle von Backus eingef¨ uhrte, hier im Kapitel 3.1.3 vorgestellte FP-System, erweitert um Konzepte von Barton und Clark (Organick 1979). Es handelt sich um Funktionale f¨ ur die Iteration von Funktionen, f¨ ur eine verallgemeinerte Selektion und f¨ ur eine durch Indizes gesteuerte Reduktion, sowie um die M¨ oglichkeit, spezielle Selektorsequenzen zu benennen und als Parameter der betrachteten Funktion anzusehen. Es sind jedoch keine Variablen im Sinne von Pure-LISP. Ein ¨ ahnliches Konzept zur vereinfachten Programmierung in FP findet man in Backus (Backus 1981) unter der Bezeichnung extended definitions“. Als Besonderheit ist zu erw¨ahnen, daß in ” EFPL explizit mit hoch- bzw. tiefgestellten Indizes programmiert wird. So bedeutet f 3 die 3-fache Anwendung f ◦ f ◦ f einer Funktion f . Die Selektion einer ’Konstruktion’ g = [f l, f 2, f 3] wird durch g3 bezeichnet. Mit diesen N syntaktischen M¨ oglichkeiten kann man auch Ausdr¨ ucke wie Ai unmitteli=1
bar eingeben. Die Syntax von EFPL ist auf bildschirmorientiertes Programmieren abgestellt. Ein EFPL-Programm ist eine auf
reduzierbare Zeichenreihe. < algorithm > ::= < f unctional expression > | < f unction def > < f unction def > ::= < f ct.name > {(< parameter list >)} = < f unctional expression > { where < f unction def > , ... < f unction def > } < f ct.name > ::= < identif ier > < parameter list > ::= < param > {, < parameterlist >} < param > ::= < identif ier > In {. . . } eingeschlossene Teile sind optional. Ein < f unctional expression > ist aufgebaut aus einem festen Satz von Funktionalen und Basisfunktionen, sowie aus Funktionen, die im where-Teil als Unterfunktionen“ definiert sind. Es ” entsteht eine streng hierarchische Struktur der Funktionsdefinitionen (d-tree).
170
3 Programmiersprachen
Die im Parameterteil (< parameterlist >) einer Funktionsdefinition auftretenden Identifikatoren (< param >) bezeichnen eine Selektorfunktion, die bestimmte Komponenten aus dem Argument der zu definierenden Funktion bestimmt. Die Struktur des Arguments (eine Folge) wird im Parameterteil mit Hilfe der Identifikatoren und durch Klammerung festgelegt. Jeder dieser Identifikatoren steht dann f¨ ur diejenige Selektorfunktion, die die bezeichnete Komponente des Arguments selektiert. Beispiel 3.3-7 Durch die folgende Funktionsdefinition wird
√ a berechnet:
sqrt(a) ≡ EN D & IT ERAT ION & ST ART where ST ART ≡ [a, 1] , IT ERAT ION (a, x) ≡ while abs((a − x ∗ x)/a) > 10 ∗ ∗(−8) do [a, (x + a/x)/2] EN D (a, x) ≡x Funktionale Komposition ist durch & bezeichnet. Ferner ist die EFPL-Konvention ausgenutzt, nach der die Querstriche u ¨ber konstanten Funktionen wegfallen k¨ onnen. Die parameterfreie“ Version dieses Programms im reinen FP-Stil ” lautet: def sqrt ≡ EN D ◦ IT ERAT ION ◦ ST ART def ST ART ≡ [id, 1] def IT ERAT ION ≡ while abs((S1 − S2 ∗ S2)/S1) > 10 ∗ ∗ (−8) do [Sl, (S2 + S1/S2)/2] def EN D ≡ S2 In ST ART bezeichnet a also die identische Funktion id. In IT ERAT ION sowie EN D steht a f¨ ur den Selektor S1 und x f¨ ur den Selektor S2. Weitere, u utzen be¨ber FP hinausgehende Konzepte in EFPL unterst¨ sonders die Programmierung numerischer Aufgaben durch die Verwendung u ¨blicher mathematischer Notationen. Das in Kapitel 3.1.4 angegebene FP¨ Programm zur Matrizenmultiplikation besitzt diese Ahnlichkeit nicht. Es gibt zwei Funktionale f¨ ur die Iteration von Funktionen: 1. while P do F Die Bedeutung ist dieselbe wie die des while-Funktionals in FP. Es dient zur Iteration der Funktion F in Abh¨ angigkeit vom Pr¨adikat P . 2. F N Hierbei bezeichnet F die zu iterierende Funktion. Der Index N ist ebenfalls eine Funktion, deren Wert eine nat¨ urliche Zahl ist und die die Anzahl der Iterationsschritte bestimmt:
3.3 Weitere Applikative Programmiersprachen
FN
171
⎧ falls N : x = 0 ⎨x : x = F & . . . &F sonst ⎩ N :x mal
F¨ ur die Selektion sehr allgemein gehaltener Teilstrukturen aus Folgen, wie sie z.B. beim Rechnen mit Matrizen auftritt, gibt es Funktionale, die wesentlich flexibler sind als die Selektoren von FP. Sei A = [F 1, F 2, . . . , F n] eine Funktion, die mit Hilfe des Funktionals Konstruktion“ definiert ist und sei I eine Funktion. Wir betrachten eine ” Applikation A : x. Wenn I : x eine nat¨ urliche Zahl i ist, dann gilt: AI : x = F i : x Wenn I als Konstruktion [K1, K2, . . . , Kp] definiert ist, und I : x eine Folge < k1, k2, . . . , kp > von nat¨ urlichen Zahlen ist, dann gilt: AI : x = [F k1, . . . F kp] : x Die mehrfache Indizierung ist m¨ oglich. Sei z. B. A = [[F 1, F 2], [F 3, F 4]], dann selektiert A1 2 die Funktion F2. Beispiele 3.3-8 1. Sei A : x eine Folge < x1 , x2 , . . . , xn >, dann wird durch AI : x eine Komponente der Folge selektiert. Sei A : x eine zeilenweise als Folge aufgelistete Matrix << x11 , . . . x1n >, . . . , < xm1 , . . . , xmn >>, dann bezeichnet z.B. A1, 2 : x das Matrixelement x12 . Die letzte Zeile l¨aßt sich durch Am : x selektieren. Alternativ ist Alen&A : x mit der Basisfunktion len, die die L¨ ange einer Folge liefert. 2. Das folgende Programm durchsucht eine Folge a = < a1, . . . , an > von Zahlen linear nach dem ersten Vorkommen der gr¨oßten Zahl max a. Es ur hat als Ergebnis die Folge < max a, Index von max a >. I + 1 steht f¨ + & [I, 1] und N − 1 f¨ ur − & [N, 1]. M AX(A) ≡ RESU LT & LIN SEARCH & IN IT IALIZE where IN IT IALIZE ≡ [A, [A1 , 1], 2], LIN SEARCH (A, max, I) ≡ [A, if AI > max1 then [AI , I]; x, I + 1]N −1 where N ≡ len & A RESU LT (A, max, I) ≡ max Als weiteres Konzept gibt es in EFPL Funktionale f¨ ur die indizierte Reduktion. Dabei handelt es sich um eine Verallgemeinerung der in der Mathematik
172
3 Programmiersprachen
u ¨blichen Summationsschreibweise
N
Ai f¨ ur Ai + A2 + · · · + An , indem man
i=1
beliebige dyadische assoziative Operatoren zul¨aßt. Bei den meisten Anwendungen treten jedoch nur f¨ ur die Reduktion mit der Addition +, Π f¨ ur die Reduktion mit der Multiplikation ∗ und C f¨ ur die Reduktion mit der Konkatenation von Listen (Operatorzeichen concat) auf. Beispiele 3.3-9 ur x =⊥ . 1. C10 k=1 (2 ∗ k) : x = < 2, 4, . . . , 20 > f¨ 2. C1en j=5 Sj−1 : < x1, . . . , x7 > = < x4, x5, x6 > . 3. Matrixmultiplikation Sei A = < Zeile 1, Zeile 2, . . . , Zeile q > und sei B = < Zeile 1, Zeile 2, . . . , Zeile p >, wobei die Zeilenl¨ ange in A genau p ist. len&b M AT M U L(ab) ≡ Clen&a i=1 [Cj=1
len&b k=1
(ai,k ∗ bk,j )]
In der Arbeit von Gram (Gram 1980 a) findet man eine F¨ ulle weiterer Programme f¨ ur numerische Verfahren, wie z.B. die L¨osung linearer Gleichungssysteme nach der Jacobi-Methode oder die lineare Regression. Aus diesen Programmen sei abschließend die numerische Integration einer Funktion F nach der Trapezformel b
F (x) dx = (F (a) + 2 ∗
N −1
F (xi ) + F (b)) ∗ (b − a)/2/N
i=1
a
ausgew¨ ahlt. Man erh¨ alt aus der Formel unmittelbar das EFPL-Programm IN T EGRAL (a, b, N ) ≡ (F (a) + 2 ∗
N −1
F (a + i ∗ dx) + F (b)) ∗ dx/2
i=1
where dx ≡ (b − a)/N, F (x) ≡ .... Hierbei muß in der letzten Zeile des Programms die EFPL-Darstellung der zu interpretierenden Funktion F eingef¨ uhrt werden. 3.3.5 BRL 3.3.5.1 Sprachbeschreibung BRL ist die Abk¨ urzung f¨ ur Berkling Reduction Language“. Die Sprache wur” de bei der Gesellschaft f¨ ur Mathematik und Datenverabeitung (GMD) in St.
3.3 Weitere Applikative Programmiersprachen
173
Augustin unter der Leitung von K. Berkling entwickelt. Sie dient zur Programmierung der von der gleichen Gruppe entwickelten Reduktionsmaschine bzw. der Simulatoren f¨ ur die Reduktionsmaschine, die auf einer IBM/370158 implementiert worden sind. Das Konzept der Reduktionsmaschine wird im Kapitel 4.3.2 vorgestellt werden. Herausragendes Merkmal der Reduktionsmaschine ist die unmittelbare Ausf¨ uhrung von BRL-Programmen durch Hardware. Die ersten Ans¨ atze f¨ ur BRL und f¨ ur die Maschine stammen aus dem Jahr 1971 (Berkling 1971). Ein erster Simulator wurde von F. Hommes (Hommes 1975) geschrieben. Die Konstruktion der Reduktionsmaschine begann 1976 und wurde 1978 abgeschlossen. Parallel dazu wurden neue Simulatoren erstellt, der letzte im Jahre 1983. Im Laufe der Entwicklung hat die Sprache einige Ver¨anderungen erfahren, vor allem im syntaktischen Bereich. Hier wird diejenige Version in den wesentlichen Komponenten vorgestellt, die im Simulator von 1983 verwendet wird. Eine ausf¨ uhrliche Sprachbeschreibung findet man in Hommes (Hommes 1983) und Schl¨ utter (Schl¨ utter 1983). BRL-Programme sind in der f¨ ur applikative Sprachen typischen Weise aufgebaut. Elementare Daten sind die Wahrheitswerte, dargestellt durch true bzw. f alse, und Dezimalzahlen in der gebr¨auchlichen Darstellung, z.B. 3.14, 0, 1, 2. Die S-Ausdr¨ ucke bzw. Listen von LISP heißen in BRL Bin¨arb¨aume und an die Stelle von cons tritt der Operator ◦. Der Ausdruck ◦1 ◦ 2 3 repr¨ asentiert also den Baum:
1 2
3
Listen sind spezielle Bin¨ arb¨ aume der Art:
e1 e1
en
N IL
Wie bei LISP steht NIL f¨ ur die leere Liste. Listen k¨onnen auch in der Form < e1 , e2 , . . . , en > dargestellt werden. Weitere Daten sind Zeichenreihen, deren Anfang durch ’ und deren Ende durch ‘ markiert ist. Zeichenreihen d¨ urfen auch geschachtelt sein.
174
3 Programmiersprachen
Beispiele 3.3-10 1. ’K.J. BERKLING‘ 2. ’MUENSTER 2006‘ 3. ’DAS IST ’EINE‘ ”MEHRFACH‘ GESCHACHTELTE‘ ZEICHENREIHE‘ Logische Ausdr¨ ucke sind wie u ¨blich aus den Wahrheitswerten (true, f alse), Variablen und den logischen Operatoren and, or und not aufgebaut. Der Test auf Gleichheit von beliebigen konstanten Ausdr¨ ucken heißt equal, w¨ahrend eq konstante atomare Ausdr¨ ucke auf Gleichheit testet. Arithmetische Ausdr¨ ucke sind aus Zahlen, Variablen, den Operatoren +, −, ∗ und / und aus den Klammern ( , ) aufgebaut. Bedingte Ausdr¨ ucke haben die Form if Ausdruck0 then Ausdruck1 else Ausdruck2 . Alle Ausdr¨ ucke k¨ onnen ferner in Klammern eingeschlossen werden. Die Abstraktion eines Ausdrucks nach einer Variablen wird bezeichnet durch: sub V ariable in Ausdruck, ur substitute“ steht. wobei sub f¨ ” Beispiel 3.3-11 sub Radius in (3.14 ∗ (Radius ∗ Radius)) Die Applikation einer derartig definierten Funktion auf ein Argument erfolgt in der Form: apply F unktion to Argument. Beispiel 3.3-12 apply sub Radius in (3.14 ∗ (Radius ∗ Radius)) to 5 Generell ist bei apply als Argument ein beliebiger Ausdruck m¨oglich, der vor der Applikation der Funktion gem¨ aß ’call by value’ reduziert wird. In BRL m¨ ussen alle Applikationen mit Ausnahme der von Standardoperatoren durch apply angegeben werden!
3.3 Weitere Applikative Programmiersprachen
175
Beispiele 3.3-13 1. apply sub Radius in (3.14 ∗ (Radius ∗ Radius)) to (4 + 6) wird zun¨ achst reduziert zu apply sub Radius in (3.14 ∗ (Radius ∗ Radius)) to 10 danach zu (3.14 ∗ (10 ∗ 10)) und zu (3.14 ∗ 100) und schließlich zu 314 . 2. apply sub Radius in (3.14 ∗ (Radius ∗ Radius)) to (r + 6) wird reduziert zu (3.14 ∗ ((r + 6) ∗ (r + 6))) Dies ist das Endergebnis, da der Ausdruck (r + 6) nicht weiter reduziert werden kann. In LISP ist dieses Programm nicht zul¨assig, da sowohl f¨ ur die Eingabe als auch f¨ ur die Ausgabe nur S-Ausdr¨ ucke zul¨assig sind! 3. apply sub N ame in < N ame,’London‘> to ’James Bond‘ wird reduziert zu <’James Bond‘,’London‘> Rekursive Funktionen werden in BRL mit dem Konstrukt rec definiert, welches die gleiche Wirkung besitzt wie label in Pure-LISP. Beispiel 3.3-14 apply rec F ak : sub N in if (N eq 1) then 1 else (N ∗ apply F ak to (N − 1)) to 3 Im Hinblick auf die Reduktionsmaschine (Kap. 4.3.2) f¨ ur BRL soll die Reduktion von bedingten Ausdr¨ ucken und die Behandlung von Rekursionen etwas n¨aher betrachtet werden. Aufgrund des hinter apply stehenden Operators rec reduziert sich der obige Ausdruck zu: apply sub N in if (N eq 1) then 1 else (N ∗ apply rec F ak : sub N in if (N eq 1) then 1 else (N ∗ apply F ak to (N − 1)) to (N − 1)) to 3
176
3 Programmiersprachen
d.h. der im Rumpf“auftretende Name Fak wird durch den ganzen Ausdruck ” ersetzt, der mit Fak bezeichnet ist. Im n¨ achsten Reduktionsschritt kann nun die Abstraktion sub N . . . to (N − 1)) appliziert werden. if (3 eq 1) then 1 else (3∗ apply rec F ak : sub N in if (N eq 1) then 1 else (N ∗ apply F ak to (N − 1)) to (3 − 1)) Nun liegt ein bedingter Ausdruck vor, dessen if -Teil als n¨achstes reduziert wird. Da dessen Wert f alse ist, ergibt sich im folgenden Reduktionsschritt: (3∗ apply rec F ak : sub N in if (N eq 1) then 1 else (N ∗ apply F ak to (N − 1)) to 2) Der mit apply beginnende Ausdruck ist nun vom gleichen Typ wie der Ausdruck vor der ersten Reduktion, und es erfolgt solange eine analoge Folge von Reduktionsschritten, bis die Abfrage im bedingten Ausdruck den Wert true ergibt. Mit den Zwischenergebnissen (3 ∗ (2 apply rec F ak : . . . to 1)) und (3 ∗ (2 ∗ 1)) ergibt sich das Resultat 6 . Im folgenden werden weitere Sprachelemente vorgestellt zur Abrundung des Eindrucks von BRL. Von den Standardoperatoren seien noch erw¨ahnt: lt le gt ne
(less than) (less than or equal) (greater than) (not equal)
zum Vergleichen von arithmetischen Ausdr¨ ucken. Neben dem Konstruktionsoperator ◦ existieren die Selektionsoperatoren head und tail, entsprechend car und cdr in LISP, sowie das Pr¨ adikat null zum Erkennnen der leeren Liste. Beispiel 3.3-15 Bestimmung des letzten Elements einer nichtleeren Liste:
3.3 Weitere Applikative Programmiersprachen
x Last(1) = Last(y)
177
falls 1 = ◦ x N il falls 1 = ◦ xy und y = N il
Die entsprechende BRL-Funktion lautet rec Last sub 1 in if apply null to apply tail to 1 then apply head to 1 else apply Last to apply tail to 1 F¨ ur Zeichenreihen gibt es den Operator + zur Konkatenation und die Veruglich der lexikographischen Ordnung. gleichsoperatoren eq, ne, lt, le, gt, ge bez¨ Beispiel 3.3-16 Die BRL-Funktion (((’Das‘+’ist‘)+’‘) =’Das ist‘) reduziert zu true. Weitere Standardoperationen sind die Ein-/Ausgabe und das Abspeichern von benutzerdefinierten Funktionen. Zur Vereinfachung der Selektion von Kompoucke. Sie sind mit dem withnenten eines Bin¨ arbaums dienen die when-Ausdr¨ Sprachelement von PASCAL vergleichbar und ersparen die h¨aufige Wiederholung von langen Selektorsequenzen mit head und tail. Ein when-Ausdruck hat die Form when Baumstruktur do Ausdruck, wobei die Variablen in der Baumstruktur beim Aufruf gebunden und in den Ausdruck substituiert werden. Beispiel 3.3-17 rec Last : when ◦ x y do if apply null to y then x else apply Last to y Das Aufbrechen von Baumstrukturen wird weiterhin durch case-Ausdr¨ ucke unterst¨ utzt, die praktisch das cond von LISP beinhalten. case when Baumstruktur1 do Ausdruck1 case when Baumstruktur2 do Ausdruck2 endcase.
178
3 Programmiersprachen
Bei der Applikation eines case-Ausdrucks erfolgt die Reduktion derart, daß das Argument zun¨ achst mit Baumstruktur1 verglichen wird, und dort vorkommende Variablen an entsprechende Teilausdr¨ ucke des Arguments gebunden und in Ausdruck1 substituiert werden. Bei erfolgreichem Vergleich reduziert sich der ganze case-Ausdruck auf den Wert von Ausdruck1 . Bei der Nicht¨ ubereinstimmung wird entsprechend mit Baumstruktur2 verfahren. Soucke wesentlich f¨ ur die Bedeutung mit ist die Reihenfolge der when-Ausdr¨ eines case-Ausdrucks. Beispiel 3.3-18 rec Last : case when < x > do x case when ◦ x y do apply Last to y endcase Ein weiteres Hilfsmittel zur Analyse des Bin¨arbaums sind die gesch¨ utzten ucke. Dabei werden die einzelnen when-Ausdr¨ ucke durch einen case-Ausdr¨ W¨ achter“ gesch¨ utzt. ” case when Baumstruktur1 do guard Boolescher Ausdruck1 Ausdruck1 case when . . . .. . endcase Stimmt das Argument eines gesch¨ utzten case-Ausdrucks mit einer Baumstruktur u ¨berein, so wird nur dann auf den zugeh¨origen Ausdruck reduziert, wenn die Reduktion des guard-Ausdrucks true liefert. Beispiel 3.3-19 rec Last: case when ◦ x y do guard apply null to y x when ◦ x y do guard apply not to apply null to y apply Last to y endcase Man erkennt hier, daß man durch guard-Ausdr¨ ucke von der Notwendigkeit ucke zu u befreit ist, sich die genaue Reihenfolge der when-Ausdr¨ ¨berlegen, wie es im vorangehenden Beispiel erforderlich war. Wie bei SASL besteht ferner die M¨ oglichkeit, den Wert eines Ausdrucks mit Hilfe von let lokal mit einer Variablen zu benennen.
3.3 Weitere Applikative Programmiersprachen
179
a) let < V ar1 , . . . , V arn > := < Ausdruck1 , . . . , Ausdruckn > Ausdruck b) let V ar1 := Ausdruck1 Ausdruck Die Variablen < V ar1 , . . . , V arn > erhalten die Werte der Ausdr¨ ucke onnen in dem auf den let-Ausdruck < Ausdruck1 , . . . , Ausdruckn > und k¨ folgenden Ausdruck lokal benutzt werden. 3.3.5.2 Ein Programm zur Unifikation von Termen Wir beenden den Abschnitt u ¨ber BRL mit einem etwas umfangreicheren Programm. Als Beispiel dient die Unifikation von Termen. Sie tritt als Teilaufgabe h¨ aufig auf, z.B. beim automatischen Beweisen, insbesondere nach der Resolutionsmethode (Robinson 1965), bei der Interpretation von PROLOGProgrammen (Clocksin 1981, Cambell 1984) und im Rahmen der Theorie der Termersetzungssysteme (Huet 1977, Siekmann 1979, Wagner 1982). Das folgende BRL-Programm zur Unifikation ist eine vereinfachte Version der entsprechenden Teile aus dem PROLOG -Interpretierer von Fehr (Fehr 1984). Die theoretischen Grundlagen gehen auf Martelli und Montanari (Martelli und Montanari 1982) zur¨ uck. Im Vordergrund steht hier die Pr¨asentation von BRL. Deshalb wurde hier auf diejenigen Einzelheiten verzichtet, die nur aus dem Gesamtzusammenhang innerhalb des PROLOG-Interpretierers zu verstehen sind. Bei der Unifikation von Termen t1 ,t2 sucht man eine Belegung σ der Variaaß σ entstehenden Terme gleich blen in t1 und t2 mit Werten, so daß die gem¨ sind, d.h. σ(t1) = σ(t2 ) . Beispiele 3.3-20 Seien f, g Funktionszeichen und x, y Variablen. 1)
, t2 = f (g(y)) t1 = f (x) Eine Unifikation ist mit der Belegung < x, g(y) > m¨oglich.
2)
, t2 = g(f (y)) t1 = f (x) Wegen f = g ist eine Unifikation nicht m¨oglich. Definitionen 3.3-1 m
Sei V eine abz¨ ahlbare Menge von Variablen, und sei F = ∪ Fi mit m ∈ N0 , i=0
wobei die Fi Mengen von i-stelligen Funktionszeichen sind, so daß F abz¨ahlbar ist und die Mengen V, F0 , . . . , Fm paarweise disjunkt sind.
180
3 Programmiersprachen
1. Die Menge M (F, V ) der Terme u ¨ber F und V ist induktiv definiert durch: i) x ∈ M (F, V ) fu ¨r x ∈ V ii) a ∈ M (F, V ) fu ¨ r a ∈ F0 iii) f (t1 , . . . , tn ) ∈ M (F, V ) f u ¨r ∈ Fn mit n > 0 und t1 , . . . , tn ∈ M (F, V ). Bemerkung : Konstanten a ∈ F0 werden als 0-stellige Funktionszeichen aufgefaßt. 2. Eine Substitution σ ist eine totale Funktion σ : V → M (F, V ), bei der f¨ ur fast alle x ∈ V gilt σ(x) = x, d.h. f¨ ur endlich viele x ∈ V ist σ(x) = x. Der Begriff der Substitution l¨ aßt sich auf Terme t E M(F,V) ausdehnen:
σ(x) f¨ ur x ∈ V σ(t) = ur t = f (t1 , . . . , tn ) mit n ≥ 0, f ∈ Fn . f (σ(t1 ), . . . , σ(tn )) f¨ Bemerkung: Eine Substitution σ ist eindeutig charakterisiert durch die Menge von Paaren {< x1 , σ(x1 ) >, . . . , < xk , σ(xk ) >}, f¨ ur die gilt σ(xi ) = xi . 3. Seien t1 , t2 ∈ M (F, V ) Terme. Eine Substitution σ heißt Unifikator von (t1 , t2 ) genau dann, wenn σ(t1 ) = σ(t2). Eine Substitution σ heißt allgemeinster Unifikator von (t1 , t2 ) genau dann, wenn σ ein Unifikator von ur alle Unifikatoren δ von (t1 , t2 ) eine Substitution λ exis(t1 , t2 ) ist und f¨ tiert, so daß δ = λ ◦ σ. Der allgemeinste Unifikator ist nicht eindeutig bestimmt. Sei F = {f, g} und V = {x, y, z}. Dann ist f¨ ur t1 = f (x) und t2 = f (g(y)) sowohl σ = {< x, g(y) >} ein allgemeinster Unifikator als auch δ = {< x, g(z) >, < y, z >, < z, y >}. Die beiden allgemeinsten Unifikatoren unterscheiden sich allerdings nur durch eine Permutation Π der Variablen: Π(x) = x, Π(y) = z, Π(z) = y. Generell gilt, daß der allgemeinste Unifikator zweier Terme bis auf eine solche Permutation eindeutig bestimmt ist. Betrachten wir nun einige Besonderheiten bei der Konstruktion von allgemeinsten Unifikatoren. Beispiele 3.3-21 1. F = {f, g, a} V = {w, x, y, z} t1 = f (g(x, y)), y, g(a, a)) t2 = f ( z, w, z )
3.3 Weitere Applikative Programmiersprachen
181
Ein simples Unifikationsprogramm im Stile eines Mustererkenners k¨onnte mit (t1 , t2 ) Schwierigkeiten haben, wenn < z, g(x, y) > und < z, g(a, a) > in den Unifikator“ aufgenommen werden. Der allgemeinste Unifikator ist ” σ = {< z, g(a, a) >, < w, a >, < x, a >, < y, a >}. 2. Ein Problem tritt bei Termpaaren mit Zyklen auf. Wenn man Substitutionen als Termersetzungssysteme betrachtet, dann hat < x, τ (x) > mit τ (x) ∈ M (F, V ) einen Zyklus beim Einsetzen in einen Term τ ‘(x) ∈ M (F, V ) zur Folge. F = {f, g, a} t1 = f (g(x), a)
V = {x} t2 = f (x, a)
Ein allgemeinster Unifikator von (t1 , t2 ) existiert nicht. F¨ ur die schw¨achste Voraussetzung σ = {< x, g(x) >} gilt σ(t1 ) = σ(t2 ). 3. Zyklen k¨ onnen auch etwas komplizierter sein. F = {f, g} t1 = f (x, g(x))
V = {x, y} t2 = f (y, y)
Auch zu (t1 , t2 ) existiert kein allgemeinster Unifikator. F¨ ur die schw¨achste Voraussetzung σ = {< x, y >, < y, g(y) >} gilt σ(t1 ) = σ(t2 ). Wenden wir uns nun der Implementierung der Unifikation in BRL zu. F¨ ur die Darstellung von Termen und Substitutionen bieten sich in BRL als Datenstrukturen bin¨ are B¨ aume wie z.B. ◦F ◦ X N IL an und Listen wie z.B. < F, X >. Diese Datenstrukturen entsprechen den dotted pairs bzw. den Listen in LISP (Kap. 3.2.2.1). Darstellung von Termen: 1. Eine Variable x ∈ V wird durch die BRL-Variable : x dargestellt. Das Zeichen : dient zur Unterscheidung von gew¨ohnlichen BRL-Variablen. 2. Ein Funktionszeichen bzw. eine Kostante f ∈ F wird durch die Zeichenkette f dargestellt. 3. Ein Term f (t1 , . . . , tn ) ∈ M (F, V ) wird als bin¨arer Baum ◦f˜ ◦ t˜1 ◦ · · · ◦ t˜n N IL dargestellt, wobei f˜, t˜1 , . . . , t˜n die Darstellungen von f, t1 , . . . , tn sind. Beispiel 3.3-22 Die Darstellung von f (c, x, y) ist ◦ f ◦ c ◦ : x◦ : y N IL
.
182
3 Programmiersprachen
Darstellung von Substitutionen: Eine Substitution σ = {< x1 , t1 >, . . . , < xk , tk >} mit xi ∈ V und ur 1 ≤ i ≤ k wird dargestellt als Liste von Tupeln: ti ∈ M (F, V ) f¨ ◦ < : x1, t˜1 > ◦ < : x2, t˜2 > .. . ◦ < : xk, t˜k > N IL, wobei t˜i die Darstellung von ti , ist. Bemerkungen: Durch die Mischung von bin¨ aren B¨ aumen und Listen erh¨oht sich die Lesbarkeit der Programme. Man benutzt auch den Begriff Umgebung f¨ ur die Darstellung einer Substitution. F¨ ur das Berechnen von Umgebungen erweist sich die folgende Konvention als zweckm¨ aßig: Wenn in einem der Terme t˜1 , . . . , t˜k eine ur der entsprechende Wert aus der Variablen : xi , . . . , : xk auftritt, so ist daf¨ der Umgebung einzusetzen. Am Beispiel der Funktion UNIFY muß noch eine Besonderheit von BRL beschrieben werden, die zur Speicherung von Programmen auf Dateien dient. Die Datei mit dem Namen UNIFY enth¨ alt den Programmtext ab case ” when < N IL, N IL, L >“ bis endcase“. Der Vorspann mit dem Funktions” namen ist leider in BRL kein Bestandteil des abgespeicherten Programms. Er ist nur hier zu Dokumentationszwecken eingef¨ ugt worden. Die Bezeichnung &UNIFY& bedeutet, daß reduziert wird, bis UNIFY appliziert werden muß. Dann erst wird der Programmtext aus der Datei geladen. Das Abspeichern von Funktionen auf Dateien hat zwei Vorteile: Das Programm ist u ¨bersichtlicher als in der geschachtelten“ Fassung mit Benut” zung des rec-Operators, und die Programme, die bei string-reduction“ an” fallen, sind k¨ urzer, da wesentlich weniger umkopiert wird, z.B. nur der Name &UNIFY& statt des Programmtextes von UNIFY. *******************
UNIFY
*******************
*** 1. Element der Argumentliste: Term t1 *** 2. Element der Argumentliste: Term t2 *** 3. Argument: Eine bislang erstellte Substitution *** Resultat: Der allgemeinste Unifikator von (t1 , t2 ) *** oder ’fail’, wenn dieser nicht existiert
*** *** *** *** ***
3.3 Weitere Applikative Programmiersprachen
183
case when < N IL, N IL, L > do L case when < ◦A1 R1, ◦A2 R2, L > let E do := apply &U N IF Y & to < A1, A2, L > if (E equal f ail ) then f ail else if (E equal L) then apply &U N IF Y & to < R1, R2, L > else apply &U N IF Y & to < apply &W ERT & to < R1, E, N IL >, apply &W ERT & to < R2, E, N IL >, E> case when < typ A1 : T Y P E, A2, L > do if (A1 eq A2) then L else if apply OCCU RS to < A1, A2 > then f ail else ◦ < A1, A2 > L case when < A1, typ A2 : T Y P E, L > do if apply &OCCU RS& to < A2, A1 > then f ail else ◦ < A2, A1 > L case when < A1, A2, L > do if < A1 equal A2 > then L else f ail endcase Bemerkung: Die Funktion typ e ergibt den Typ des Ausdrucks e. Sie wird hier sinngem¨aß im Mustererkenner von BRL benutzt. So bedeutet typ Al : TYPE, daß eine Variable f¨ ur Al erkannt wird. *******************
OCCURS
******************
*** 1. Element der Argumentliste: Variable V *** 2. Element der Argumentliste: Term EX *** Resultat: true, falls V in EX vorkommt *** f alse sonst
*** *** *** ***
184
3 Programmiersprachen
case when < typ V : T Y P E, N IL > f alse do case when < typ V : T Y P E, typ EX : T Y P E > (V eq EX) do case when < typ V : T Y P E, ◦ EXA EXR > (apply &OCCU RS& do to < V, EXA > or apply &OCCU RS& to < V, EXR >) case when < typ V : T Y P E, EX > f alse do endcase Um den Wert eines Termes bez¨ uglich einer Umgebung zu berechnen, muß jedes Vorkommen einer Variablen durch den Wert des dazugeh¨origen Ausdrucks ersetzt werden, d.h. der Wert eines Termes bez¨ uglich einer Umgebung L ist induktiv definiert durch: 1. Eine Konstante oder eine Variable, die nicht in L gebunden ist, bleibt bestehen. 2. Eine Variable, die in L an den Term T gebunden ist, wird durch den Wert von T bez¨ uglich L ersetzt. 3. Der Wert eines aus Teiltermen zusammengesetzten Terms setzt sich aus den Werten seiner Komponenten zusammen. Beispiel 3.3-23 In der Umgebung ◦ < : x, a‘ > ◦ < : y, b‘ > ◦ < : z, x‘ > N IL ist der Wert der Variablen : z gleich dem Wert der Variablen : x in derselben Umgebung und dieser ist a . Bei der Suche nach einer passenden Bindung muß die Umgebung L von oben nach unten abgearbeitet werden. Da die Wertfunktion jedoch beim Auffinden einer solchen Bindung nach 2. wieder rekursiv auf T und die ganze Liste angewendet wird, muß diese Umgebung in einem eigenen Parameter G mitgef¨ uhrt werden. Eine Effizienz¨ uberlegung ergibt jedoch, daß es gen¨ ugt, in G diejenigen Bindungspaare zu sammeln, die beim Abarbeiten aus L entfernt werden, da diejenigen Variablen, die in T auftreten, nur oberhalb von der Bindung < : x, T > vorkommen k¨ onnen. Damit diese Eigenschaft im Verlauf der gesamten rekursiven Wertberechnung erhalten bleibt, wird bei der Wertberechnung von T die Umgebung, die durch Invertierung aus G entsteht, herangezogen. Die BRL-Funktion &REV& leistet die Invertierung von Listen.
3.3 Weitere Applikative Programmiersprachen
*******************
WERT
185
*******************
*** 1. Element der Argumentliste: Term *** 2. Element der Argumentliste: Umgebung *** 3. Element der Argumentliste: Hilfsumgebung zum *** L¨ osen von geschachtelten Variablenbindungen
*** *** *** ***
case when < X, N IL, G > X do case when < ◦ A R, L, G > ◦ apply &W ERT & to < A, L, G > do apply &W ERT & to < R, L, G > case when < typ X : T Y P E, ◦ < Y, T >, L, G > if (X equal Y ) do then apply &W ERT & to < T, apply &REV & to < G, N IL > N IL > else apply &W ERT & to < X, L, ◦ < Y, T > G > case when < X, L, G > X do endcase *******************
REV
*******************
*** 1. Element der Argumentliste: Bin¨ arer Baum mit *** *** Listenstruktur *** *** 2. Element der Argumentliste: Hilfsspeicher *** *** Resultat: Invertiertes erstes Element der Argumentliste ***
case when < N IL, L > L do case when < ◦ A R, L > apply &REV & do to < R, ◦A L > endcase Abschließend soll die Arbeitsweise von UNIFY anhand einiger Beispiele erl¨ autert werden. Wir beschr¨ anken uns dabei auf die Angabe von interessanten Termen, die miteinander verglichen werden und auf Umgebungen. Zur weiteren Vereinfachung wollen wir hier zwischen Termen und ihren Darstellungen nicht unterscheiden.
186
3 Programmiersprachen
Beispiel 3.3-24 1.
t1 = f (g(x, y)), y, g(a, a)) t2 = f (z, w, z) uhrt zu einer Der Funktionsaufruf apply &U N IF Y & to < t1 , t2 , N IL > f¨ Umgebung ◦ < z, g(x, y) > N IL, in der die Reste“ von t1 und t2 mit ” &W ERT & evaluiert werden. Im n¨ achsten Schritt entsteht die Umgebung ◦ < y, w > ◦ < z, g(x, y) > N IL . Nach der Evaluation von g(a, a) und g(x, y) in dieser Umgebung werden g(a, a) und g(x, w) verglichen. Das Programm terminiert mit der folgenden Umgebung als Resultat: ◦ < w, a > ◦ < x, a > ◦ < y, w > ◦ < z, g(x, y) > N IL . Die zugeh¨ orige Substitution lautet σ = {< z, g(a, a) >, < w, a >, < x, a >, < y, a >}
2.
t1 = f (g(x), a) t2 = f (x, a) Beim Vergleich von x mit g(x) sorgt &OCCU RS& f¨ ur das Terminieren mit f ail als Resultat.
3.
t1 = f (x, g(x)) t2 = f (y, y) Es entsteht die Umgebung ◦ < x, y > N IL und beim anschließenden Vergleich von g(y) mit y entsteht f ail‘ als Resultat analog zu 2. .
4.
t1 = f2 (f2 (x, f1 (y)), f1 (x)) t2 = f2 (f2 (h(y), f1 (g(z))), f1 (h(a))) Nach der Unifikation von f2 (x, f1 (y)) und f2 (h(y), f1 (g(z))) liegt als Umgebung vor: ◦ < y, g(z) > ◦ < x, h(y) > N IL .
3.3 Weitere Applikative Programmiersprachen
187
und W ERT wird auf die Reste“ von t1 und t2 angewendet. Die Eva” uhrt wegen der rekursiven Definition von W ERT zu luation von f1 (x) f¨ dem Resultat f1 (h(g(z))) und die Unifikation von t1 und t2 terminiert mit f ail . Ohne die Rekursion w¨ are f1 (h(y)) mit f1 (h(a)) verglichen worden, und es w¨ are das falsche Resultat ◦ < y, a > ◦ < y, g(z) > ◦ < x, h(y) > N IL . mit zwei verschiedenen Bindungen f¨ ur y entstanden. 3.3.6 Scheme Die Programmiersprache Scheme ist ein LISP-Dialekt. Entwickelt wurde Scheme Mitte der siebziger Jahre von Gerald Jay Sussman und Guy L. Steele am Massachusetts Institute of Technology (MIT). Da vor allem Steele stark an der Entwicklung von Common Lisp beteiligt war, finden sich einige Merkmale von Common Lisp in Scheme wieder. So unterst¨ utzt Scheme auch imperative und objektorientierte Programmierung. Letzteres ist nicht direkt vorgesehen, l¨ aßt sich jedoch u ¨ber Makros relativ leicht realisieren. Sußman und Steele nannte ihre Sprache zun¨achst Schemer“. Mit diesem ” Namen wollten sie darstellen, daß es sich um eine Art Schema-Sprache handelt, die leicht an individuelle Bed¨ urfnisse anpaßbar ist. Da jedoch ihr damaliges Betriebssystem, das ITS operating system“, eine Namensbeschr¨ankung auf ” 6 Zeichen besaß , wurde aus Schemer“ der Name Scheme“. Scheme ist so” ” mit eine programmierbare Programmiersprache, die vom Programmierer bei Bedarf sehr flexibel erweitert werden kann. Die erste Beschreibung vom Scheme erschien 1975, der erste revised re” port“ im Jahre 1978. Als erstes wurde Scheme an den Universit¨aten MIT, Yale und der Indiana University f¨ ur Ausbildungszwecke eingesetzt. Inzwischen findet es weltweit Verwendung. 3.3.6.1 Datentypen Als Datentypen stehen in Scheme unter anderem zur Verf¨ ugung: - list (Listen) - integer (ganze Zahlen) - rational (Br¨ uche) - real (Dezimalzahlen) - complex (komplexe Zahlen) - symbol (Zeichen) - string (Zeichenktette) - booleon (Wahrheitswerte) - procedure
188
3 Programmiersprachen
Die Zahlen k¨ onnen beliebig lang sein. Die Wahrheitswerte Wahr“ und ” Falsch“ werden durch #t und #f dargestellt, wobei Scheme streng genommen ” nur #f als wirklich falsch interpretiert; alles andere gilt als Wahr“. Zeichen” reihen werden in ” eingeschlossen, z.B. ”Hallo”. Standardm¨ aßig sind die u ¨blichen Operatoren wie cons, car, cdr, +, - usw. vordefiniert. Hierbei handelt es sich um implizit definierte Prozeduren, die vom Programmierer jederzeit umdefiniert werden k¨onnen. Eine leere Liste wird mit (quote ( )) gebildet. Durch cons kann eine Liste erweitert werden, durch car und cdr wird sie verk¨ urzt. Mittels quote wird angegeben, daß Scheme den nachfolgenden Ausdruck nicht interpretieren, sondern als Liste darstellen soll. Anstelle von (quote()) kann man auch abk¨ urzend ’() schreiben. Beispiele 3.3-25 1) (cons 6 (quote())) 2) (cons 1 (cons 2(cons 3(cons 4(quote()))))) Da die Standardoperatoren vordefinierte Prozeduren, d.h. Funktionen, sind, werden sie wie diese angewandt. Die syntaktische Regel lautet < procedurecall >::= (< operator >< operand1 > · · · < operandn >) Prozeduraufrufe (Funktionsaufrufe) sind somit stets in Klammern eingeschlossen, d. h. es liegt eine vollst¨ andig geklammerte Pr¨afixnotation vor. Damit kommt Scheme mit nur einer einzigen Pr¨ azedenz f¨ ur alle Operatoren aus (dagegen hat die Programmiersprache C u ber zw¨ o lf Pr¨ azedenzstufen.) ¨ 3.3.6.2 Globale Definitionen Mit Hilfe des Schl¨ usselwortes define wird ein Name mit einem Wert verbunden. Diese Bindung ist global, d. h. der Name kann an jeder beliebigen Stelle im Programm nach der Definition verwendet werden. Eine Definition wird stets in Klammern eingeschlossen. Da Prozeduren in Scheme ebenfalls Daten (d. h. Werte) sind, k¨ onnen mit define auch globale Prozeduren definiert werden. Beispiele 3.3-26 1. (define a-number 13) Durch diesen Code wird der Name a-number mit der Zahl 13 verbunden. 2. (define square) (lambda (x) (∗ x x))) Durch diesen Code wird der Name square mit der Funktion (λ (x) (∗ x x)) verbunden, die eine Zahl quadriert.
3.3 Weitere Applikative Programmiersprachen
189
Namen k¨ onnen auch umdefiniert werden. Eine derartige Umdefinition zeigt das folgende Beispiel: Beispiel 3.3-27 Durch (define a 1) wird dem Namen a der Wert 1 zugewiesen. Folgt anschließend der Code (define a ”Hallo”) so wird nun a zu einer Zeichenreihe mit dem Wert Hallo“. ” Welchen Wert eine Variable besitzt, kann mit den sogenannten Pr¨adikatfunktionen festgestellt werden. Sie bestehen aus dem Namen des Datentyps gefolgt von einem Fragezeichen, z. B. String?. Das Beispiel zeigt, dass Scheme eine dynamisch getypte Sprache ist, d. h. die Variablen m¨ ussen nicht explizit mit einem Typ deklariert werden. Ein Typ ergibt sich indirekt aus der Definition bzw. der Anwendung. Define-Deklarationen werden meist benutzt um auf globaler Ebene Funktionen und Konstanten zu deklarieren. Sie k¨ onnen jedoch auch innerhalb des Rumpfes einer Prozedur verwendet werden. Die Sichtbarkeit der so gebundenen Variablen beschr¨ ankt sich auf den Rumpf, in dem die Definition steht. Define-Deklarationen, die nicht auf globaler Ebene stehen, werden interne Deklarationen genannt. Die allgemeine Syntax einer define-Deklaration lautet < def ine − declaration > ::= (def ine < N ame > < ausdruck >) Innerhalb von < ausdruck > darf < name > rekursiv verwandt werden. 3.3.6.3 Lokale Deklarationen Neben globalen Deklarationen gibt es auch lokale Deklarationen. Diese erfolgen u usselwort let, bzw. die Varianten let* und letrec. Durch let ¨ber das Schl¨ lassen sich an mehrere Namen jeweils ein Ausdruck (≈ Wert) binden. Diese Bindungen sind ausschließlich innerhalb des Rumpfes des let-Ausdrucks sichtbar, d.h. dort g¨ ultig. Die allgemeine Syntaxdefinition lautet: < let − Ausdruck >::= (let((< name1 >< Ausdruck1 >) ... ()) ) Die Ausdr¨ ucke Ausdruck1 bis Ausdruckn werden zun¨achst in einer nicht spezifizierten Reihenfolge ausgewertet und die resultierenden Werte an die entsprechenden Namen gebunden. Anschließend wird der Rumpf des letAusdrucks ausgewertet. Die Bindungen der Werte an die Namen gelten erst
190
3 Programmiersprachen
im Rumpf. Dies bedeutet u. a., daß man in einem Ausdrucki nicht auf einen Namen zugreifen kann, der im selben let-Ausdruck gebunden ist. Der Wert des letzten Ausdrucks im Rumpf ergibt den Wert des gesamten let-Ausdrucks. Da die Auswertungsreihenfolge der Ausdr¨ ucke nicht festgelegt ist und theorethisch sogar alle Ausdr¨ ucke parallel ausgewertet d¨ urfen, spricht man auch von einem parallelen let. Beispiele 3.3-28 1. Der Code (let ((a 1) (b (+ 2 3)) (c (λ (n) (* n 2)))) (c (+ a b)) ) liefert das Ergebnis 12. 2. Der Code (let ((a 1) (let ((a 0) (b a)) b ) liefert das Ergebnis 1. Intern u ¨bersetzt das Scheme-System eine let-Deklaration in einen Funktionsaufruf: Beispiel 3.3-29 Der Code (let ((a (+ 1 1)) (b (+ 2 2)) (+ a b) ) wird intern zu ((λ (a b) (+ a b)) (+ 1 1) (+ 2 2)) u ¨bersetzt. Von let unterscheidet sich let* dadurch, daß die Reihenfolge, in der die Ausdr¨ ucke ausgewertet werden, festgelegt ist: sie erfolgt von oben nach unten (von links nach rechts). Man spricht daher auch von einem sequentiellen let*. Bei den weiter unten (rechts) stehenden Bindungspaaren kann auf die weiter oben (links) eingef¨ uhrten Namen zugegriffen werden.
3.3 Weitere Applikative Programmiersprachen
191
Beispiel 3.3-30 Der Code (let ((a 1)) (let* ((a 0) (b a)) b ) ) liefert das Ergebnis 0. Innerhalb der let*-Deklaration wurde die Bindung von 1 an a aus der let-Deklaration durch die Bindung von 0 an a ersetzt. Analog zur let-Deklaration wird eine let*-Deklaration intern in einen, in diesem Fall verschachtelten, Funktionsaufruf umgewandelt. Beispiel 3.3-31 Der Code (let* ((a (+ 1 1)) (b (+ a 1))) (x a b) ) wird zu ((lambda (a) ((lambda (b) (+ a b) (+ a 1))) (+ 1 1) ) u ¨bersetzt. Die letrec-Deklarationen besitzen die selbe Syntax wie let-Deklarationen. Der Unterschied zu let-Deklarationen bzw. let*-Deklarationen liegt in der Sichtbarkeit der zu bindenden Namen. Die Namen, d. h. die linken Seiten der Bindungspaare, k¨ onnen in jedem Ausdruk der Bindungspaare verwendet werden. Die bei let* bestehende Einschr¨ ankung, daß sich die Verwendung eines Namens nur auf weiter oben (links) eingef¨ uhrte Namen besitzen kann, besteht hier nicht. Dies erlaubt insbesondere die Definition lokal rekursiver Funktionen. Beispiel 3.3-32 Der Code (letrec ((sum lambda (first s) (if (null ? first) a (sum (alr first) (+ s (car first)))))) (sum (list 1 2 3) 0)) liefert das Ergebnis 6.
192
3 Programmiersprachen
Mit Hilfe von letrec k¨ onnen auch wechselseitig rekursive Funktionen definiert werden Beispiel 3.3-33 Der Code (letrec ((even? (lambda (n) (if (zero? n) #t (odd? (- n 1))))) (odd? (lambda(n) (if (zero? n) =f (even? (- n 1))))) ) (even? 42) ) liefert das Ergebnis #t ( true“). ” Eine let-Deklaration kann auch benannt werden. Die Syntax hierf¨ ur lautet < name let > :: (let < name > (< bindings >) < rumpf >) Hierbei sind < bindings > die von let-Deklarationen her bekannten Paare (Bindungen) aus Name und Ausdruck (Wert). Der < rumpf > l¨aßt sich als Rumpf einer Funktion (Prozedur) auffassen, die den Name < name > und genau so viele Parameter besitzt, wie Bindungspaare in < bindings > angegeben sind Beispiel 3.3-34 Das Bsp. 3.3-32 f¨ ur eine letrec-Deklaration kann unter Verwendung einer named let-Deklaration geschrieben werden als (let sum ((first (list 1 2 3)) (s 0)) (if (null? first) s (sum (cdr first) (+ s (car first))) ) ) Anstatt einer letrec-Deklaration kann jedoch auch eine define-Deklaration benutzt werden.
3.3 Weitere Applikative Programmiersprachen
193
Beispiel 3.3-35 Der Code (let ((x 5)) (letrec ((f (lambda (y) (bar x y))) (bar (lambda (a b) (+ * a b) a)))) (f (+ x 3)) ) ) entspricht dem Code (let ((x 5)) (define (f y) (bar x y)) (define (bar a b) (+ (* a b) a)) (f(+ x 3)) 3.3.6.4 Prozeduren Prozeduren stellen in Scheme ein wesentliches Sprachkonstrukt dar. Der Name ¨ ist jedoch mißverst¨ andlich. Ublicherweise bezeichnet man mit Prozeduren im Rahmen der imperativen (prozeduralen) Programmierung Unterprogramme, die auch Seiteneffekte besitzen k¨ onnen. Bei Scheme handelt es sich hierbei um Funktionsdefinitionen. Die Syntaxdefinition lautet < procedure > ::= (lambda < f ormals > < body >) Hierbei sind < f ormals > die Liste der formalen Parameter und < body > der Funktionsrumpf. Mit Hilfe von define k¨ onnen Prozeduren benannt werden. Beispiel 3.3-36 Der Code (define beispiel (lambda (arg1 arg2) ... ) ) erzeugt eine zweiparametrige Prozedur mit dem Namen beispiel. Der define- und der lambda-Ausdruck k¨ onnen auch abk¨ urzend zusammengefaßt werden.
194
3 Programmiersprachen
Beispiel 3.3-37 Der Code aus Bsp. 3.3-36 kann verk¨ urzt werden zu (define (beispiel arg1 arg2) ... ) Aufrufe von Prozeduren sind stets in Klammern eingeschlossen. Die Syntax f¨ ur einen Prozeduraufruf lautet < procedure null > ::= (< operator > < operand1 > . . . ) Prozeduraufrufe werden auch combinations genannt. Beispiele 3.3-38 1. Die Prozedur aus den Bsp. 3.3-36 wird aufgerufen mit (beispiel wert1 wert2) 2. Die Standardoperatoren sind vordefinierte Prozeduren. Daher k¨onnen sie durch eine neue Prozedurdeklaration umdefiniert werden. Der Code (define (+ x y) (- x y) bewirkt, daß der Standardoperator + bei einem Aufruf nicht mehr addiert sondern subtrahiert. 3.3.6.5 Fallunterscheidungen Scheme sieht mehrere M¨ oglichkeiten zur Programmierung von Fallunterscheidungen vor. Die wichtigsten sind if und cond. Dar¨ uber hinaus gibt es u. a. noch when, unless, case um Bedingungen zu realisieren. Ein if-Ausdruck besitzt die Syntax < if expression > ::= (if < test > < consequent > < alternate >) bzw. < if expression > ::= (if < test > < consequent >) Zun¨ achst wird der test-Ausdruck ausgewertet. Je nach dessen Wahrheitswert wird anschließend der zweite Operand (Konsequenz) oder der dritte Operand (Alternative) ausgewertet. Zu beachten ist, daß die Alternative nur ausgewertet wird wenn der Test-Ausdruck den Wert #f besitzt. Andernfalls wird die Konsequenz ausgewertet, da alle Ergebnisse außer #f als wahr betrachtet werden. If-Ausdr¨ ucke k¨ onnen auch innerhalb von Ausdr¨ ucken verwendet werden.
3.3 Weitere Applikative Programmiersprachen
195
Beispiele 3.3-39 1. Der Code (if (> 3 2) ja nein) liefert als Ergebnis ja“. ” 2. Der Code (if (> 2 3) ja nein) liefert als Ergebnis nein“. ” 3. Der Code (if (> 3 2) (− 3 2) (+ 3 2) ) liefert als Ergebnis ”1“. 4. Der Code (∗ 4 (if (> x max) max x)) ist ein Beispiel f¨ ur die Verwendung eines if-Ausdrucks innerhalb eines anderen Ausdrucks. Mit Cond-Ausdr¨ ucken ist es m¨ oglich mehrere F¨alle zu unterscheiden. Die Syntax lautet < cond expression > ::= (cond < clause1 > < clause2 > . . . ) < clause > ::= (< test > < expression > Das letzte < clause > kann ein else-Ausdruck der Form < else expression > ::= (else < expression >) sein. Eine Fallunterscheidung besteht somit aus einem Test und einer Konsequenz. Die F¨ alle werden von links nach rechts u uft. Liefert das Ergebnis ¨berpr¨ von < test > einen Wert ungleich #f, so wird die Konsequenz ausgewertet. Das Ergebnis der Auswertung ist der Wert des Cond-Ausdrucks. Trifft keiner der F¨ alle zu, so wird der else-Ausdruck als Wert ausgewertet. Ist kein else-Fall vorhanden, ist der Wert des Cond-Ausdrucks undefiniert. Beispiel 3.3-40 (cond ((= wert 0) f ) ((= wert 1) t) (else undef ) )
196
3 Programmiersprachen
3.3.6.6 Rekursionen Aus Effizienzgr¨ unden besitzt jedes Scheme-System Mechanismen zur Optimierung von Endrekursionen, auch endst¨ ammige Rekursionen, tail-Rekursionen oder Postrekursionen genannt (s. Kap. 4.1). Hierdurch wird der Speicherplatzbedarf und die Laufzeit des Programms deutlich verringert. Der Effekt verdeutlicht das nachfolgende Beispiel. Beispiel 3.3-41 Der folgende Code ist eine m¨ ogliche Version zur Berechnung der Fakult¨atsfunktion: (def ine (f ak1 n) (if (= n 0) 1 (∗ n (f ak1 (− n 1))) ) ) Bei dieser Version ohne Endrekursion entstehen bei Anwendung von fak1 auf das Argument 4 folgende Zwischenschritte: (fak1 4) (* 4 (fak1 3)) (* 4 (* 3 (fak1 2))) (* 4 (* 3 (* 2 (fak1 1)))) (* 4 (* 3 (* 2 (* 1 (fak1 0))))) (* 4 (* 3 (* 2 (* 1 1)))) (* 4 (* 3 (* 2 1))) (* 4 (* 3 2)) (* 4 6) 24 Man sieht, daß diese Version der Programmierung der Fakult¨atsfunktion w¨ahrend des Ablaufs zunehmend mehr Speicherplatz zum Speichern von Zwischenergebnissen ben¨ otigt. Bei einer endst¨ ammigen Version tritt dieser Effekt nicht auf: (define (fak2 n a) (if (= n 0) a (fak2 (- n 1) (* n a)) ) ) (define (fak n) (fak2 n 1) )
3.3 Weitere Applikative Programmiersprachen
197
Die Zwischenschritte bei der Anwendung auf das Argument 4 ergeben sich zu: (fak 4) (fak2 4 1) (fak2 3 4) (fak2 2 12) (fak2 1 24) (fak2 0 24) 24 Scheme erkennt, daß das Ergebnis des Prozeduraufrufs nur noch zur¨ uckgegeben wird und kann somit f¨ ur alle Aufrufe denselben Speicherplatz verwenden. Die zus¨ atliche Variable a in der Prozedur fak2 akkumuliert die Zwischenergebnisse. 3.3.6.7 Programmierbeispiele Im folgenden soll anhand der Aufgabe, ein Programm zu erstellen, welches die Summe der Elemente einer Liste berechnet, gezeigt werden, welche vielf¨altigen M¨ oglichkeiten Scheme bietet, eine derartige Aufgabe zu l¨osen. Die erste Variante des L¨ osungsprogramms lautet (define (list sum 1 list) (cond ((null? list) 0) (else + (car list) (list sum 1 (cdr list))) ) ) Die erste Zeile definiert den Programmnamen und die Parameteranzahl, in diesem Fall ein Parameter mit Namen list“. Der Rumpf besteht aus einer ” Fallunterscheidung auf der Basis eines Cond-Ausdrucks. Die Fallunterscheidung beginnt mit der (einzigen) Abbruchbedingung, die u uft, ob die ¨berpr¨ Liste leer ist. In diesem Fall ist das Ergebnis ”0“, anderenfalls wird rekursiv aufaddiert. Verwendet man an Stelle des Cond-Ausdrucks einen If-Ausdruck und benennt man die Standardoperatoren car und cdr um, so erh¨alt man das Programm (define first car) (define rest cdr) (define (list sum 2 list) (if (null? list 0 (+ (first list) (list sum 2 (rest list))) ) )
198
3 Programmiersprachen
Dies ist ein Programm mit einer Rekursion, jedoch handelt es sich nicht um eine Endrekursion. Eine m¨ ogliche L¨ osung mit einer Endrekursion w¨are (define (list sum 3 acc list) (if (null? list) acc (list sum 3 (+ (first list) acc) (rest list))) ) ) (define (list sum 4 list) (list sum 3 0 list) ) Alternativ l¨ aßt sich dieses Programm schreiben (define (list sum 4 list) (define (list sum 3 acc list) (if (null? list) acc (list sum 3 (+ (first list) acc) (rest list))) ) (list sum 3 0 list) ) An den letzten beiden Programmbeispielen sieht man, wie leicht man in Scheme Code-St¨ ucke verschieben kann. Es ist kein Problem, eine interne Definition auf ein h¨ oheres Level zu verschieben bzw. eine ehemals globale Funktion intern einzusetzen. Hat man bei der Programmerstellung Probleme mit der L¨osung einer Definition, so kann man einfach einzelne Teile auslagern, separat testen und anschließend wieder zusammenf¨ ugen. Eine weitere L¨ osung auf der Basis einer letrec-Deklaration ist (define (list sum 5 list) (letrec ((list sum 6 (lambda (acc list) (if (null? list) acc (list sum 6 (+ acc (first list)) (rest list))) ) ) (list sum 6 0 list) ) Bei der Verwendung einer Named let-Deklaration erh¨alt man (define (list sum 7 list) (let loop ((acc 0) (a1 list))
3.3 Weitere Applikative Programmiersprachen
199
(if (null? a1) acc (loop (+ acc (first a1)) (rest 1))) ) ) Man sieht gut die Einf¨ uhrung der Schleifenvariablen. Der Variablen acc wird 0“ zugeordnet und der Variablen a1 wird die Ausgangsliste zugeordnet. Beim ” rekursiven Aufruf werden die beiden Variablen entsprechend angepasst: zu acc wird der Wert des ersten Elements von list addiert und in der Liste um ein Element vorger¨ uckt. ¨ Die Variante list sum 2 stellt eine bereits vereinfachte Form dar. Ublicherweise w¨ urde ein Programmierer zun¨ achst programmieren (define (list sum 8 (lambda (list) (define list sum 9 (lambda (acc list) (if (null? list) 0 (+ (first list) (list sum 8 (rest list))))))) (list sum 9 0 list) ) ) An Stelle der bisherigen rekursiven L¨ osungen lassen sich in Scheme auch qua” si iterative“ L¨ osungen programmieren. (define (list sum 10 list) (do ((acc 0 (+ acc (first a1))) (a1 list (cdr a1))) ((null? a1)acc) ) ) Hier wird zun¨ achst der Variablen acc den Wert 0“ zugewiesen. Bei jeder Ite” ration wird zu acc der Wert des ersten Elements der Restliste addiert. Der Variablen a1 wird zun¨ achst die Ausgangsliste zugewiesen und danach in jeder Iteration die Restliste zugeordnet. Die Abbruchbedingung pr¨ uft, ob a1 leer ist. In diesen Fall ist der Wert von acc das Endergebnis. Eine weitere iterative Variante ist (define (list sum 11 list) (do ((acc 0) (a1 list)) ((null? a1 acc) (set! acc (+ acc first a1)))
200
3 Programmiersprachen
(set! a1 (rest a1)) ) ) Diese Variante zeigt zum ersten Mal, dass man in Scheme auch nicht seiteneffektfrei, d. h. prozedural programmieren kann. Das auf set folgende ! weist den Programmierer auf die Gefahr an dieser Stelle hin. Im Prinzip erreicht man mit (set! variable neuer-Wert) genau den Effekt, den sonst eine Wertzuweisung hat. So entspricht (set! acc (+ acc (first a1))) der Wertzuweisung acc := acc + first(a1) 3.3.6.8 Scheme-Systeme Nach den Anf¨ angen am MIT und den nachfolgenden Arbeiten in Yale und an der Indiana University fand Scheme schnell weltweite Verbreitung. Besonders im Ausbildungsbereich wird es oft eingesetzt. Inzwischen existiert ein SchemeStandard in Form des IEEE-Standards 1178-1990, der 1991 gefaßt wurde. Die derzeit akktuellste Spezifikation tr¨ agt die Bezeichnung R5RS und ist 1998 erschienen. An der Version R6RS wird noch gearbeitet. Weltweit existieren eine Reihe von Scheme-Implementierungen. Hiervon seien nur einige kurz charakterisiert: • MIT-Scheme Es handelt sich um ein sehr umfangreiches und sehr ausgereiftes System. Es enth¨ alt eine Reihe von interessanten Erweiterungen, so u.a. ein Objektsystem und Graphik-Funktionen. • Guile Hierbei handelt es sich um eine Skript-Sprache • Elk Das Elk-System wurde mit dem Ziel entwickelt, auf einfache Art C- oder C++- Code einbinden zu k¨ onnen. • Scsh Wird vor allem zur Shell-Programmierung eingesetzt. • Kawa Hierbei handelt es sich um ein Scheme-System, welches in Java geschrie¨ ben wurde. Es erm¨ oglicht auch die Ubersetzung nach Java byte-code.
3.3 Weitere Applikative Programmiersprachen
201
• Dr. Scheme Dieses System wurde speziell f¨ ur Ausbildungszwecke entwickelt und ist fast so umfangreich wie MIT-Scheme. 3.3.7 Miranda Die Sprache Miranda wurde von David Turner zwischen 1983 - 1986 als Nachfolgesprache von SASL und KRC entwickelt. Auch Elemente der Sprache ML flossen bei der Entwicklung ein. Der Name stammt nicht von dem gleichnamigen Mond des Planeten Uranus, sondern stammt von dem lateinischen Verb miror“ und bedeutet soviel wie verwundert sein“ oder bewundern“. ” ” ” Miranda wurde sp¨ ater von der englischen Firma Research Software Ltd. vermarktet und war somit die erste applikative bzw. funktionale Sprache, die auch kommerziell eingesetzt wurde. Programme werden als Script“ bezeichnet. Ein einfaches Beispiel f¨ ur ein ” Miranda-Script ist z = sq x / sq y sq n = n ∗ n x =a+b y =a-b a = 10 b =5 Es ist eine streng getypte Sprache mit verz¨ ogerter Auswertung ( lazy evaluati” on“ ). Ihre wichtigste Datenstruktur ist die Liste. Sie wird in eckige Klammern eingeschlossen. Als wichtigste Listenoperatoren stehen ++, : # , ! , −− zur Verf¨ ugung. Beispiele 3.3-42 1. Werktage = [”Mon”, ”Die”, ”Mitt”, ”Don”, ”Frei”] Wochentage = Werktage ++ [”Sam”, ”Son”] 2. 0 : [1, 2, 3] ergibt [0, 1, 2, 3] # Wochentage ergibt 7 Wochentage ! 0 ergibt ”Mon” [1, 2, 3, 4, 5] - - [2, 4] ergibt [1, 3, 5] Bestehen die Listenelemente aus einer fortschreitenden Anzahl von Zahlen, so kann mittels “..“ eine abk¨ urzende Schreibweise benutzt werden. So l¨aßt sich z.B. die Fakult¨ atsfunktion schreiben als fac n = product [1 .. n] und die Summe der ungeraden Zahlen zwischen 1 und 100 als
202
3 Programmiersprachen
ergebnis = sum [1, 3 .. 100] Alle Elemente einer Liste m¨ ussen den gleichen Typ besitzen. Listenelemente unterschiedlichen Typs werden in Tupeln zusammengefaßt. Im Gegensatz zur Liste werden sie in runde Klammern eingeschlossen. Der Zugriff auf Tupelelemente erfolgt u ¨ber pattern matching“. ” Beispiel 3.3-43 Das Stammblatt f¨ ur einen Angestellten einer Firma kann z. B. lauten angestellter = (”Michael”, ”Hamann”, 39, True, False) Funktionen k¨ onnen durch Fallunterscheidungen definiert werden. Hierzu existieren zwei M¨ oglichkeiten. Die erste M¨ oglichkeit erfolgt u ¨ber die Struktur der formalen Parameter ( pattern matching“). ” Beispiele 3.3-44 1. Die Fakult¨ atsfunktion l¨ aßt sich schreiben als fac 0 =1 fac (n+1) = (n+1) ∗ fac n 2. Die Ackermannfunktion l¨ aßt sich schreiben als ack 0 n = n+1 ack (m+1) 0 = ack m 1 ack (m+1) (n+1) = ack m (ack (m+1) n) 3. Die Berechnung der Fibonacci-Zahlen kann z.B. durch fib 0 =0 fib 1 =1 fib (n+2) = fib (n+1) + fib n erfolgen. Besonders interessant ist pattern matching im Zusammenhang mit Listen und Tupeln. Beispiele 3.3-45 1. sum [ ] = 0 sum (a:x) = a + sum x 2. product [ ] = 1 product (a:x) = a ∗ product x 3. reverse [ ] = [ ] reverse (a:x) = reverse x ++ [a]
3.3 Weitere Applikative Programmiersprachen
203
Die zweite M¨ oglichkeit zur Realisierung von Fallbeispielen stellen guards“ ” dar. Die einzelnen F¨ alle werden zeilenweise aufgelistet und die Pr¨amissen ( if” port“) durch Kommata getrennt. Beispiel 3.3-46 Die Funktion zur Bestimmung des gr¨ oßten gemeinsamen Teilers zweier Zahlen kann durch ggT a b = ggT (a-b) b , if a>b = ggT a (b-a) , if a | ] wobei entweder von der Form var ← exp oder ein Filter ist, der den Bereich der Variablen einschr¨ankt. Beispiele 3.3-47 1. Ein einfaches Beispiel ist [ n ∗ n | n <− [1 .. 100]] 2. Das folgende Skript liefert die Permutationen aller Elemente einer gegebenen Liste permutation [ ] = [[ ]] permutation x = [a : y | a <− x; y <− permutation (x - - [a])] 3. Die Benutzung eines Filters zeigt das Skript factors n = [i | i <− [1..n div 2]; n mod i = 0 ] Diese Schreibweise f¨ ur Listen kann oft benutzt werden, um kurze Programme schreiben zu k¨ onnen. Dies zeigen die folgenden beiden Programme. Beispiele 3.3-48 1. Das bekannte Quicksort-Verfahren l¨ aßt sich folgendermaßen als MirandaScript schreiben:
204
3 Programmiersprachen
sort [ ] = [ ] sort (a:x) = sort [b|b <− x; b<=a] ++ [a] ++ sort [b| b <− x;b>a] 2. Das nachfolgende Miranda-Script liefert beim Aufruf durch (queens 8) eine Liste aller L¨ osungen f¨ ur das Acht-Damen-Problem. queens 0 = [[]] queens (n+1)=[q:b|b <− queens n;q <− [0..7];safe q b] safe q b = and [∼checks q b i | i <−[0..# b-1]] checks q b i = qb!i ∨ abs(q - b!i)=i+1 Die Auswertung der Argumente erfolgt nach dem Prinzip der verz¨ogerte Auswertung ( lazy evaluation“), d. h. ein Argument wird erst dann ausgewertet, ” wenn es wirklich ben¨ otigt wird. Hierdurch lassen sich unendliche Datenstrukturen behandeln. Beispiel 3.3-49 Die Berechnung der Fibonacci-Zahlen kann anstelle des Script aus Beispiel 3.3-44 auch folgendermaßen erfolgen fib 0 = 1 fib 1 = 1 fib (n+2=flist!(n+1)+flist!n where flist=map fib [0..] Miranda ist eine streng getypte Sprache, d. h. jeder Ausdruck besitzt einen ¨ festen Typ. Dieser kann bereits bei der Ubersetzung festgestellt werden und seine Konsistenz u uft werden. ¨berpr¨ Wie aus den bisherigen Beispielen ersichtlich ist, besitzt Miranda drei primitive Basistypen: num, bool und char. -
Der Basistyp num umfaßt Integer- und Gleitkomma-Zahlen. Ihre Unterscheidung erfolgt erst zur Laufzeit.
-
Der Basistyp bool umfaßt die Werte True und False.
-
Der Basistyp char umfaßt die Menge der ASCII-Zeichen. Einzelne Zeichen werden in ’ ’ eingeschlossen, z. B. ’a’, ’8’ oder ’3’.
Ist T ein spezieller Typ, dann ist [T] vom Typ Liste, deren Elemente vom Typ T sind. So ist z. B. [[1,2],[1,3],[1,4]] eine Liste von Listen von Zahlen, d.h. vom Typ [[num]]. Sind T1 bis Tn verschiedene Typen, dann ist (T1 , . . . , Tn ) vom Typ Tupel, wobei die einzelnen Komponenten vom Typ Ti sind. So ist z. B.
3.3 Weitere Applikative Programmiersprachen
205
(True, ”hallo”, 61) vom Typ (bool, [char], num). Sind T1 und T2 Typen, dann ist T1 − > T2 vom Typ Funktion, wobei die Argumente vom Typ T1 und das Ergebnis vom Typ T2 ist. Dem Benutzer ist es u ¨berlassen, ob er Typdeklarationen explizit angibt ¨ oder nicht. Falls sie nicht spezifiziert sind, werden sie zur Ubersetzungszeit bestimmt. Ihre Angabe tr¨ agt jedoch meistens zur besseren Lesbarkeit der Programme bei. Die Spezifikation eines Typs erfolgt durch ::“. So kann z. B. ” der Typ f¨ ur eine Funktion, die eine Zahl quadriert durch quadrat :: num − > num quadrat n :: n x n spezifiziert werden. Bei den Typen handelt es sich um polymorphe Typen, d. h. Operatoren d¨ urfen u ¨berladen werden, dies wird durch das Zeichen * dargestellt. Beispiel 3.3-50 Die Typen aus einigen der bisherigen Beispielen ergeben sich zu fac :: num − > num permutation :: [*] − > [[*]] sum :: [num] − > num reverse :: [*] − > [*] ack :: num − > num − > num Neue komplexe Datenstrukturen k¨ onnen vom Benutzer durch Typdeklarationen eingef¨ uhrt werden. Die Syntax hierf¨ ur lautet < identifier> :: = Ein bin¨ arer Baum, dessen Knoten Zahlenwerte sind, l¨aßt sich folgendermaßen typisieren binbaum :: = Nilt | Node num binbaum binbaum Hierbei sind Nilt und Node Konstruktoren, wobei Nilt ein Basiskonstrutor ist und Node ein Konstruktor mit drei Argumenten. Ein konkreter Baum w¨are z. B. t = Node 6 (Node 3 Nilt Nilt) (Node 5 Nilt Nilt) M¨ochte man B¨ aume mit beliebigen Werten spezifizieren (polymorpher Datentyp), so erfolgt dies wieder u ¨ber die *-Notation. Bei einer Baumstruktur kann dies z. B. durch baum* ::= Nilt | Node* (baum*) (baum*) erfolgen.
206
3 Programmiersprachen
Aufz¨ ahlungstypen k¨ onnen z. B. durch farbe ::= rot | gelb | gr¨ un | blau eingef¨ uhrt werden. Dar¨ uber hinaus k¨ onnen auch bereits existierende Typen, z. B. Basistypen, vom Benutzer umbenannt werden. Dies geschieht durch ==“. Ein Beispiel ” ist string == [char] Hiervon sollte man jedoch nur in Ausnahmef¨ allen Gebrauch machen, da dies nicht zur Verbesserung der Lesbarkeit eines Programms beitr¨agt. Miranda erlaubt, neben den bisher behandelten M¨oglichkeiten eigenst¨andige Typen einzuf¨ uhren, auch die Einf¨ uhrung auf der Basis der Theorie der abstrakten Datentypen. Zur Erl¨ auterung sei hierf¨ ur zum Abschluß das Beispiel eines stacks“ angegeben: ” abstype stack * with empty :: stack* isempty :: stack * − > bool push::*− >stack*− >stack* pop::stack*− >stack* top::stack*− > * stack*==[*] empty=[] isempty x=(x=[]) push a x = (a:x) pop(a:x)=x top(a:x)=a 3.3.8 Haskell Bis zu der Mitte der 80er-Jahre hatten sich eine große Vielzahl unterschiedlicher applikativer und funktionaler Programmiersprachen entwickelt. Bei ihrer Entwicklung standen die verschiedensten Motivationen im Vordergrund. Was eine weltweiten Verbreitung im Wege stand, besonders im kommerziellen Bereich, war eine Standardisierung, wie sie - in einem gewissen Maße - nur durch LISP gegeben war. Daher wurde 1987 ein internationales Kommitee ins Leben gerufen, um eine Standard“-Sprache zu definieren. Ihm geh¨orten u.a. Paul ” Hudak, Simon Peyton Jones und Philip Wadler an. Zun¨ achst war geplant, Miranda als Ausgangssprache zu verwenden. Da die Zusammenarbeit mit den Entwicklern von Miranda aber nicht sehr gut funktionierte, ging man dazu u ¨ber, eine eigenst¨andige Sprache zu entwerfen. Dennoch finden sich in Haskell viele Elemente von Miranda wieder. Das Ergebnis war das 1990 vorgestellte Haskell 1.0. Im Jahre 1992 erschien die Haskell
3.3 Weitere Applikative Programmiersprachen
207
1.2 Sprachdefinition und 1995 der Vorschlag f¨ ur Haskell 1.3. Die zur Zeit am meisten verwendete Variante ist der Haskell-98-Standard von 1999. Benannt wurde die Sprache nach dem amerikanischen Mathematiker Haskell Brooks Curry. Seine Arbeiten zur mathematischen Logik lieferten maßgebliche Grundlagen f¨ ur die Entwicklung funktionaler und applikativer Sprachen. 3.3.8.1 Sprachkonzepte ¨ Uberblick Haskell ist eine applikative Programmiersprache, die auf dem Lambda-Kalk¨ ul basiert. Aus diesem Grund wird auch der griechische Buchstabe Lambda als Logo verwendet. Die wichtigsten Implementierungen sind der Glasgow Haskell Compiler (GHC) und Hugs, ein Haskell-Interpreter. Daneben exisiteren zahlreiche Spachderivate. Hierzu z¨ ahlen u.a. Parallel Haskell, Distributed Haskell, Goffin, Eager Haskell, Eden, DNA-Haskell, Haskell ++, O’Hasekell und Mondrian. Haskell wurde konzipiert als nicht-strikte applikative Programmiersprache mit einem strengen Typ-Konzept. Da Haskell eine nicht-strikte Sprache ist, werden nur diejenigen Ausdr¨ ucke ausgewertet, die f¨ ur die Berechnung des Ergebnisses gebraucht werden. Die Implementierung beruht somit auf lazy-evaluation“ bzw. call-by-need“. Diese Bedarfsauswertung erlaubt das ” ” Arbeiten mit (partiell) undefinierten Werten und potentiell unendlich großen Datenmengen. Beispiele 3.3-51 1. Gegeben sei die Funktion first x y = x Die Funktion first liefert bei Eingabe zweier Parameter nur den ersten als Ergebnis zur¨ uck. Wird also die Funktion first z.B. durch first x (2+6) gestartet, so ist die Auswertung der Summe (2+6) zur Ergebnisbestimmung nicht notwendig und wird daher auch nicht durchgef¨ uhrt. 2. Gegeben sei die Funktion quadrat x = x ∗ x Die Funktion quadrat berechnet bei Eingabe eines Parameters dessen Quadrat. Wird die Funktion quadrat z.B. durch quadrat (2+6) gestartet, so muß im Rahmen der Auswertung der Ausdruck (2+6) ∗(2+6) berechnet werden. Haskell berechnet nun den Teilausdruck (2+6) nur einmal und verwendet das Ergebnis zweimal.
208
3 Programmiersprachen
Haskell besitzt ein strenges Typ-Konzept. So wird z. B. streng zwischen Wahrheitswerten, ganzen Zahlen, Gleitkommazahlen und Character unterschieden. Von der Konzeption her ist Haskell statisch typisiert. Es beinhaltet jedoch auch Erweiterungen f¨ ur dynamische Typen. Somit stehen in den meisten F¨ allen bei den Berechnungen die Typen bereits zum Zeitpunkt der Programm¨ ubersetzung fest, wodurch Fehler noch vor der Ausf¨ uhrung des Programms erkannt werden k¨ onnen. Da Typvariablen erlaubt sind, k¨ onnen Funktionen sehr allgemein formuliert werden. Wird eine allgemeingehaltene Funktion f¨ ur bestimmte Typen verwendet, so werden automatisch die Typen abgeglichen (s. Typinferenz). Beispiel 3.3-52 Gegeben sei die Typ-Definition map :: (a → b)− > [a] − > [b] Diese Funktion wendet eine beliebige Funktion auf die Elemente einer Liste an. Wird sie nun mit der speziellen Funktion toUpper aufgerufen, die von Typ char - > char ist, so ergibt der Typabgleich map toUpper :: [char] - > [char] Die erhaltene Funktion map toUpper kann jetzt z.B. als eine Funktion angesehen werden, die alle Kleinbuchstaben eines Textes in Großbuchstaben umwandelt. Neben den in der Spachdefinition fest vorgegebenen Datentypen k¨onnen zus¨ atzliche Datentypen vom Benutzer definiert werden. Grundlage hierbei ist die Theorie der algebraischen Datentypen. Die Konstruktion erfolgt mit Hilfe von Datenkonstruktoren. Beispiel 3.3-53 Die Datenstrukrur eines mit ganzen Zahlen beschrifteten bin¨aren Baumes l¨aßt sich durch den Benutzer mit data Tree Int = Leaf Int / Node Int (Tree Int) (Tree Int) definieren. Der so definierte Baum (Tree Int) besteht somit entweder aus einem (einzigen) Blatt (Leaf Int) oder einem Knoten (Node Int t1 t2 ), wobei t1 und aume repr¨ asentieren, die wiederung die Struktur (Tree Int) besitzen. t2 Teilb¨ Zur Definition dieser Datenstruktur wird sowohl der einstellige Konstruktor Leaf“ als auch der dreistellige Konstruktor Node“ verwendet. ” ” Haskell unterst¨ utzt dar¨ uberhinaus Typklassen. Hiermit lassen sich Typen zusammenfassen, die ¨ ahnliche Operationen verwenden. Alle Auspr¨agungen einer
3.3 Weitere Applikative Programmiersprachen
209
Methode einer Typklasse tragen den gleichen Namen. In gewisser Weise ent¨ sprechen Typklassen dem Uberladen von Funktionen. Der gleiche Funktionsname steht also in Abh¨ angigkeit vom Typ der angewandten Argumente f¨ ur verschiedene Funktionen. Z.B. ist mit der == - Methode der Klasse Eq sowohl der Vergleich zweier Zahlen als auch zweier Texte m¨oglich. Trotz gleichen Namens ist die funktionelle Vorgehensweise unterschiedlich. Haskell unterst¨ utzt auch Funktionale. Die Implementierung beruht u ¨blicherweise auf einer Curryfizierung. Damit wird eine partielle Auswertung von Funktionen m¨ oglich. Pattern-Matching ist ebenfalls Bestandteil von Funktionsdefinitionen, d.h. Konstruktorterme d¨ urfen als formale Parameter verwendet werden. Hierbei sind die Parameterterme die Muster (Pattern) der Funktionsargumente. Beispiel 3.3-54 Gegeben sei das Programm fak :: Integer -> Integer fak 0 = 1 fak n = n ∗ fak (n-1) Die Funktion fak berechnet die Fakult¨ at einer Zahl. Angewandt auf das Argument 0 ergibt sich 1 als Ergebnis. In allen anderen F¨allen erfolgt die Berechnung nach der Formel n ∗ fak (n-1) Hierbei ruft sich die Funktion fak rekursiv selbst auf. Die Muster (Pattern) von denen die Bestimmung des Ergebnisses abh¨angen sind 0 und n. Die Syntax von Haskell unterscheidet zwischen Groß - und Kleinschreibung. Bei Typen und Konstruktoren beginnen die Namen mit Großbuchstaben, bei Typvariablen, Funktionen und Parametern beginnen sie mit Kleinbuchstaben. F¨ ur Standardfunktionen d¨ urfen sowohl die alphanumerische Notation (Buchstaben) als auch die symbolische Notation (z.B. +, -, ∗, <, >) verwendet werden. Erlaubt ist ferner sowohl die Infix- als auch die Pr¨afix-Notation. F¨ ur Listenverarbeitung ist eine Notation vorgesehen, die sich an die mathematische Schreibweise f¨ ur Mengen orientiert. So erh¨ alt man eine Folge der geraden Zahlen durch [ x | x < - [1. .], even x] Ein Haskell Programm besteht aus einem oder mehreren Modulen. Ein Modul besteht aus Definitionen und Deklarationen. Definitionen werden im allgemeinen in Form von Gleichungen angegeben. Auf der linken Seite der Gleichung steht ein Name (Bezeichner, Identifikator) und auf der rechten Seite ein Ausdruck, der u ¨ber diesen Namen identifiziert wird. Jeder Ausdruck besitzt einen
210
3 Programmiersprachen
speziellen Typ. M¨ ochte ein Programmierer ein Datum einf¨ uhren, so kann dies z. B. durch datum : : (Int, String, Int) datum = (12, ”Oktober”, 1945) erfolgen. Funktionen werden ebenfalls durch Gleichung definiert. Der oder die formalen Parameter werden nach dem Namen der Funktion aufgef¨ uhrt. 1. succ :: Int -> Int succ n = n + 1 2. twice f a = f (f a) Hierbei bezeichnet f a die Anwendung der Funktion f auf das Argument a. Funktionen k¨onnen auch mit Hilfe mehrerer Gleichungen definiert werden. Eine Funktionsdefinition hat somit generell die syntaktische Form f < pat11 > · · · < pat1k > < match1 > ... < patn1 > · · · < patnk > < matchn > wobei < match >“ eine der beiden folgenden Formen hat: ” =< exp > where {< dec11 >; . . . ; < decln >} oder | < guard1 > = < exp1 > ... | < guardm > = < expm > where { < decl1 >; . . . ; < decln > } Mit “where“ werden lokale Definitionen eingef¨ uhrt. Dei W¨achter (“guards“ ) sind Boolesche Ausdr¨ ucke, die die Anwendung u ¨berwachen. Beispiel 3.3-55 Das bekannte Quicksortverfahren l¨ aßt sich in Haskell wie folgt programmieren: qsort :: Ord a => [a] − > [a] qsort [ ] =[ ] qsort [a] = [a] qsort (a:x) = partition [ ] [ ] x where partition l r [ ] = qsort l ++ a : qsort r partition l r (b:y) | b <= a partition (b:l) r y | otherwise partition l (b:r) y
3.3 Weitere Applikative Programmiersprachen
211
Bei diesem Beispiel f¨ allt auf, daß im Gegensatz zur Syntaxdefinition kein Semikolon auftritt. Dies liegt an einer Besonderheit von Haskell. Eine sogenannte Arbeitsregel“ erlaubt es Definitionen ohne syntaktischen Ballast“ zu notie” ” ren. Folgt dem where“ (bzw. let“ oder of “) keine offene Klammer {“, so tritt ” ” ” ” die Abseitsregel in Kraft: Die Einr¨ uckung der n¨achsten lexikalischen Einheit wird vermerkt. Mit den folgenden Zeilen wird wie folgt verfahren: • kleinere Einr¨ uckung: es wird eine geschlossene Klammer }“ eingef¨ ugt ” • gleiche Einr¨ uckung: es wird ein Semikolon “;“ eingef¨ ugt • gr¨ oßere Einr¨ uckung: es wird nichts eingef¨ ugt Muster dienen sowohl zur Unterscheidung verschiedener F¨alle als auch zur Benennung von Komponenten eines Wertes, z. B. head (a:x) = a Die Gleichung ist nur anwendbar, wenn der aktuelle Parameter die Form e1:e2“ hat (ggf. muß der Parameter reduziert werden). In diesem Fall wird ” a“ an den Listenkopf e1“ und x“ an den Listenrest e2“ gebunden. ” ” ” ” Muster werden von links nach rechts und Gleichungen von oben nach unten abgearbeitet. Somit spielt unter Umst¨ ande die Reihenfolge von Gleichungen eine Rolle. take (n+1) (a:x) = a : take n x =[] take Muster haben die folgende syntaktische Form (leicht vereinfacht). < pat > −− > < var > [ @ < pat > ] geschachteltes Muster | < con > < pat1 > . . . < patn > Konstruktoranwendung | [ − ] < integer > Konstante | < var > + < interger > Nachfolgermuster anonyme Variable | | (< pat1 >, . . . , < patn >) Tupelmuster | [< pat1 >, . . . , < patn >] Listenmuster Mit ’let’ und ’where’ werden lokale Definitionen eingef¨ uhrt. Nur Typdeklarationen (f :: t), Funktions- und Wertedefinitionen d¨ urfen lokal verwendet werden, keine Typdefinitionen (data’, type’). Die Unterschiede zwischen let und where sind: • let{< decls >} in e’ ist ein Ausdruck; @’=e where {< decls >}’ ist syntaktischer Bestandteil einer Gleichung. • Die in let {< decls >} in e’ definierten Bezeichner sind in e’ sichtbar.
212
3 Programmiersprachen
Beispiel 3.3-56 Die Sichtbarkeit der Definitionen in | g1 = e1 | ... | gm = gi where { < decls >} erstreckt sich auf die ”ei” und die ”gi”. 3.3.8.2 Das Typsystem Haskell besitzt ein statisches Typsystem, welches auf dem Hindley-MilnerTypsystem beruht. Die Angabe einer vollst¨ andigen Typsignatur ist nicht notwendig. Das System folgert den Typ jedes Wertes aus dem Kontext, in dem er verwendet wird. Eine Reihe von Basistypen sind vordefiniert. Diese vordefinierten Typen besitzen jedoch keine Sonderstellung, d. h. sie k¨onnen z. B. auch umdefiniert werden. Der Benutzer kann dar¨ uberhinaus eigene Typen einf¨ uhren. Hierzu stehen ihm die Typkonstruktoren () ( , , . . .) [] →
= trivialer Typ = Tupeltypen = Listentypen = Funktionstypen
zur Verf¨ ugung. Die Einf¨ uhrung von benutzerdefinierten Typen kann auf drei verschiedene Arten erfolgen: - type = Einf¨ uhrung eines Synonyms (Operatoren sichtbar) - newtype = Umbenennung eines Typs (Operationen nicht mehr sichtbar) - data = algebraischer Datentyp (Termalgebra) Typsignatur Funktionen werden anhand einer Typsignatur beschrieben. Diese Signaturen sind zwar nicht n¨ otig, erh¨ ohen aber das Verst¨ andnis beim Lesen des Quelltextes. Die Signatur beinhaltet diejenigen Typen, die in der zu beschreibenden Funktion als Parameter und als Ergebnis vorkommen. Die einzelnen Typen werden in der Signatur durch Pfeile getrennt. Der ¨außerst rechts stehende Typ gibt dabei den Ergebnistyp an, alle links von diesem die Typen der Parameter der Funktion. Die Typsignatur inc :: Int − > Int definiert eine Funktion mit dem Namen inc, welche als Parameter einen Wert vom Typ Int verlangt und als Ergebnis einen Wert von Typ Int zur¨ uckliefert.
3.3 Weitere Applikative Programmiersprachen
213
Die Pfeilnotation l¨ aßt sich als ein “. . . wird abgebildet auf . . .“ lesen, also in diesem Beispiel: Ein Int wird abgebildet auf ein Int“. Ein passender Aus” druck zu dieser Signatur ist folgender: inc x = x + 1 Die formalen Parameter einer Funktion werden in Haskell, jeweils durch ein Leerzeichen getrennt, dem Funktionsnamen nachgestellt. Dabei ist die Rei¨ henfolge einzuhalten, wie sie in der Typsignatur vorgegeben wurde. Ahnlich ließe sich die Zahl Pi definieren, allerdings ohne Parameter: pi :: Float pi = 3.14159 Wird der Funktionsname in Klammern gesetzt, so handelt es sich um einen Infix-Operator. Bei Infix-Operatoren m¨ ussen, wenn sie aufgerufen werden, die Parameter nicht hinter dem Funktionsnamen stehen, sondern der erste Parameter wird vor dem Funktionsnamen, der zweite hinter den Funktionsnahmen geschrieben. Hier ein Beispiel mit dem Plusoperator: (+) :: Int − > Int Aufgerufen wird diese Funktion nicht durch +56 sondern durch 5+6 Basistypen Haskell besitzt eine ganze Reihe von vordefinierten Datentypen mit entsprechend vordefinierten Operatoren (Funktionen). Zu den wichtigsten geh¨oren: Boolesche Ausdru ¨cke Die Wahrheitswerte sind vordefiniert. data Bool = False | True Fallunterscheidungen k¨ onnen vielf¨ altig programmiert werden: neben den bereits erw¨ ahnten W¨ achtern auch mit bedingten Ausdr¨ ucken. Ihre Syntax lautet if < expr > then < expr > else < expr > Zeichen und Zeichenketten Der Typ Char“ umfaßt die ASCII - Zeichen. Die Zeichen werden wie u ¨blich ” notiert: ”a” Zeichenketten sind Listen von Zeichen. Ihre Syntax lautet T ype String = [Char]
214
3 Programmiersprachen
Zeichenketten k¨ onnen abk¨ urzend dargestellt werden. So l¨aßt sich die Zeichenkette ’h’ : ’a’ : [ ] abk¨ urzend durch ”ha” darstellen. Beispiel 3.3-57 Die Zeichenkette [’A’, ’ ’, ’s’, ’t’, ’r’, ’i’, ’n’, ’g’] kann abgek¨ urzt dargestellt werden durch ”A string” Zahlen Haskell verf¨ ugt u ¨ber eine große Zahl numerischer Typen: -
’Int’ ’Integer’ ’Float’ ’Double’ ’Ratio a’ ’Complex a’
ganze Zahlen mit beschr¨ ankter Genauigkeit ganze Zahlen mit unbeschr¨ ankter Genauigkeit Fließkommazahlen (einfache Genauigkeit) Fließkommarzahlen (doppelte Genauigkeit) Rationale Zahlen Komplexe Zahlen
Tupel Tupel werden benutzt, um eine feste Anzahl von Werten unterschiedlichen Typs zusammenzufassen. Sie k¨ onnen von beliebiger L¨ange sein. Ihre Syntax lautet: data ( ) =() - - das leere Tupel data (a, b) = (a, b) - - Paare date (a, b, c) = (a, b, c) - - Tripel Tupel k¨ onnen auch u uhrt werden. Der ¨ber einen speziellen Konstruktor eingef¨ Konstruktor f¨ ur ein n-Tupel lautet ( , . . . , ) wobei in den runden Klammern n-1 Kommata auftreten. Listen In Anlehnung an LISP sind Listen die wichtigste Datenstruktur in Haskell. Sie werden benutzt, um eine beliebige Anzahl von Werten unterschiedlichen Typs zusammenzufassen. Ihre Syntax lautet data [a] = [ ] | a : [a]
3.3 Weitere Applikative Programmiersprachen
215
Um Liste zu konstruieren, gibt es vielf¨ altige M¨oglichkeiten. Arithmetische Folgen k¨ onnen z. B. wie folgt notiert werden: ’[1 ..]’
Liste aller positiven Zahlen
’[1 .. 99]’
dito bis einschließlich 99
’[1, 3 ..]’
Liste aller positiven, ungeraden Zahlen
’[1, 3 .. 99]’ dito bis einschließlich 99 ’[’a’ .. ’z’]’ Liste aller Buchstaben Um Funktionen auf Listen zu definieren kann die sog. Listenbeschreibung“ ” verwendet werden. Listenbeschreibungen haben die Form [ < exp > | < qual1 >, . . . , < qualn > ] mit
< qual > −− > < pat > < − < exp > Generator | < exp > W¨achter
F¨ ur den Generator ‘p <− e’ muß gelten: Wenn ‘p’ vom Typ ‘t’ ist, muß ‘e’ vom Typ ‘[t]’ sein. Generatoren f¨ uhren Variablen ein, die ’weiter rechts’ oder im Kopf verwendet werden k¨ onnen. W¨ achter, das sind Boolesche Ausdr¨ ucke, schr¨ anken die Belegungen von Variablen ein. Die Syntax von Listenbeschreibungen ist eng an die Notation von Mengen (z. B. { n | n < −N, n >= 7 }) angelehnt. Im Unterschied zu Mengen spielt die Reihenfolge eine Rolle und es k¨ onnen Duplikate auftreten. Beispiele 3.3-58 1. Mit Hilfe von Listenbeschreibungen l¨ aßt sich das Quicksort Verfahren folgendermaßen programmieren qsort [ ] = qsort (a : x) = ++ ++
[] qsort [ b | b < − x, b <= a ] [a] qsort [ b | b < −x, b > a ]
2. Die Funktion perms angewandt auf das Argument X berechnet die Liste aller Permutationen von X. perms perms[ ] perms x
:: [a] − > [ [a] ] =[[]] = [a : y | (a, y) < remove x, y < −perms y ]
remove :: [a] − > [(a, [a])] remove [ ] =[] remove (a:x) = (a, x) : [ (b, a : y) | (b, y) < −remove x ]
216
3 Programmiersprachen
3. Das nachfolgende Programm berechnet die Liste aller L¨osungen des nDamen-Problems: queens n = place n [ ] [ ] [1 .. n] place c d1 d2 rs | c == 0 = [[]] | otherwise = [ q:qs | (q, rs ) < −remove rs, (q-c) ’notElem’ d1, (q+c) ’notElem’ d2, qs¡-place (c-1) ((q-c):d1) ((q+c):d2) rs’] Aufz¨ ahlungstypen Aufz¨ ahlungstypen geben die Werte an, die ein Name annehmen kann. Sie lassen sich besonders gut f¨ ur Fallunterschiedungen verwenden. Beispiel 3.3-59 data Farbe = rot | blau | gr¨ un | gelb next Farbe X = rot − > blau blau − > gr¨ un gr¨ un − > gelb gelb − > undef iniert. Typklassen Typklassen erlauben es, den gleichen Namen f¨ ur syntaktisch verschiedene, aber semantisch ¨ ahnliche Funktionen zu verwenden. Ein typisches Beispiel ¨ f¨ ur eine derartige u ¨berladene Funktion ist der Aquivalenztest: class Eq a where (==) :: a − > a − > Bool Diese Klasse tr¨ agt den Namen Eq und soll all diejenigen Typen zusammenfassen, f¨ ur die der == - Operator definiert ist. Damit ist erstmal die Klasse Eq bekannt gemacht worden. Allerdings hat diese noch keine Mitglieder. Um nun ein Datentyp Mitglied der Klasse werden zu lassen, muß eine Instanz von dieser Klasse u unschten Datentyp gebildet werden. F¨ ur einen ¨ber den gew¨ einfachen Datentyp wie Int k¨ onnte dies so aussehen: instance Eq Int where x == y = intEq x y Die Funktion intEq ist dabei die grundlegene Funktion zum Vergleichen von Int-Werte. Auch f¨ ur den bekannten Datentyp Tree l¨aßt sich eine solche Instanz bilden. data Tree a = Leaf a | Branch Tree a Tree a instance Eq a => Eq (Tree a) where
3.3 Weitere Applikative Programmiersprachen
Leaf a Branch 11 r1
217
== Leaf b = a == b == Branch 12 r2 = 11 == l2 r1 == r2 == = False
Der == -Operator wurde hier mehrfach implementiert. Je nachdem welches Muster paßt, also ob Leaf mit Leaf oder Branch mit Branch verglichen werden soll, wird der hierzu entsprechende Ausdruck ausgewertet. == ist ein Muster, welches auf alle Gleichheitsoperationen paßt. Da auch der Datentyp ausgewertet wird, mit dem die Baumstruktur aufgebaut ist und dieser hier ja polymorph definiert ist, muß auch angegeben werden, daß seine m¨ogliche sp¨atere Auspr¨ agung eine Instanz der Klasse Eq besitzten muß. Dies wird mit dem Ausdruck Eq a => Eq (T ree a) sichergestellt. Wird der Gleichheitsoperator nun in einer Funktion benutzt, so geschieht folgendes: Das Typsystem von Haskell u uft, mit welchem Datentyp die Ver¨berpr¨ gleichsoperation arbeiten soll. Dann wird die entsprechende Instanz zu diesem Datentyp herausgesucht und die Operation aus der Funktion mit der aus der Instanz u ¨berladen. ¨ Das Uberladen von Funktionen ist eine Eigenschaft der Ad-hoc Polymorphie, welche eigentlich vom Hindley-Milner Typsystem ausgeschlossen ist. In Haskell wurde das Hindley-Milner Typsystem allerdings so erweitert, dass auch eine Ad-hoc-Polymorphie mit Hilfe der Typklasse m¨oglich ist. Einer class-Definition lassen sich noch default functions“ mitgeben. Diese ” Funktionen werden verwendet, falls in der Instanz dieser Operator nicht definiert ist. F¨ ur die Klasse Eq l¨ aßt sich neben dem == -Operator noch der /= -Operator implementieren, mit dem zwei Werte auf Ungleichheit“ u uft ¨berpr¨ ” werden. Der /= -Operator l¨ aßt sich mit Hilfe des == -Operators zusammenbauen. class Eq a where (==) , (/ =) :: a − > a − > Bool x/=y = not (x==y) x == y = not (x/=y) Die obigen Eq-Instanzen von Int und Tree sind mit dieser Definition immer noch g¨ ultig, besitzen jetzt allerdings auch den /= -Operator. Dieser ist aufgrund der default function“ x /= y = not (x==y) definiert. ” Vererbung Klassen lassen sich vererben, um so Operationen, die schon in anderen Klassen definiert wurden, zu u ¨bernehmen und um neue zu erweitern. So kann die oben definierte Eq-Klasse noch um Operationen, wie <, >, <= oder >= erweitert werden. Um die <= und >=-Operatoren zu implementieren, ist es ufen lassen. Das auch n¨ otig zu wissen, wie sich Werte auf Gleichheit u ¨berpr¨ ist genau das, was die Klasse Eq schon kann. Also wird eine neue Klasse Ord
218
3 Programmiersprachen
geschaffen, welche von Eq erbt. class Eq a => Ord a where (<), (<=), (>=), (>) :: a − > a − > Bool max, min :: a − > a − > a Eine Instanz dieser Klasse kann so aussehen: instance Ord a => Ord (Tree a) where Leaf < Branch = True Leaf a < Leaf b = a < b < Leaf = False Branch Branch 11 r1 < Branch 12 r2 = 11 < 12 && r1 < r2 t1 <= t2 t1 < t2 | | t1 == t2 ... Die > und >=-Operatoren lassen sich ¨ ahnlich definieren. Ableitungen Das Notieren der Instanzen f¨ ur viele Datentypen ist sehr m¨ uhselig, und Haskell bietet eine M¨ oglichkeit, sich hier Arbeit zu sparen. Wird ein neuer Datentyp erstellt, so l¨ aßt sich dieser von einer Klasse oder mehreren Klassen ableiten. Die Instanz zu diesem neuen Datentyp wird dabei automatisch gebildet. Die Klassen, von denen der Datentyp ableiten soll, werden in der data-Deklaration hinter dem Wort deriving angegeben. Der Typ Tree“ ließe sich mit einer ” solchen Ableitung so deklarieren: date Tree a = Leaf a | Branch Tree a Tree a deriving Ord Die Instanz, die oben noch von Hand notiert wurde, wird hier nun automatisch erzeugt. Oft ist bei Datentypen auch eine Ableitung zu der Klasse Show zu sehen. Damit l¨ aßt sich eine Variable jenes Typs auf dem Bildschirm ausgeben. M¨ochte man einen Baum auf dem Bildschirm ausgeben, so leitet sich dieser zus¨ atzlich von Show ab. data Tree a = Leaf a | Branch Tree a Tree a deriving (Ord,Show) Typu ¨ berpru ¨ fung ¨ Die Uberpr¨ ufung eines Ausdrucks auf Typkorrektheit auch ohne direkte Angabe der Signatur erfolgt mittels Typinferenz. ¨ Das Uberpr¨ ufen von monomorphen Typen ist sehr einfach. Als Beispiel sei dieser Ausdruck gegeben: ord ’c’ + 3
3.3 Weitere Applikative Programmiersprachen
219
Die Signaturen der ord-Funktion und des +-Operators lauten: ord :: Char − > Int (+) :: Int − > Int − > Int Bei der Typ¨ uberpr¨ ufung des obigen Ausdrucks wird zuerst bei der ordFunktion gepr¨ uft, ob, wie in der Signatur gefordert, auch ein Wert vom Typ Char als Parameter steht. Dem ist so. Als n¨ achstes wird bei dem +-Operator u uft, ob als Parameter Werte vom Typ Int stehen. Die Char-Funktion ¨berpr¨ liefert einen Int zur¨ uck. Der erste Parameter stimmt also. Der zweite Parameter ist ein Wert vom Typ Int und stimmt auch. Somit ist dieser Ausdruck typkorrekt. Der folgende Ausdruck ist nicht typkorrekt, da es sich bei dem zweiten Parameter f¨ ur den +-Operator um einen Wert vom Typ Bool handelt, passt also nicht zu der Signatur des +-Operators. ord ’c’ + False Die Typ¨ uberpr¨ ufung einer Funktion kann abstrakt so dargestellt werden: f :: t1 − > t2 − > . . . − > tk − > t f p1 p2 . . . pk | g1 = e1 | g2 = e2 ... | gn = en Die senkrechten Striche sind dabei Guards. Damit der Typ der Funktion korrekt ist, m¨ ussen 3 Bedingungen stimmen 1. Jeder Guard“ gi muss vom Typ Bool sein ” 2. Jeder Ausdruck ei muss vom Typ t sein 3. Jedes pj muss zum Typ tj passen. Bei polymorphen Typen ist es ein wenig komplizierter, da es mehrere verschiedenen M¨ oglichkeiten gibt, wie sich eine Signatur zusammensetzt. Um dieses Problem zu l¨ osen, ist ein Algorithmus n¨otig, welcher eine bestimmte gew¨ unschte Signatur ausfindig macht. Sinnvoll ist dabei, die Signatur zu finden, welche die allgemeinste Form besitzt. Im folgenden soll an einem einfachen Beispiel erkl¨ art werden, wie Haskell eine Typsignatur bestimmt. Gegeben ist folgende Funktion: f (x,y) = (x,[’a’ ..y]) Die Signatur l¨ aßt sich leicht ablesen. Der erste Parameter der Funktion ist ein Tupel mit 2 Variablen. Die erste Variable x des Tupels kann von irgendeinem Typ sein. Es muß also als ein polymorpher Datentyp gesetzt werden. Bei der 2. Variable im Tupel muß es sich, aufgrund von [’a’ .. y], um eine Char-Typ handeln. Das Ergebnis der Funktion ist wiederum ein Tupel, bei dem auch hier die erste Variable ein polymorpher Datentyp sein muß und die 2. Variable eine Liste von Char. Die Signatur sieht also folgendermaßen aus:
220
3 Programmiersprachen
f :: (a, Char) − > (a, [Char]) Nun eine zweite Funktion: g (m,zs) = m + length zs Auch hier ist nur ein Parameter vorhanden. Es ist ein Tupel aus 2 Variablen. Aufgrund des +- Operators muß die Variable m ein numerischer Wert sein. Da der Wert von m aber zusammen mit dem Ergebnis der length-Funktion addiert wird und die length-Funktion als Ergebnis einen Wert vom Typ Int zur¨ uckliefert, hat m auch Int zu sein. Die zweite Variable zs des Tupels muß aufgrund der Verwendung in der length-Funktion eine Liste mit einem polmymorphen Datentyp sein. Der Ergebnistyp von der Funktion wird durch den +-Operator bestimmt, muß also auch Int sein. Die Signatur sieht so aus: g :: (Int, [b]) − > Int Zum Schluß noch eine dritte Funktion, die sich aus den Funktionen f und g zusammensetzt: h=g. f Die Komposition g.f bedeutet, daß das Ergebnis der Funktion f als Parameter an die Funktion g u ¨bergeben wird. Die Signatur von h besteht aus einem Parameter und dem Funktionsergebnis. Um nun das Aussehen des Parameters bestimmen zu k¨ onnen, m¨ ussen die schon gefundenen Signaturen von f und g zusammengef¨ ugt werden. Genauer gesagt muß ein Typ gefunden werden, welcher die Eigenschaften von g und von f ber¨ ucksichtigt. Da das Ergebnis von f als Parameter f¨ ur g dient, muß als erstes u uft werden, wie sich der ¨berpr¨ ¨ Ergebnistyp von f und der Typ des Parameters von g in Ubereinstimmung bringen l¨ aßt. Es soll ein gemeinsamer Typ f¨ ur (a, [Char]) und (Int, [b]) gefungen werden. f :: (a,Char) − > (a, [Char]) g :: (Int, [b]) − > Int Beide Typen, also (a, [Char]) und (Int, [b]), enthalten polymorphe Datentypen und lassen sich somit jeweils als Menge ihrer m¨oglichen Auspr¨agungen darstellen. Wird u ¨ber diese Mengen nun eine Schnittmenge gebildet, so ergeben sich daraus alle erlaubten Kandidaten f¨ ur den gesuchten gemeinsamen Typ. Aus diesen Kandidaten wird nun derjenige herausgesucht, der die allgemeinste Form besitzt. In diesem Beispiel ergibt die Schnittmenge nur einen m¨oglichen Typ und dieser (Int, [Char]) stellt auch das Ergebnis dar. Allerdings muß es sich nicht immer um monomorphe Typen als Ergebnis handeln, sondern auch polymorphe sind durchaus m¨ oglich. Dieser Prozess zum Finden eines gemeinsamen Typs nennt sich Unifikation. Nun ist bisher aber nur ein gemeinsamer Typ f¨ ur f und g bestimmt worden. Aus diesem l¨ aßt sich aber nun der Parametertyp f¨ ur h einfach bestimmen. Da der Parameter von f vom Typ (a, Char) ist und der Ergebnistyp von f schon
3.3 Weitere Applikative Programmiersprachen
221
auf (Int, [Char]) festgelegt wurde, folgt daraus, daß der Parameter von h den Typ (Int, Char) hat. Der Ergebnistyp von h muß derselbe wie von g sein, also Int. h :: (Int, Char) − > Int Die Typfeststellung und -¨ uberpr¨ ufung f¨ ur h ist somit abgeschlossen. 3.3.8.3 Beispielprogramme In diesem Abschnitt werden einige Haskell-Programme vorgestellt. Beispiel 3.3-60 Das aus der Numerik bekannte Newton-Verfahren l¨aßt sich in Haskell folgendermaßen darstellen: approxs :: Float − >Float − > [Float] approxs r x = x : approxs r ((x + r/x)/2) within :: Float − > [Float] − > Float within eps (x1:(x2:1))= if abs (x1-x2) < eps then x2 else within eps (x2:1) sqrt :: Float − > Float − > Float − >Float sqrt x0 eps r = within eps (approxs r x0) Beispiel 3.3-61 Die nachfolgende Funktion build“ u uhrt eine Liste in einen vollst¨andig ¨berf¨ ” balancierten Bin¨ arbaum. build build x
:: [a] − > BinTree a = fst (buildn (length x) x)
buildn :: Int − > [a] − > (BinTree a, [a]) buildn 0 x :: Int − > [a] − > (BinTree a, [a]) buildn (n+1) x = (Node 1 a r, z) where m = n ’div’ 2 (l, a:y) : buildn m x (r, z) = buildn (n - m) y Beispiel 3.3-62 orige OperaIn diesem Beispiel werden Bin¨ arb¨ aume als Datentyp und zugeh¨ tionen eingef¨ uhrt. Als erstes muß mittels einer Datentypdefinition ein neuer Typ eingef¨ uhrt werden. data IntTree = Empty | Node IntTree Int IntTree
222
3 Programmiersprachen
Hierbei ist der Bezeichner IntTree“ der Typname. Als Konstruktoren treten ” Empty“ und Node“ auf. Mit ihrer Hilfe werden Elemente des Datentyps ” ” konstruiert. Zu beachten ist, daß Bezeichner vom Typen und Konstruktoren mit Großbuchstaben beginnen. Konstruktoren werden wie normalen Funk” tionen“ verwendet. Zus¨ atzlich - und das unterscheidet sie von Funktionen dienen sie zur Programmierung von Fallunterscheidungen. Dies sieht man an der Funktion insert, die einen Baum erweitert. insert :: Int − > IntT ree − > IntT ree insert a Empty = Node Empty a Empty insert a (Node 1 b r) = if a <= b then Node (insert a l) b r else Node l b (insert a r) Die kontr¨ are L¨ oschoperation l¨ aßt sich z.B. durch delete :: Int − > IntT ree − > IntT ree delete a Empty = Empty delete a (Node l b r) |a
:: IntT ree − > IntT ree − > IntT ree =r = let (b, t) = split ll a lr in Node t b r
split
:: IntT ree − > Int − > IntT ree − > (Int, IntT ree) = (a, l)
split l a Empty split l a (Node rl b rr)
= let (c, t) = split rl b rr in (c, Node l a t) 3.3.9 ML 3.3.9.1 Entwicklung Die Sprache ML wurde 1973 von Robin Milner an der Edinborough Universit¨at entwickelt. Die UrSprache“ entstand im Rahmen von Arbeiten u ¨ber auto” matische Korrektheitsbeweise f¨ ur Programme und war Teil eines TheoremBeweis-Programms mit Namen LCF (Logic of Computable Functions). Seither hat sich ML zu einer vollst¨ andigen, eigenst¨andigen und weitverbreiten Programmiersprache entwickelt.
3.3 Weitere Applikative Programmiersprachen
223
Der Name ML steht f¨ ur MetaLanguage. Wie der Name besagt, handelt es sich um die Beschreibung eines Sprachkonzeptes mit statischer Typisierung, Polymorphie, automatischer Speicherverwaltung (Garbage Collection) und im allgemeinen - strikter Semantik. Allerdings sind die Konzepte der funktionalen und applikativen Programmierung nicht rein“ umgesetzt worden, da ” auch imperative Konstrukte und Wirkungen (Seiteneffekte) vorgesehen sind. Neben Dutzenden von Varianten sind die bekanntesten Vertreten von ML Standard ML“ (SML), Lazy ML“ und Caml“. Caml steht f¨ ur Catego” ” ” ” rical Abstract Machine + ML“ und wurde am INRIA von G´erard Huet in den Jahren 1984-85 entwickelt und 1990 unter Xavier Leroy zu Objective ” Caml“ (OCaml) erweitert. OCaml vereinigt funktionale, imperative und objektorientierte Spachkonzepte. Lazy ML ist ein Dialekt von ML, der mit dem Grundsatz der strikten Semantik bricht. Standard ML hingegen war Robert Milners Versuch 1984 die Sprachdialekte von ML zu vereinigen. Neben dem ML-Kern sind auch Ideen, wie die Funktionsdeklaration durch Muster der Programmierspache Hope“, in SML eingeflossen. Es gibt eine 1997 u ¨berar” beitete Version von SML, die in der Literatur meist Standard ML’97“ ge” nannt wird, um sie von der Ursprungsversion zu unterscheiden. SML selbst hat auch eine Reihe von Varianten und verschiedenste Implementationen und Erweiterungen (SML/NJ, MoscowML, ML Works, ML Kit . . . ), die hier nicht n¨ aher betrachtet werden. Die wichtigsten Eigenschaften von ML sind: -
¨ interaktive Programmentwicklung durch inkrementellen Ubersetzer strenge und statische Typisierung strikte Auswertung von Ausdr¨ ucken (mit geringen Einschr¨ankungen), Parameter¨ ubergabe: call-by-value leistungsf¨ ahiges Typinferenzsystem (daher k¨onnen fast alle Typangaben unterbleiben) Polymorphie bei Typen und Funktionen u ¨bersichtlicher Zugriff auf Datenstrukturen mit Hilfe von Pattern-matching anstelle von oder zus¨ atzlich zu Selektoren benutzergesteuerte Behandlung von Laufzeitfehlern (exception handling) umfangreiches Modulkonzept.
3.3.9.2 Sprachelemente Elementare Datentypen ML unterscheidet zwischen alphabetischen Bezeichnern und symbolischen Bezeichnern. Alphabetische Bezeichner beginnen mit einem Buchstaben gefolgt von einer beliebigen (!) Zahl von Buchstaben, Ziffern, Unterstrichen und Hochkommata. Groß - und Kleinbuchstaben werden unterschieden. Symbolische Bezeichner bestehen aus einer beliebigen Folge von Zeichen aus dem Zeichenvorrat
224
3 Programmiersprachen
!%&$#+-*/:<=>?@\ ∼’ ˆ| Einige symbolische Zeichenfolgen besitzen eine besondere Bedeutung und d¨ urfen nicht als Bezeichner verwendet werden. Dies sind: : |= => − > # Als elementare Standarddatentypen sind vorhanden: Name unit bool int
Beispiele f¨ ur Konstanten 0 true, false 0, 12, ∼ 23, 003
real
0.01, 3.1415, 1.6E3, 7E∼5 ”O.K.”, ”Apfel”, ”ich sagte:\” Nein!\” ”
string
Operationen keine not, =, <> +, -, *, div, mod, abs, =, <>, <, >, >=, <= +,-,*,/,sin, cos, tan, ln, exp, sqrt,=, <>, <, >, >=, <= size (L¨ ange des strings), ˆ (Konkatenation), chr, ord
Das einzige Element des Datentyps unit ist ( ). ∼ ist das Vorzeichen Minus. Ein Vorzeichen Plus existiert nicht. \ in Strings symbolisiert den Beginn einer escape-Sequenz. Oben zeigt z. B. \“ an, daß das Zeichen “ hier als Teil des Strings und nicht als Begrenzer zu betrachten ist. Bei einigen ML-Versionen existiert noch der Basistyp char“. Aus den Ba” sistypen k¨ onnen komplexere Datenstrukturen wie Listen, Tupel, Funktionen oder Records bzw. selbstdefinierte Typen konstruiert werden. Wegen der strengen Typisierung von ML sind die Typen int und real disjunkt. Dies bedeutet, ein int-Objekt kann nicht mit einem real-Objekt arithmetisch verkn¨ upft werden. Hier sind vorher Typanpassungen vom Benutzer vorzunehmen. Sie erfolgen nicht, wie in anderen Sprachen, automatisch. Die zugeh¨ origen Anpassungsfunktionen lauten real: int − > real bzw. floor: real − > int. floor(x) liefert die gr¨ oßte ganze Zahl ≤ x. Ferner ist zu beachten, daß es im Typ bool keine and- und keine orFunktion gibt. Stattdessen stellt ML zwei Infix-Funktionen zur Verf¨ ugung: andalso: bool×bool→bool mit f alse, falls x = false x andalso y = y, sonst. orelse: bool×bool →bool mit true, falls x = true x orelse y = y, sonst.
3.3 Weitere Applikative Programmiersprachen
225
Das zweite Argument wird nur ausgewertet, wenn es f¨ ur den Wert des Ausdrucks noch relevant ist. Beide Funktionen sind also nicht strikt, da z. B. (true andalso y) auch einen Wert liefert, wenn y nicht definiert ist. In Tupel werden Elemente unterschiedlichen Typs, in Listen Elemente des gleichen Typs zusammengefaßt. Beispiele 3.3-63 1. Das Tupel (12, 10, 1945, ”Wolfram” ) ist vom Typ int * int * int * string 2. Die Liste [”Wolfram” , ”Lippe” ] ist vom Typ string list 3. Die Liste [(2, 3), (2, 2), (9, 1)] ist vom Typ (int * int) list Wie aus Beispiel 3.3.-63 ersichtlich ist, werden Tupel durch runde Klammern und Listen durch eckige Klammern characterisiert. Die leere Liste wird mit nil bzw. [ ] bezeichnet. Zur linksseitigen Verkn¨ upfung eines einzigen Wertes mit einer Liste wird das Zeichen ”::” verwendet. Beispiel 3.3-64 nil 1::nil 2::(1::nil) 3::(2::(1::nil))
[] [1] [2,1] [3, 2, 1]
Die Verkn¨ upfung zweier Listen erfolgt mit dem Operator @. Bei der Konstruktion von Listen sind die Randbedingungen zu beachten. So kann z.B. die Liste [1, 2, 3, 4] aus 1::[2, 3, 4] oder [1] @ [2, 3, 4] entstehen. Dagegen verursachen 1 @ [2, 3, 4] und [1]::[2, 3, 4] eine Typverletzung. Dies gilt auch f¨ ur [1, 2, 3]::4. Ausdru ¨ cke Ein Ausdruck besteht entweder aus einem einfachen Ausdruck, der u ¨ber den elementaren Datentypen mittels der vordefinierten oder selbstdefinierten
226
3 Programmiersprachen
Funktionen in der u ¨blichen Weise gebildet worden ist, aus einem bedingten Ausdruck oder aus einem Funktionsausdruck. Bedingte Ausdr¨ ucke besitzen die allgemeine Form if < Boolescher-Ausdruck > then < Ausdruck> else Die bedingeten Ausdr¨ ucke k¨ onnen hierbei auch geschachtelt werden d. h. if n < 0 then 0 else if n = 1 then 1 else −1 ist auch zul¨ assig. Analog zu andalso und orelse ist auch die Funktion if-then-else nicht strikt, denn es wird entweder der then-Zweig oder der else-Zweig ausgewertet, aber nie beide. So liefert der bedingte Ausdruck also auch einen Wert, wenn der nicht benutzte Zweig undefiniert ist. Alle u ucke werden in ML ¨brigen Ausdr¨ strikt ausgewertet. In ML werden Funktionen ebenso wie Werte der elementaren Typen als Werte eines Funktionstyps aufgefaßt. Man kann daher auch Ausdr¨ ucke bilden, die Funktionen als Argumente besitzen und Funktionen als Werte liefern. Funktionen definiert man mit dem Schl¨ usselwort fun. Die allgemeine syntaktische Form lautet fun < Funktionsname> < Parameter> = Beispiel 3.3-65 Ist die mathematische“ Definition der Funktion max gegeben durch ” x falls x > y max: → .max(x, y) = y sonst so l¨ aßt sich die in ML darstellen durch fun max (x:real, y:real) = if x>y then x else y; val max = fn : real − > real Wie man sieht ist die ML-Funktion zu der mathematischen Definition identisch. Da der Operator > f¨ ur mehrere Typen definiert ist und ML in diesem Fall standardm¨ aßig int nehmen w¨ urde, muß an dieser Stelle explizit typisiert werden. Bei einigen ML-Varianten besteht auch die M¨ oglichkeit mit Hilfe des Schl¨ usselwortes fn namenlose Funktionen einzuf¨ uhren. Die Syntax lautet fn < Parameter > => < Ausdruck> Beispiele 3.3-66 1. fn x => 2 x 2. fn x => x > 0 else -1
3.3 Weitere Applikative Programmiersprachen
227
3. fn f => (fn g (fn x => f (g(x)))) Hier handelt es sich um ein Beispiel f¨ ur ein h¨oheres Funktional. Die Funktionsapplikation (fn f => (fn g => (fn x => f(g (x))))) sin ws wird ausgewertet zu fn x => sin (ws (x)) : real − > real Funktionen, die nur eine lokale G¨ ultigkeit besitzen sollen, z. B. Hilfsfunktionen in Funktionsdeklarationen, die aus Effizienzgr¨ unden eingef¨ uhrt werden, k¨ onnen mit Hilfe des Schl¨ usselwortes lokal eingef¨ uhrt werden. Die Syntax lautet lokal < Funktionsdefinition> in < Funktionsdefinition > end Muster Muster (Pattern Matching) stellen ein leistungsf¨ahiges Konstrukt bei der Funktionsdefinition dar. Je nachdem auf welches Muster die Eingabe passt, wird der entsprechende Ausdruck der Funktion ausgewertet. Man kann sich Muster wie eine Kette von Fallunterscheidungen vorstellen. Viele der bisherigen Beispiele bauen auf Muster-Definition auf. Drei Punkte sind bei der Musterdefinition wichtig: 1. Die Argumente m¨ ussen in allen F¨ allen typkompatibel sein. 2. Die Muster sollten alle Argumentwerte abdecken. Andernfalls gibt der Compiler eine match nonexaustive“-Warnung aus, und das Programm ” k¨ onnte bei bestimmten Eingaben abst¨ urzen. 3. Die einzelnen F¨ alle sollten sich gegenseitig ausschließen. Wenn das nicht m¨ oglich ist, gilt: Zuerst die Sonderf¨ alle notieren, dann den allgemeinen Fall. Die Muster werden in der Reihenfolge abgearbeitet, in der sie deklariert wurden und der erste Treffer z¨ ahlt. Die Fallunterscheidungen bei den Mustern erfolgt durch das Schl¨ usselzeichen |“. ” Beispiel 3.3-67 Die Fakult¨ atsfunktion fak kann unter Verwendung einer Hilfsfunktion folgendermaßen programmiert werden: local fun fak helper (0, x)= x | fak helper (k, x) = fak helper (k-1, k*x) in fun fak(k) = fak helper(k,1) end;
228
3 Programmiersprachen
Wertdeklarationen In ML k¨ onnen Werte eines beliebigen ML-Typs deklariert werden. Werte k¨onnen also nicht nur Elemente der elementaren Typen sein, sondern auch Listen, Tupel, B¨aume usw. sowie Funktionen und Funktionale. Wertdeklarationen besitzten die allgemeine Form val = < Ausdruck>. Hierdurch wird der Wert des Ausdrucks an den Bezeichner gebunden. Benutzerdefinierte Datentypen Benutzerdefinierte Datentypen k¨ onnen auch u usselwort datatype ¨ber das Schl¨ eingef¨ uhrt werden. Seine Anwendung zeigen die folgenden Beispiele Beispiele 3.3-68 1. Das bekannte Kinderspiel Schere-Stein-Papier“ l¨aßt sich darstellen durch ” datatype ssa = Schere | Stein | Papier; fun schlaegt (Schere, Papier) = true | schlaegt (Stein, Schere) = true | schlaegt (Papier, Stein) = true | schlaegt (-,-) = false; 2. Ein benutzerdefinierter Datentyp Bin¨ arbaum“ l¨aßt sich einf¨ uhren und ” verwenden durch datatype ’a btree = Empty | Node of ’a btree * ’a * ’a btree; val bt = Node (Node( Empty, 2, Empty), 3, Node (Empty, 4, Empty))); Es wird ein Bin¨ arbaum als Datentyp btree deklariert, der entweder leer ist oder ein linker Teilbaum gefolgt von dem Knoten vom Typ ’a gefolgt vom rechten Teilbaum ist. Der Beispiel-Bin¨ arbaum bt sieht folgendermaßen aus: 3
2 4 3. Eine Zahlung kann entweder in bar oder durch einen Scheck erfolgen. Dies kann modeliert werden durch datatype Zahlung = cash of real | cheque of string * real; Hierbei repr¨asentiert real ein Betrag in EURO und cheque den Namen einer Bank, z. B. val Buskarte = cash 5.50 val Auto = cheque (”Deutsche-Bank” , 20 000.00)
3.3 Weitere Applikative Programmiersprachen
229
3.3.10 Hope Die Sprache Hope wurde 1980 von Rod Burstall und David McQueen in Edinburgh entwickelt. In Hope wurden zum ersten Mal benutzerdefinierte Datenstrukturen und Pattern Matching erlaubt. Namen sind in Anlehnung von Pascal Zeichenreihen bestehend aus Großund Kleinbuchstaben sowie Ziffern, die jedoch stets mit einem Buchstaben beginnen m¨ ussen. Funktionsdefinitionen beginnen mit dem Schl¨ usselwort dec und verlangen die Angabe des Typs f¨ ur die Argumente und das Ergebnis, z.B. hat die Typdefinition f¨ ur eine Funktion max, die von zwei Werten den gr¨oßeren als Ergebnis liefert, die Form dec max : num # num − > num ; Hier ist max der Funktionsname. Nach dem Doppelpunkt erfolgt die Typdeklaration, wobei bei mehrdimensionaler Ein- bzw. Ausgabe die einzelnen Typen mittels # getrennt werden. Abgeschlossen werden Definitionen durch ein Semikolon. Das funktionale Verhalten von max beschreibt die Definition - - - max (x,y) <= if x > y then x else y; Das Sonderzeichen - - - wird als Wert von“ und das Sonderzeichen <= wird ” als ist definiert“ gelesen. Der G¨ ultigkeitsbereich der Parameter x und y ” beschr¨ ankt sich auf die Funktion, d. h. er endet bei dem Semikolon. Der Start eines Hopeprogramms erfolgt durch einen einzigen Ausdruck, der einen oder mehrere Funktionsaufrufe enthalten kann. So kann z. B. ein Programm durch max (10, 20) + max (1 , max (2,3)) gestartet werden. Als Ergebnis w¨ urde man 23 : num erhalten. In Funktionsdefinitonen k¨ onnen auch die Namen bereits definierter Funktionen benutzt werden. So liefert z.B. die Funktion max3 den gr¨oßten Wert von drei gegebenen Werten: dec max3 : num # num # num − > num; - - - max3 (x, y, z) <= max (x, max (y, z)); Hope besitzt keine expliziten Schleifen. Sie m¨ ussen daher durch entsprechende Rekursionen ersetzt werden. So kann z. B. eine Funktion mult, die zwei Zahlen mittels wiederholter Addition multipliziert, dargestellt werden durch dec mult : num # num − > num; - - - mult (x, y) <= if y=0 then 0 else mult (x, y-1) + x;
230
3 Programmiersprachen
Funktionen k¨ onnen auch in Infix-Notation verwendet werden. Dies gilt in Hope f¨ ur die meisten Standardoperatoren. Soll eine benutzerdefinierte Funktion in Infix-Notation verwendet werden, so muß dies in der Definition besonders gekennzeichnet werden. Dies geschieht mittels des Schl¨ usselwortes infix. In dem Beispiel kann dies z. B. mittels infix mult : 8; dec mult : num # num − > num; - - - x mult y <= if y=0 then 0 else x mult (y-1) + x; erfolgen. Die Zahl 8 beschreibt die Priorit¨ at der Funktion. Neben dem Basistyp num existieren noch die Basistypen truval (mit den Werten true und false) sowie char. Daneben existieren stadardm¨aßig die h¨ oheren Datenstrukturen Tupel und Liste. In Tupeln k¨ onnen Objekte gleicher oder unterschiedlicher Typen zusammengefaßt werden. So sind z. B. (2,3) und (’ a ’, true) Tupeln der Typen num # num bzw. char # truval. Im folgenden Hope-Programm wird ein Tupel benutzt, um eine Funktion zu definieren, die mehrere Ausgaben erzeugt: dec zeit24 : num − > num # num # num; - - - zeit24 (s) <= (s div 3600), s mod 3600 div 60, s mod 3600 mod 60 ); Hierbei ist div die gew¨ ohnliche Division von integer-Zahlen und mod liefert den Rest der Division. Wird das Programm mit zeit24 (45765); gestartet, so liefert es als Ergebnis (12, 42, 36) : (num # num # num) In Listen k¨ onnen nur Objekten identischen Typs zusammengefaßt werden. Listen werden in eckigen Klammern eingeschlossen. Ein Beispiel f¨ ur eine Liste mit drei Objekten vom Typ num ist [1, 2, 3]. Zur Bildung von Listen stehen zwei Standard-Operatoren zur Verf¨ ugung. Anstelle des cons-Operatores in LISP wird das Sonderzeichen ”::” verwendet. So erzeugt 10 :: [20, 40, 80]
3.3 Weitere Applikative Programmiersprachen
231
die Liste [10, 20, 40, 80] Durch nil wird die leere Liste erzeugt. Jede beliebige Liste kann mit Hilfe von :: und nil a ¨quivalent dargestellt werden. Beispiel 3.3-69 1. Der Ausdruck [ a + 1, b - 3 , c * d ] entspricht dem l¨ angeren Ausdruck a+1 :: (b-3 :: (c * d :: nil)) 2. Die folgenden drei Ausdr¨ ucke sind ¨ aquivalent ”uni” [’u’, ’n’, ’i’] ’u’, :: (’n’ :: (’i’ :: nil)) In Hope wurde erstmalig das Konzept des pattern matching“ eingef¨ uhrt. ” M¨ochte man z. B. eine Funktion definieren, die die Summe ihrer Elemente bildet, so hat die Typdeklaration die Form dec sumlist : list (num) − > num; Da man zur Realisierung der Funktionalit¨ at auf die einzelnen Komponenten der als aktuellen Parameter u ¨bergebenen Liste zugreifen muß, l¨aßt sich dies in Hope definieren durch - - - sumlist (x :: y) <= x + sumlist (y); Erfolgt ein Aufruf von sumlist durch sumlist ([ 1, 2, 3]) so erh¨ alt x den aktuellen Wert 1 und y den aktuellen Wert [2,3]. Hierbei ist zu beachten, daß der Aufruf sumlist (nil) wegen der strengen Typkonformit¨ at zu einem Fehler f¨ uhrt. Diese Situation muß durch eine zweite Definition - - - sumlist (nil) <= 0; abgefangen werden. Dicke Ausdr¨ ucke k¨ onnen durch das Schl¨ usselwort let abgek¨ urzt werden. Tritt mehrmals der dicke Ausdruck x∗y auf, z. B. in (x+y)∗(x+y), so kann dies abgek¨ urzt dargestellt werden durch z let z == x+y in z ∗ z;
232
3 Programmiersprachen
In der Wirkung identisch ist das Schl¨ usselwort where. Der obige Effekt erzeugt where in dem Ausdruck z ∗ z where z = = x+y ; Eine vollst¨ andige Funktionsdefinition mit einem where-Ausdruck ist dec zeit12 : num # num ; - - - zeit12 (s) <= (if h > 12 then h-12 else h + m) where (h,m,s) == zeit24 (s); Aufgrund des strengen Typkonzeptes erlaubt Hope, um eine h¨ohere Flexibilit¨ at zu erreichen, die Definition polymorpher Funktionen. Soll z. B. eine Funktion definiert werden, die zwei Listen konkateniert, und unabh¨angig vom Typ der Elemente der Liste ist, so kann dies erfolgen durch type var alpha; infix cat : 8; dec cat : list (alpha) # list (alpha) − > list (alpha); Wie man sieht, beginnt die Definition mit der Einf¨ uhrung einer Typ-Variablen, die einen universellen“ Typ repr¨ ansentiert. Hierdurch entspricht list (alpha) ” einer Liste von beliebigem Typ. Allerdings ist zu beachten, daß alpha w¨ahrend der ganzen Deklaration stets f¨ ur den gleichen Typ steht. Somit sind [1, 2, 3] cat [4, 5, 6] und ”1, 2, 3” cat ”4 5 6” korrekte Anwendungen von cat. Im ersten Fall repr¨ansentieren sie list (num) und im zweiten Fall list (char). Dagegen ist der Ausdruck [1, 2, 3] cat ”r 5 6” inkorrekt, da hier alpha einmal als num und einmal als char interpretiert w¨ urde. Neben den h¨ oheren Standardtypen Tupeln und Listen besteht auch die M¨oglichkeit, eigene Datenstrukturen zu definieren. Im Zusammenhang mit funktionalen und applikativen Sprachen war Hope die erste Sprache, die diese M¨ oglichkeit vorsah. Die Definition erfolgt u usselwort data. ¨ber das Schl¨ M¨ochte man z. B. eine Datenstuktur unscharf“ einf¨ uhren, die die Werte ” ja“, nein“ oder vielleicht“ annehmen kann, so kann dies durch ” ” ” data unscharf == ja ++ nein ++ vielleicht; erfolgen. Ein bin¨ arer Baum, an dessen Knoten sich Zahlen befinden, l¨aßt sich darstellen durch data baum == empty ++ spitze (sum) ++ knoten (baum # baum);
3.3 Weitere Applikative Programmiersprachen
233
Soll die Definition des Baumes universeller sein, in dem Sinn, daß die Knoten Elemente eines beliebigen Typs enthalten d¨ urfen (jedoch stets des gleichen Typ), so kann die Definition erfolgen durch data baum (alpha) => empty ++ spitze (alpha) ++ knoten (baum, (alpha) # baum (alpha)); Da Hope eine funktionale Sprache ist, sind sowohl funktionale Argumente, als auch funktionale Ergebnisse m¨ oglich. Zur Demonstration betrachten wir zun¨ achst die Hilfsfunktion quadrat, die eine Liste definiert, deren Elemente die Quadrate der Elemente einer anderen Liste sind. dec quadrat : num − > num; - - - quadrat (n) < − n*n Die Funktion allliste, die gegeben ist durch dec allliste : list (num) # (num − > num ) − > list (sum); - - - allliste (nil, f) < − nil; - - - allliste (n :: 1, f) < − f (n) :: allliste (1,f); liefert dann bei der Anwendung auf die Liste [2, 4, 6] und die Funktion quadrat durch allliste ([2, 4, 6] , square) das Ergebnis [4, 16, 36] : list (num) Um ein funktionales Ergebnis zu erzeugen, wird das Schl¨ usselwort lambda verwendet, wobei durch lambda generell, d. h. auch in Ausdr¨ ucken, lokal g¨ ultige Funktion definiert werden k¨ onnen. Ein klassisches Beispiel f¨ ur eine Funktion sowohl mit funktionalen Argumenten als auch mit funktionalen Ergebnis ist twice: dec twice : (alpha − > alpha) − > (alpha) − > alpha); - - - twice (f) <= lambda (x) => f (f(x)); Die Wirkung sei an zwei verschiedenen Anwendungen der Funktion twice demonstriert: twice (quadrat) lambda (x) => quadrat (quadrat (x)) : num − > num twice (quadrat) (3); 81 ; sum twice kann auch auf sich selbst angewandt werden, wie die zweite Anwendung zeigt:
234
3 Programmiersprachen
twice (twice); lambda (x) => twice (twice (x)): (alpha − > alpha) − > (alpha − > alpha) twice (twice) (quadrat) (3); 43046721 : num 3.3.11 Curry 3.3.11.1 Einfu ¨ hrung Curry ist eine Kombination funktionaler und logischer Programmierkonzepte. Die Sprache wurde erstmals 1995 vorgestellt und geht auf Arbeiten von Michael Hanus von der RWTH Aachen zur¨ uck, der zeigte, daß sich beide Programmierkonzepte in ein gemeinsames Berechnungsmodell einbetten lassen. Ausgangspunkt war die Sprache Haskell, die um Elemente der logischen Programmierung erweitert wurde. Als Vorg¨ angersprachen k¨onnen ALF und Babel angesehen werden. Die funktional-logische Programmierung entsteht im Wesentlichen dadurch, daß freie Variablen in Termen erlaubt werden. Der Auswertungsmechanismus funktionaler Sprachen (Reduktion) muß daf¨ ur durch den Auswertungsmechanismus logischer Sprachen (Resolution) erweitert werden. Hierbei wird das Pattern-Matching, welches bei funktionalen und applikativen Sprachen verwendet wird, durch die aus der logischen Programmierung bekannten Unifikation der formalen und aktuellen Parameter eines Funktionsaufrufs ersetzt. Der resultierende Mechanismus wird Narrowing genannt. Der Vorteil des Narrowing besteht darin, daß auch unvollst¨andig instanziierte Regelanwendungen ausgef¨ uhrt werden k¨ onnen. In diesem Fall werden die formalen Parameter einfach an die Variablen gebunden, welche die nicht instanziierten Parameter darstellen. Da durch die Unifikation Informationen nicht nur in eine Regel hinein-, sondern auch herausfließen k¨onnen, sind gr¨oßere Modifikationen der Reduktionssemantik notwendig als bei der der nachfolgend beschriebenen Residuation. Bei Narrowing wird die Effizienz deterministischer Funktionen mit den Suchm¨ oglichkeiten und dem Umgang mit unvollst¨andigen Informationen logischer Sprachen kombiniert. Als alternativer Auswertungsmechanismus existiert die Residuation. Bei der Residuation werden Funktionsaufrufe, bei denen unistanziierte Variablen als Parameter vorkommen, solange verz¨ ogert, bis alle Variablen von parallel laufenden Prozessen gebunden wurden bzw. bis die Variablen soweit instanziiert wurden, daß eine eindeutige Regelauswahl m¨oglich ist. Hierdurch ist eine eingeschr¨ ankte Nebenl¨ aufigkeit realisierbar. Bei Residuation werden somit Funktionsaufrufe deterministisch ausgewertet, wodurch es erforderlich wird, daß die Argumente soweit instanziiert sind, daß eine eindeutige Regelauswahl m¨oglich ist. Ist ein Argument nicht eindeutig an einen Wert gebunden, wird die Regelauswertung verz¨ ogert. Sobald ein separater Berechnungsvorgang das Argument instanziiert hat, kann die Berechnung fortgesetzt werden. Diese
3.3 Weitere Applikative Programmiersprachen
235
Verz¨ ogerung bedeutet, daß die nichtdeterministische Suche, falls sie gew¨ unscht ist, durch Pr¨ adikate oder explizite Disjunktionen dargestellt werden muß. Der Nachteil der Residuation besteht darin, daß sie unvollst¨andig ist und f¨ ur manche Terme keine L¨ osung berechnen kann, selbst wenn die darin enthaltenen ungebundenen Variablen nicht zur L¨ osung beitragen. Dagegen ist Narrowing vollst¨ andig, da es die Reduktion funktionaler und applikativer Sprachen mit der Unifikation bei der Parameter¨ ubergabe verbindet. Es k¨onnen damit auch Funktionsaufrufe reduziert werden, die ungebundene Variablen enthalten. In dem Berechnungsmodell von Hanus sind Reduktion, nichtdeterministische Suche, Residuation und Narrowing vereint. Vom Benutzer k¨onnen sowohl die effizierte Auswertung der funktionalen und applikativen Programmiersprachen genutzt werden. 3.3.11.2 Sprachkonzepte Da Curry eine Erweiterung von Haskell um Konzepte der logischen Program¨ mierung ist, besteht eine starke syntaktische Ubereinstimmung beider Sprachen. Funktionen Die Definition einer Funktion besteht aus einer Typdeklaration mit dem Schl¨ usselwort function und der Form function f : τ1 − > τ2 − > . . . − > τn − > τ wobei die τ1 , . . . , τn , τ polymorphe Typen sind und τ kein Funktionstyp sein darf. Die Funktionalit¨ at der Funktion wird spezifiziert durch f t1 . . . tn = t <= C Die linke Seite dieser Spezifikation besteht aus dem Funktionsnamen, gefolgt von n sog. pattern. Hierbei handelt es sich entweder um Variablen oder um die Anwendung eines Konstruktors auf pattern (s. u.). Der Teil ”<= C” gibt die Bedingung an, unter der die Spezifikation der Funktion gilt, d.h. unter welchen Bedingungen sie angewandt werden darf. Sie kann auch wegfallen. Der Ausdruck C wird auch als goal bezeichnet und besteht entweder aus einem Booleschen Ausdruck oder einer strikten Gleichung der Art l == r. Ein Boolscher Ausdruck besteht aus Booleschen Funktionen und den Operatoren ”,” (AND), ”;” (OR) bzw. ”not”. Im Gegensatz zu anderen Sprachen besitzt Curry keine expliziten Wahrheitswerte true und false. Sie werden u ¨ber Boolesche Funktionen definiert: p t1 . . . tn = true p t1 . . . tn = true <= p1 S11 . . . S1n1 , ... pk Sk1 . . . Sknk
236
3 Programmiersprachen
Da C freie Variablen enthalten darf, wird an dieser Stelle Nichtdeterminismus eingef¨ uhrt, der in Curry standardm¨ aßig mit Tiefensuche bearbeitet wird, d. h. die freien Variablen werden nacheinander mit allen m¨oglichen Werten instanziiert. Schl¨ agt die Reduktion fehlt, so wird ein Backtracking ausgel¨ost. Wenn eine Funktion aufgerufen wird, werden die Argumente und die formalen Parameter unifiziert. Dazu m¨ ussen evtl. die Argumente erst zur Kopfnormalform ausgewertet werden, um entscheiden zu k¨onnen, welche der linken Regelseiten einer Funktionsdefinition zu den gegebenen Parametern passen. ¨ Ublicherweise erfolgt die Auswertung von Funktionsapplikationen also durch Narrowing. Ist z. B. die Definition funtion append : [A] − > [A] − > [A] append [ ] L = L append [EIR] L = [E — append R L ] gegeben, so liefert Curry auf die Ziel-Gleichung append L [3,4] == [1,2,3,4] aufgrund der Auswertung durch Narrowing das Ergebnis L = [1,2] Der Benutzer kann jedoch mittels des Schl¨ usselworter rigid einen Auswertungsmechanismus gem¨ aß Residuation erzwingen. F¨ ugt man z.B. in der obigen Definition eval append 1 : rigid ein, so bewirkt dies, daß beim Aufruf der Funktion append immer das erste Argument ausgewertet wird, bevor eine Regel f¨ ur diese Funktion gesucht wird. Das Schl¨ usselwort eval wird generell benutzt, um Einschr¨ankungen hinsichtlich der Auswertung zu spezifizieren. Ferner kann spezifiziert werden, dass die Auswertung bestimmter Argumente nur dann erfolgen soll, wenn die Auswertung anderer Argumente zu speziellen Ergebnissen gef¨ uhrt hat. So u uft die im folgenden definierte ¨berpr¨ Funktion leq zwei nat¨ urliche Zahlen auf die Kleiner-Gleich-Relation: function leq : nat − > nat − > bool leq 0 N = true leq (s M) 0 = false leq (s M) (s N) = leq M N Bei einem Funktionsaufruf (leq 1 2 ) muß zun¨ achst das erste Argument 1 solange ausgewertet werden, bis eine Kopfnormalform vorliegt. Andererseits ist die Auswertung des zweiten Arguments S2 nur dann notwendig, wenn die Auswertung von S1 zu der Form (S )
3.3 Weitere Applikative Programmiersprachen
237
f¨ uhrt. Diese Abh¨angigkeit zwischen dem ersten und dem zweiten Argument kann der Benutzer mittels der Gleichung eval leq 1 : (s => 2) spezifizieren, und somit unn¨ otige Auswertungen des zweiten Arguments vermeiden. Typen Curry besitzt ein polymorphes Typsystem, welches sich eng an dasjenige von Haskell anlehnt, und wie dieses auch Typ-Klassen kennt. Es wird zwischen Funktionen, die Daten-Typen konstruieren (constructors), und Funktionen, die auf diesen Daten-Typen operieren (defined functions), unterschieden. Konstruktoren werden durch Datentyp-Deklarationen mittels des Schl¨ usselwortes datatype eingef¨ uhrt. Beispiele hierf¨ ur sind datatype bool = true | false datatype nat = 0 | s nat datatype tree A = leaf A | node (tree A) A (tree A) Konstrukte h¨ oherer Ordnung Die logischen Programmierm¨ oglichkeiten von Curry erlauben es auch, nach Funktionen zu suchen. Hierzu unterst¨ utzt Curry eine eingeschr¨ankte Variante der Unifikation h¨ oherer Ordnung, wobei durch eine Suche u ¨ber die definierten Funktionen eines Programms auch Funktionen berechnet werden k¨onnen. Außerdem kann eine Funktion auch dadurch genutzt werden, daß einfach alle definierten Funktionen des gesuchten Typs aufgez¨ahlt werden. In vielen logischen Programmiersprachen existiert ein sog. cut-Operator, um aus Effizienzgr¨ unden den Suchraum einzuschr¨anken. Da dieser Operator stark Seiteneffekt behaftet ist, existiert er in Curry nicht. Stattdessen existieren Implikationen und Quantoren, mit deren Hilfe Regeln lokal f¨ ur die Auswertung eines Goals definiert werden k¨ onnen, die nach Beendigung dieser Auswertung nicht mehr existieren. In dem folgendem Beispiel ist das Pr¨adikat select ein lokales Hilfspr¨ adikat f¨ ur das Pr¨ adikat perm: perm ([], []) ([E—L], [F—M]) <= select (F, [E—L], N), perm (N,M) where select(E, [E—L], L) select(E,[F—L],[F—M]) <= select(E,L,M) 3.3.11.3 Ein Programmbeispiel oglichkeiten von Curry sei ein Programm angegeben, Als Beispiel f¨ ur die M¨ welches alle Homomorphismen zwischen zwei Abelschen Gruppen berechnet.
238
3 Programmiersprachen
function hom: [nat] − > (nat− >nat− >nat) − > [nat] − >(nat− >nat− >nat) − > (nat− >nat) − > bool hom G1 0p1 G2 0p2 F = and [ test 0p1 0p2 F X Y | X < − G1, Y < − G1] function test: (nat − >nat− >nat) − > (nat− >nat− >nat) − > (nat − >nat) − > nat − > nat − > bool test 0p1 0p2 F X Y = true <= 0p2 (F X) (F Y) == F (0p1 X Y) Der Programmaufruf hom [0,1,2,3] add4 [0,1] add2 mod2 u uft, ob der Rest der Division durch 2 (mod 2) ein Homomophismus ¨berpr¨ zwischen den Gruppen < {0, 1, 2, 3}, add4> und < {0, 1}, add2> ist. Hierbei ist add4 die Addition modulo 4 und add2 die Addition modulo 2. Vordefiniert sind mod2, mod4, add2 und die Verkn¨ upfung and von Listenelementen. Das Programm kann auch durch hom [0,1,2,3] add4 [0,1] add2 F aufgerufen werden. Hierbei ist die Variable F an einen Homomorphismus zwischen den beiden Gruppen G1 und G2 gebunden. Dies ist ein Beispiel f¨ ur die o.a. M¨ oglichkeit in Curry Funktionen zu suchen. Die Bearbeitung des Aufrufs erfordert eine Unifikation h¨ oherer Ordnung. Ein m¨ogliches Ergebnis ist F = mod2 3.3.12 Weitere Sprachen Neben den bisher aufgef¨ uhrten Sprachen existieren zahlreiche weitere Sprachentwicklungen. Die Motive f¨ ur ihre Entwicklung waren sehr unterschiedlich. Sie reichen u.a. von Untersuchungen zur Semantikdefinition, zur Entwicklungen von Implementierungstechniken, zu Kombinationsm¨oglichkeiten der unterschiedlichen Programmierparadigmen bis zu kommerziellen Interessen. Die nachfolgende Auflistung kann daher keinen Anspruch auf Vollst¨andigkeit erheben, sondern soll ein Bild der vielf¨ altigen Ausgestaltungsm¨oglichkeiten der Konzepte der funktionalen und applikativen Programmierung vermitteln. 3.3.12.1 ASpecT Die Sprache ASpecT wurde an der Universit¨ at Bremen entwickelt. Die letzte Standard-Definition stammt aus dem Jahr 1996. Ziel der Entwicklung war es, ein m¨ oglichst benutzerfreundliches System zu entwickeln. Die Auswertungsstrategie beruht auf call-by-value. Seine Hauptanwendung fand es bei der Entwicklung des interaktiven graphischen Visualisierungs-Systems da-Vinci.
3.3 Weitere Applikative Programmiersprachen
239
3.3.12.2 Caml Bei der Sprache Caml handelt es sich um einen ML-Dialekt, der in Frankreich an der INRIA entwickelt wurde. Der Name steht f¨ ur Categorical Abstract ” Machine + ML“. Die Entwicklung fand 1984-1985 statt. Im Jahre 1990 wurde sie von Xavier Leroy zu Objective Caml“ (OCaml) erweitert. OCaml ” vereinigt funktionale, logische und objektorientierte Sprachkonzepte. 3.3.12.3 Cayenne Die Sprache Cayenne wurde 1998 von L. Augustsson eingef¨ uhrt. Sie ist eine experimentelle Sprache, mit der die M¨ oglichkeiten der Einf¨ uhrung der aus Varianten des getypten Lambda-Kalk¨ uls bekannten sogenannter abh¨angiger Typen (dependent Types) erforscht werden sollen. Hierbei k¨onnen Typen ¨ beliebig von Werten abh¨ angen oder w¨ ahrend der Ubersetzung von Funktionen berechnet werden. Damit lassen sich genaue Spezifikationen in den Typ kodieren. Die Typ¨ uberpr¨ ufung ist in diesem Fall jedoch nicht immer eindeu¨ tig entscheidbar, d.h. der Ubersetzungsvorgang terminiert unter Umst¨anden nicht. 3.3.12.4 CELP Die Sprache CELP wurde 2002 an der Universit¨at Udine von N. Kobayashi, M. Marin, T. Ida und Z. Che entwickelt. Es ist eine Sprache, die verz¨ogerte funktionale Programmierung, logische Programmierung und ConstraintL¨ osen mit reellen Zahlen kombiniert. Damit ist die Sprache sowohl Obermenge von higher-order polymorphen verz¨ ogerten funktionalen Sprachen als auch der Contraint-logischen Programmierung. Die Auswertungsstrategie basiert auf Lazy Narrowing und dem L¨ osen von Constraints. Der Name lehnt sich an die allgemeine constraint-logische Programmierung CLP(X) an, nur dass in diesem Fall die funktionale Programmierung ebenfalls eine wichtige Rolle spielt. 3.3.12.5 Clean Die Sprache Clean, auch bekannt unter dem Namen Cuncurrent Clean, ist eine Entwicklung der Universit¨ at Nijmegen in den Niederlanden. Bei der Entwicklung wurde ein besonderer Wert auf kurze Laufzeit gelegt. Es stellt zur Zeit eine der schnellsten Implementierungen funktionaler und applikativer Sprachen dar. Ein weiterer Entwicklungsschwerpunkt war die M¨oglickteit, interaktive Programme programmieren zu k¨ onnen.
240
3 Programmiersprachen
3.3.12.6 Eden Eden ist eine nebenl¨ aufige deklarative Sprache zur Programmierung reaktiver Systeme und paralleler Algorithmen auf Systemen mit verteiltem Speicher aus dem Jahre 1996. Dabei handelt es sich um eine Erweiterung von Haskell. Die funktionale Sprache dient als Berechnungssprache f¨ ur sequenzielle Programme, welche um eine Koordinationssprache zur Spezifizierung von Nebenl¨ aufigkeit erweitert wurde. Die Aufteilung in zwei Teilsprachen zur Berechnung und Koordination erlaubt die Beibehaltung der rein funktionalen Semantik der Kernsprache und deren einfache Erweiterung. Hiermit hat Eden ¨ eine Ahnlichkeit mit Goffin. Nebenl¨ aufigkeit wird in Eden durch Prozesse ausgedr¨ uckt. In der Sprache k¨ onnen Prozessabstraktionen definiert werden, welches statische Konstrukte - a¨hnlich Lambda-Ausdr¨ ucken - sind. Durch die Instanziierung (entspricht der Applikation eines Lambda-Ausdrucks) werden dann Prozesse erzeugt, die nebenl¨ aufig ausgef¨ uhrt werden. Untereinander k¨onnen Prozesse u ¨ber Kan¨ale kommunizieren, die als verz¨ ogerte Listen modelliert werden. Lesende Prozesse greifen einfach auf Listenelemente zu, w¨ ahrend schreibende Prozesse Listenelemente generieren. Die Kommunikation und Synchronisierung ist f¨ ur die Prozesse transparent. Es ist aber f¨ ur einzelne Prozesse m¨oglich, AntwortKan¨ ale explizit zu erzeugen und zu nutzen, um die Kommunikation zwischen mehreren Prozessen gleichzeitig durchzuf¨ uhren. 3.3.12.7 Erlang Die Sprache Erlang wurde seit 1987 von der schwedischen Firma Ericsson entwickelt. Die Entwicklung erfolgte in Zusammenarbeit mit der Universit¨at Uppsala. Der Name steht f¨ ur ERicsson LANGuage. Nach anderen Quellen ist die Sprache nach A. K. Erlang genannt worden. Im Jahre 1998 wurde Erlang als Open Source allgemein freigegeben. Die Sprache wird laufend weiterentwickelt. Ziel der Entwicklung von Erlang war die Implementierung eines kommerziellen Systems zur Modellierung nebenl¨ aufiger Prozesse, wie sie vor allem in der Telekommunikation auftreten. Obwohl sich die Syntax an Prolog anlehnt, ist es keine logische, sondern eine strikte funktionale Sprache. Erlang besitzt eine dynamische Typisierung, ¨ahnlich wie Prolog oder Lisp, d.h. dass nicht die Variablen typisiert werden, sondern die Werte, die zur Laufzeit an diese Variablen gebunden werden. Das Hauptanliegen der Entwicklung von Erlang, die Unterst¨ utzung der Programmierung nebenl¨aufiger Prozesse, wird durch eine kleine aber m¨ achtige Menge primitiver Operationen zur Einf¨ uhrung von Prozessen und der Modellierung von Kommunikation zwischen diesen Prozessen unterst¨ utzt. Die Nebenl¨ aufigkeit wird durch drei Sprachkonstrukte unterst¨ utzt: Die primitive Funktion spawn erzeugt einen neuen Prozess, der Operator ! (genannt send) verschickt einen Konstruktorterm als Nachricht und der nichtdeterministische Empfang wird durch receive-Anweisungen durchgef¨ uhrt. Bei
3.3 Weitere Applikative Programmiersprachen
241
der Erzeugung eines Prozesses kann optional auch noch der Rechner (Knoten in der Erlang-Terminologie) angegeben werden, auf dem der Prozess ablaufen soll. Durch diese Erweiterung wird Verteiltheit erm¨oglicht; die Kommunikation l¨ auft bei verteilten Systemen genauso durch Prozesskommunikation ab wie bei nicht verteilten. Hierzu besitzt jeder Prozess eine Mailbox“. Die ” Auswertungsstrategie in Erlang ist strikt, und Variablen k¨onnen nur einmal an einen Wert gebunden und danach nicht mehr ge¨andert werden. Daher ist der Charakter der Sprache funktional. Ein- und Ausgabe sind aber nicht deklarativ, und es gibt u ¨ber die Stadard-Bibliotek die M¨oglichkeit, globale und ver¨ anderbare Variablen zu definieren. 3.3.12.8 Escher Die Sprache Escher beruht auf einer Entwicklung von J. W. Lloyd von der Universit¨ at Bristol aus dem Jahre 1995. Sie kombiniert wie Curry Elemente der funktionalen und der logischen Programmierung. Allerdings gibt es in Escher keine nichtdeterministischen Funktionen, sondern alle Funktionen liefern ein Ergebnis, das allerdings eine Disjunktion verschiedener Ergebnisse sein kann. Auf diese Weise wird in Escher Nichtdeterminismus modelliert. Funktionen, die einen booleschen Wert als Resultat liefern, werden in Escher auch als Pr¨ adikate bezeichnet. Die Auswertungsstrategie ist Residuation, wobei durch die Angabe eines sog. Modus f¨ ur jede Funktion spezifiziert werden kann, auf welche Art und Weise Funktionsaufrufe verz¨ogert werden. Wenn f¨ ur einen Parameter der Modus NONVAR festgelegt wird, werden alle Aufrufe dieser Funktion verz¨ ogert, bis der entsprechende Parameter instanziiert ist. Auf diese Weise l¨ asst sich der Nichtdeterminismus beim Aufruf von Pr¨ adikaten einschr¨ anken. Eine Besonderheit von Escher sind die syntaktischen Elemente SOME und ALL, welche als Existenz- und Allquantor freie Variablen deklarieren. SOME als rechte Seite einer Funktion f¨ uhrt dazu, dass es f¨ ur die entsprechenden Variablen im Ergebnis eine g¨ ultige Belegung gibt, ALL fordert, dass alle m¨oglichen Belegungen auch g¨ ultig sind. Escher bietet auch einige syntaktische und semantische Erweiterungen, die ¨ aus funktionalen Programmiersprachen bekannt sind. Ahnlich zu Haskell unterst¨ utzt Escher list comprehensions zur pr¨ agnanten Konstruktion von Listen sowie Funktionen h¨ oherer Ordnung und anonyme Funktionen (λ-Ausdr¨ ucke). 3.3.12.9 FALCON Die Sprache FALCON wurde 1993 am Imperial Collage London von Y. Guo und H. Pull entwickelt. Die Erfahrungen mit dieser Entwicklung flossen in die Entwicklung der Sprache Goffin ein. Die Sprache kombiniert funktionale und logische Programmierung mit Constraints. Systaktisch bestehen FALCONProgramme aus bewachten (guarded) funktionalen Termersetzungsregeln sowie aus Relationen u ucken. Semantisch ist FAL¨ber funktionalen Ausdr¨ CON eine Constraint-logische Programmiersprache, bei der das unterliegende
242
3 Programmiersprachen
Constraint-System mit funktionalen Programmen erweitert werden kann. Guo beschreibt diese Kombination mit der Gleichung FALCON = CLP(CFP), die constraint-funktionale Programmierung stellt also das Constraint-System f¨ ur ein allgemeines constraint-logisches System dar. Die funktionale Programmierung eignet sich gut zur Definition eines Constraint-Systems, da sie benutzerdefinierte Objekte und die F¨ahigkeit, Constraints u ucken zu l¨ osen, zur Verf¨ ugung stellt. ¨ber allemeinen Ausdr¨ 3.3.12.10 Goffin Die Sprache Goffin wurde 1997 von M. M. T. Chakravarty, Y. Guo, M. K¨ohler und H. C. R. Lock entwickelt. Y. Guo hat zuvor bereits die Sprache FALCON entwickelt. Konzepte dieser Sprache flossen auch in Goffin ein. Es ist eine constraint-funktionale Erweiterung der Programmiersprache Haskell, bei der Funktionen in eine nebenl¨ aufige Constraint-Programmiersprache eingebettet ¨ sind. Die grundliegende Idee dieser Integration basiert auf der Uberlegung, eine Programmiersprache aus einer Berechnungssprache und einer Koordinationssprache aufzubauen. Die Berechnungssprache f¨ uhrt sequenziell die eigentlichen Berechnungen aus, w¨ ahrend die Koordinationssprache Berechnungen erzeugt und f¨ ur die Kommunikation der einzelnen Berechnungen untereinander sorgt. Der Constraint-Teil von Goffin der Sprache ist explizit getrennt vom funktionalen Teil und dient diesem als Koordinationssprache, stellt also Mittel zur Organisation funktionaler Refuktionen zur Verf¨ ugung. Im Constraint-Teil k¨ onnen logische Variablen erzeugt werden, w¨ahrend die Aufrufe von Funktionen verz¨ ogert werden, bis ein Parameter instanziiert ist. Funktionen k¨ onnen Constraints erzeugen und erlauben damit die Definition parametrisierter Koordinations-Konstrukte. Constraints werden in geschweifte Klammern ({ und }) eingeschlossen, wobei ungebundene (logische) Variablen explizit deklariert werden: {x, y in e} Logische Variablen werden durch Gleichheitsconstraints (geschrieben als exp1 ← exp2 ) instanziiert. Mehrere dieser einfachen Constraints k¨onnen durch Kommata getrennt zu Konjuktionen zusammengefasst werden: {x, y, r in x ← f oo a, y ← bar a, r ← x + y} In Goffin k¨ onnen logische Variblen nur an Datenstrukturen gebunden sein, die keine Funktionen enthalten, dadurch wird Unifikation h¨oherer Ordnung vermieden. Nat¨ urlich ist es damit in Goffin nicht m¨oglich, Funktionen zu berechnen, wie dies z. B. Curry erlaubt. Die M¨ oglichkeit, durch Funktionen Constraints zu berechnen, wird genutzt, um z. B. Berechnungen zu parallelisieren, da die einzelnen Constraints einer Konjunktion nebenl¨ aufig abgearbeitet werden k¨onnen. Weiterhin k¨ onnen Funktionen aus Constraints andere Constraints bilden, also als Kombinatoren f¨ ur Koordinationsstrukturen dienen.
3.3 Weitere Applikative Programmiersprachen
243
3.3.12.11 λ-Prolog λ-Prolog ist eine relativ alte Programmiersprache, die bereits 1988 von G. Nadathur und D. Millner entwickelt wurde. Ausgangspunkt war die logische (pr¨ adikative) Sprache Prolog, die um Elemente der funktionalen und applikativen Programmierung erweitert wurde. Somit ist λ-Prolog eine prim¨ar logische Programmiersprache, die Funktionen h¨oherer Ordnung, polymorphe Typisierung, abstrakte Datentypen sowie λ-Abstraktionen in Datenstrukturen erlaubt. Die Erweiterung um λ-Terme gegen¨ uber Prolog erfordert eine erweiterte Auswertungsstrategie, und zwar Unifikation h¨oherer Ordnung zur Berechnung von Funktionen. 3.3.12.12 Lλ D. Miller, der bereits an der Entwicklung der Sprache λ-Prolog maßgeblich mitgewirkt hatte, entwickelte 1991 die funktional-logische Sprache Lλ . Im Gegensatz zu λ-Prolog erfordert sie keine Unifikation h¨oherer Ordnung, da sie das Auftreten von Funktionsvariablen einschr¨ankt. Der Vorteil dieser Restriktionen ist, daß die Unifikation in diesem Fall entscheidbar ist und immer ein allgemeinster Unifikator gefunden wird, falls dieser existiert. Die Motivation zur Entwicklung von Lλ ist die Meta-Programmierung, also die Verarbeitung von Programmen durch Programme. Dazu ist die Manipulation syntaktischer Strukturen (Programme, Formeln, Typen und Beweise) notwendig. Die Unifikation erleichtert den Umgang mit diesen Termen, z. B. die Berechnung der Termgleichheit modulo α, β und η-Konvertierungen. 3.3.12.13 Leda Die Sprache Leda ist eine Entwicklung von T. A. Budd aus dem Jahre 1995. Sie ist eine streng getypte Programmiersprache, die sowohl objekt-orientierte als auch funktionale und logische (bzw. relationale) Aspekte in einer Sprache vereint. Der logische Anteil wird durch ein neues Sprachkonstrukt, die Relation, in die Sprache intergriert. Relationen ¨ahneln Prozeduren und Funktionen, werden aber als eine Art boolscher Wert behandelt, der in Konjunktionen und Disjunktionen allerdings durch Backtracking ausgewertet wird, um so L¨ osungen zu generieren. Syntaktisch sind Relationen Funktionen, die Werte des speziellen Typs relation als Ergebnis haben. Einfache Relationen werden gebildet, indem mit dem relationalen Zuweisungsoperator < − Werte an eine Variable zugewiesen werden. Dieser Operator unterscheidet sich von der normalen Zuweisung dadurch, dass die Variable auf den vorigen Wert zur¨ uckgesetzt wird, sollte das Backtracking diese Operation r¨ uckg¨angig machen. Boolsche Ausdr¨ ucke k¨ onnen u ¨berall verwendet werden, wo Relationen syntaktisch erwartet werden und komplexere Relationen k¨onnen dann durch Dis- und Konjunktionen von Relationen gebildet werden.
244
3 Programmiersprachen
Wenn eine Relation als Bedingung in einer Fallunterscheidung oder als Bereich in einer for-Schleife angewendet wird, beginnt der Berechnungsprozess. In einer Fallunterscheidung gilt die Bedingung als wahr, falls die Relation erf¨ ullt ist, in einer Schleife k¨ onnen alle L¨ osungen einer Relation bearbeitet werden. Die Auswertungsstragegie ist call-by-value, es ist aber auch m¨oglich, durch Parameterdeklarationen call-by-reference und call-by-name f¨ ur Funktionen zu deklarieren. Referenzparameter werden benutzt, um beim Aufruf von Relationen Variablen zu binden. Durch die Verwendung von call-by-name Parametern ist es sogar m¨ oglich, nicht-strikte Kontrollstrukturen in Leda selbst zu programmieren. Die Unterst¨ utzung funktionaler Programmierung basiert auf Funktionen h¨oherer Ordnung und der M¨ oglichkeit, Datentypen (Klassen) und Funktionen mit Datentypen zu parametrisieren. Objekt-orientierte Programmierung wird ¨ durch Klassendefinitionen, Vererbung und dem Uberschreiben von Methoden durch dynamische Bindung erm¨ oglicht. Als Nachfolgesprache wurde von Budd 2002 die Sprache J/mp entwickelt, bei der sich der objektorientierte Teil der Sprache an Java orientiert. 3.3.12.14 Mercury Die Sprache Mercury wurde 1995 von Z. Somogyi, F. Henderson und T. Conway an der Universit¨ at Melburne entwickelt. Sie ist prim¨ar eine logische Programmiersprache, besitzt jedoch einige Eigenschaften, die die funktionale und applikative Programmierung unterst¨ utzen. So ist es beispielsweise m¨oglich, neben Prolog-artigen Pr¨ adikaten deterministische Pr¨adikate in einer Notation wie in funktionalen und applikativen Sprachen zu definieren: fibonacci(N) = F :( if N=< 2 then F = 1 else F = fibonacci (N - 1) + fibonacci(N - 2) ). Mercury unterst¨ utzt auch Funktionen h¨ oherer Ordnung. Funktionswerte k¨ onnen durch λ-Ausdr¨ ucke erzeugt werden und auf Argumente angewendet werden. Dies geschieht entweder durch Verwendung des eingebauten Pr¨adikats apply oder durch die Anwendung einer Variablen auf Parameter, wobei die Variable an einem λ-Ausdruck gebunden sein muss. Durch ein ausgefeiltes Typ- und Modus-System k¨onnen weiterhin alle Typen und Parameter-Modi (unbestimmt, nonvar, in, out) deklariert werden. Der Compiler pr¨ uft diese Deklarationen und nutzt die Informationen zur Optimierung. Neben den Parameter-Modi k¨ onnen Pr¨adikate als deterministisch (immer genau einmal erfolreich), semi-deterministisch (erfolglos oder genau einmal erfolgreich) sowie nicht-deterministisch deklariert werden. Der Compiler kann anhand dieser Informationen (semi-)deterministische Funktionsaufrufe effizienter u ¨bersetzen als nicht-deterministische.
3.3 Weitere Applikative Programmiersprachen
245
Der Mercury-Compiler ist selbst in Mercury geschrieben und erzeugt CCode. Generell ist der Anteil an compiliertem Code gegen¨ uber demjenigen an interpretiertem Code wesentlich gr¨ oßer als sonst u ¨blich. Eine weitere Besonderheit besteht in den vielf¨ altigen M¨ oglichkeiten, Code anderer Sprachen wie Java und C++ u ¨ber Interfaces einzubinden. 3.3.12.15 Oz Die Sprache Oz wurde 1995 von G. Smolka entwickelt. Sie vereint logische, funktionale und objekt-orientierte Programmierparadigmen. Sie umfaßt u. a. Funktionen h¨ oherer Ordnung sowie einem Zustand, der objekt-orientierte Programmierung erm¨ oglicht. Die Programmierung mit Oz l¨aßt sich folgendermaßen beschreiben: Eine Berechnung ist als ein Netzwerk nebenl¨aufiger Objekte organisiert, die miteinander und mit der umgebenden Welt (Ein-/Ausgabe) interagieren. Innerhalb der Objekte werden Funktionen und Pr¨adikate verwendet, um zustandsloses Wissen in algorithmischer oder deklarativer Form zu verarbeiten. Nichtdeterministische Berechnungen werden in Oz explizit durch Sprachkonstrukte f¨ ur logische Disjunktionen sowie f¨ ur die Suche beschrieben. Durch die Nebenl¨ aufigkeit und die M¨ oglichkeit zur Suche ist es m¨oglich, in vielen F¨ allen, in denen a¨quivalente Prolog-Programme keine L¨osung finden, mit Oz alle L¨ osungen zu berechnen. Objekte in Oz sind Prozeduren, die eine Zelle referenzieren, die den aktuellen Zustand des Objektes repr¨ asentiert. Zellen k¨onnen in einer ununterbrechbaren Operation modifiziert werden. Da Objekte Prozeduren sind, entspricht das Versenden einer Nachricht einem Prozeduraufruf, bei dem die entsprechende Methode mit dem aktuellen Zustand und einer logischen Variable, die den Zustand zum Methodenende aufnimmt, aufgerufen wird. Der Rumpf einer Methode wird so ausgef¨ uhrt, dass der Zustand von einer Anweisung zur n¨ achsten durchgef¨ adelt“ wird, so dass er alle Attributzugriffe und ” -zuweisungen widerspiegelt. Eine interaktive Implementierung von Oz entstand am Deutschen Forschungszentrum f¨ ur K¨ unstliche Intelligenz (DFKI). 3.3.12.16 Scala Die Sprache Scale wurde 2005 von einer Forschungsgruppe um M. Odersky entwickelt. Es handelt sich hierbei um eine Kombination von objektorientierten und funktionalen Programmierparadigmen. Die Sprache beinhaltet eine statische Typ¨ uberpr¨ ufung. Spezielle polymorphe Klassen erlauben eine generische Programmierung, wie sie z. B. auch in Java 5 vorgesehen ist. Allerdings lassen sich die Typparameter genauer auf Sub- oder Superklassen einer gegebenen Klasse einschr¨ anken, mit Varianzannotationen k¨onnen Vererbungsbeziehungen zwischen generischen Klassen festgelegt werden.
246
3 Programmiersprachen
3.3.12.17 TyPiCal ¨ Die Sprache TyPiCal wurde 2002 von Naoki Kobayashi eingef¨ uhrt. Ahnlich wie Erlang ist sie eine Sprache zur Modellierung nebenl¨aufiger Prozesse und deren Kommunikation. Ein speziell f¨ ur diese Sprache entwickeltes Typsystem stellt z. B. sicher, daß in einem korrekt getypten System keine Deadlocks auftreten.
4 Implementierungstechniken
4.1 Interpretierer 4.1.1 Einleitung Die klassische Technik zur Implementierung funktionaler und applikativer Sprachen ist die des Interpretierens. Stammvater aller Interpretierer ist der in Kapitel 3.2.2.12 vorgestellte Interpretierer von McCarthy. Das Hauptproblem aller Implementierungstechniken, die Bindung von Werten an Namen, wird bei ihm durch das Konzept der Assoziationsliste (A-Liste) realisiert. Die Elemente dieser Liste sind Paare, deren 1. Komponente aus einem Namen und deren 2. Komponenete aus einem Wert besteht. Anschaulich kann man sich die A-Liste als einen Keller vorstellen, in den bei jeder Bindung eines Wertes an einen Namen, z.B. bei einer Funktions-Deklaration (Label-Deklaration) oder einer Funktionsapplikation, das entsprechende Paar gespeichert wird.
N ame W ert .. . A-Liste Trifft man beim Interpretieren auf einen Namen und ben¨otigt seinen Wert, so sucht man im Keller von oben nach unten, bis man das erste Paar findet, dessen 1. Komponente der gesuchte Name ist, und nimmt den zugeh¨orenden Wert. Erreicht man das Ende einer Funktion, so werden die f¨ ur diese Funktion vorgenommenen Eintr¨ age wieder gel¨ oscht. Das L¨oschen ist einfach, da es sich stets um die obersten Eintr¨ age im Keller handelt. In dieser Form ist das A-Listen-Konzept auf den Seiten 1-19 des LISP-1.5 Manuals (McCarthy 1962) beschrieben. Es handelt sich hierbei um eine reine dynamische Variablenbindung (dynamic scoping); man erh¨alt eine statische
248
4 Implementierungstechniken
Variablenbindung (static scoping), indem man in der A-Liste zus¨atzliche Informationen speichert (s. Kap. 3.2.5). Trifft man beim Interpretieren auf eine Funktionsdefinition, so speichert man in der 1. Komponente den Funktionsnamen, in der 2. Komponente ihre Definition und in der 3. Komponente den derzeitigen Inhalt der A-Liste. Ben¨ otigt man bei der Interpretation des Funktionsrumpfes den Wert einer freien Variablen, so durchsucht man nicht die aktuelle A-Liste, sondern die 3. Komponente. Dies sei an dem folgenden Beispiel erl¨ autert, wobei wir nicht die LISP-Notation, sondern eine LambdaKalk¨ ul-¨ ahnliche Notation zugrunde legen. Beispiel 4.1-1 Beim Interpretieren habe man die Definition einer Funktion f erreicht . . . f = λv .r . . .
.
Die momentane Belegung der A-Liste sei N amen W ertn .. . N ame1 W ert1 A-Liste In der A-Liste wird das Tripel f f = λv .r Momentaner Inhalt der A-Liste
,
eine sogenannte Closure von f, abgespeichert:
f
f = λv .r
W ertn W ert1 ··· N amen N ame1
N amen W ertn .. . N ame1 W ert1 A-Liste Hierzu sei bemerkt, daß bei der Implementierung dieser Technik nat¨ urlich anstelle von Kopien mit Verweisen auf Abschnitte der A-Liste gearbeitet wird (vgl. den Interpretierer aus Kap. 3.2.2.12). Die obige Darstellung dient lediglich dem besseren Verst¨ andnis. Erh¨ alt nun bei der weiteren Interpretation eine Variable x den Wert von f , so wird wieder ein Tripel in der A-Liste abgespeichert. Die 1. Komponente ist der Name x, den Inhalt der 2. und 3. Konponente erh¨alt man aus dem Eintrag f¨ ur f :
4.1 Interpretierer
x
f = λv .r
249
W ertn W ert1 ··· N amen N ame1
.. . f
f = λv .r
W ertn W ert1 ··· N amen N ame1
N amen W ertn .. . N ame1 W ert1 A-Liste
Es wird also die Closure von f an die Variable x weitergereicht“. Das ge” schieht aus folgendem Grund: Trifft man bei der weiteren Interpretation auf eine Applikation x(arg1 , . . . , argn )
,
so wird die A-Liste von oben nach unten durchsucht, bis man den ersten Eintrag mit dem Namen x findet. Als Wert von x erh¨alt man die Definition f = λv .r . In der A-Liste werden die neuen Werte f¨ ur die Variablen v abgespeichert, und der Rumpf r wird ausgewertet. Ben¨otigt man hierbei den Wert einer freien Variablen y, so wird er durch Suchen in der 3. Komponente des Eintrags f¨ ur x bestimmt und nicht durch Suchen in der A-Liste. Dieses Vorgehen garantiert, daß der Interpretierer bei jedem Zugriff auf den Wert einer freien Variablen gerade den gem¨ aß statischer Variablenbindung korrekten Wert dieser Variablen erh¨ alt (zum Beweis siehe Bauchrowitz (Bauchrowitz 1980)). Durch die langwierigen Suchvorg¨ ange ist das A-Listen-Konzept jedoch sehr zeitaufwendig. Daher wurden inzwischen eine Reihe von anderen Konzepten entwickelt, die wesentlich effizienter sind. Das bekannteste wird im folgenden Abschnitt vorgestellt. 4.1.2 Shallow-Binding Bei dem Shallow-Binding-Konzept wird der Suchvorgang ersetzt durch den Zugriff auf eine feste Zelle. Hierzu verf¨ ugt der Interpretierer u ¨ber einen speziellen Wertespeicher. Beim Einlesen des Programms vom Eingabemedium wird jeder Name mit einer festen Adresse im Wertespeicher identifiziert. Ausschlaggebend ist lediglich der Name selbst, d.h. gibt es z.B. in einem Programm eine Funktion f = λx.rf und eine andere Funktion g = λx.rg , so erh¨alt die Variable x der Funktion f die gleiche Adresse wie die Variable x der Funktion g. Der Interpretierer muß daf¨ ur sorgen, daß stets der korrekte Wert in dieser Zelle steht. Hierf¨ ur steht ein sogenannter Datenkeller zur Verf¨ ugung. Außer Wertespeicher und Datenkeller verf¨ ugt der Interpretierer noch u ¨ber einen
250
4 Implementierungstechniken
Keller f¨ ur R¨ uckkehradressen, der jedoch in diesem Zusammenhang noch ohne Bedeutung ist. An Hand der Situation von Beispiel 4.1-1 wird die Technik sowohl f¨ ur dynamische als auch f¨ ur statische Variablenbindung illustriert. Dynamische Variablenbindung Nach dem Einlesen des Programms ist f¨ ur jeden Namen eine feste Adresse im Wertespeicher reserviert worden, also auch f¨ ur f und v = (v1 , . . . , vn ). ¨ Der Ubersichtlichkeit halber wird der Wertespeicher hier als ein zusammenh¨ angender Block dargestellt, der symbolisch u ¨ber Namen adressiert ist. .. . vn : : v1 : f: .. . Wertespeicher Erreicht man beim Interpretieren zum ersten Mal die Definition f = λv .r, so wird im Wertespeicher unter der Adresse f diese Definition eingetragen: .. . vn : : v1 : f:
f = λv .r .. . Wertespeicher
Bei der ersten Applikation der Funktion f werden in den Adressen f¨ ur v die (ggf. ausgewerteten) Argumente eingetragen. Ist die erste Applikation alt man als Belegung des Wertespeichers f (a1 , . . . , an ), so erh¨
vn : : v1 : f:
.. . an a1 f = λv .r .. . Wertespeicher
4.1 Interpretierer
251
Danach kann die Interpretation des Rumpfes r erfolgen. Ergibt sich hierbei ussen im Werteein zweiter (rekursiver) Aufruf von f , z.B. f (b1 , . . . , bn ), so m¨ speicher die neuen Argumente eingetragen werden. Da man jedoch nach der R¨ uckkehr aus der Rekursion die alten Argumente unter Umst¨anden wieder ussen in den ben¨ otigt, d¨ urfen die ai nicht u ¨berschrieben werden, sondern m¨ Datenkeller gerettet werden. Dies gilt i.a. auch f¨ ur den Wert des Funktionsnamens f . Trifft man also auf den Funktionsaufruf f (b1 , . . . , bn ), so speichert man im Datenkeller f¨ ur jedes v1 das Paar (v1 , ai ) ab. Danach kann man im Wertespeicher die ai durch die bi u ¨berschreiben:
vn : : v1 : f:
an
vn
.. .
an
bn
a1 b1 f = λv .r .. .
Funktions-
−→
aufruf
Wertespeicher
. v1 .. a1 .. f . f = λv .r ∗∗∗ .. . Datenkeller
Nun wird wieder neu mit der Interpretation von r begonnen. Erreicht man das Funktionsende, so m¨ ussen die im Datenkeller zwischengespeicherten Werte wieder in den Wertespeicher zur¨ uckgeladen werden: Die Paare (N ame, W ert) - genauer (Adresse, W ert) - werden sukzessive bis zur Endmarkierung ∗ ∗ ∗ gel¨oscht, wobei die Werte in die Zelle zur¨ uckgeschrieben werden, deren Adresse jeweils in der 1. Komponente angegeben ist. vn : an bn an : v1 : a1 b1 a1 f: f = λv .r .. . Wertespeicher
Funktions-
−→ ende
.. . Datenkeller
ur nichtDamit ist die Interpretation des Aufrufs f (b1 , . . . , bn ) beendet. F¨ rekursive Aufrufe wird analog verfahren. Statische Variablenbindung Statische Variablenbindung erfordert eine besondere Behandlung der freien Variablen im Rumpf einer Funktion. Wie zuvor wird beim Einlesen des Programms f¨ ur jeden Namen eine feste Adresse im Wertespeicher reserviert. Zus¨ atzlich werden f¨ ur jede Funktionsdefinition die im Rumpf enthaltenen freien Variablen bestimmt. Nehmen wir an, der Rumpf von f enthalte die freien
252
4 Implementierungstechniken
Variablen vf = vf 1 , . . . , vf k . Jeder dieser Variablen ist ebenfalls eine feste Adresse im Wertespeicher zugeordnet: .. . vn : .. . v1 : f: .. . vf k : .. . vf 1 : .. . Wertespeicher Erreicht man beim Interpretieren zum ersten Mal die Definition f = λv .r, so wird im Wertespeicher unter der Adresse von f neben der Definition selbst auch eine Liste von Paaren (vf i , wf i ) abgespeichert, wobei wf i , der momenur vf i : tane Wert von vf i , ist. Er ergibt sich aus dem Inhalt der Zelle f¨ .. . vn : .. . v1 : f : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vf k : .. .
wf k
vf 1 :
wf 1 .. . Wertespeicher
Analog zu Kap. 4.1.1 wird der Eintrag f¨ ur f als Closure von f“ bezeichnet. ” Bei der ersten Applikation von f werden die (ggf. ausgewerteten) Argumente in die Adressen f¨ ur v eingetragen, und die in f angegebenen Werte wf i werden in die Zellen von vf i geladen. Danach wird mit der Interpretation von r begonnen. Die Belegung des Wertespeichers ist jetzt:
4.1 Interpretierer
vn : .. .
253
.. . an
v1 : a1 f : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vf k : .. .
wf k
vf 1 :
wf 1 .. . Wertespeicher
Erh¨ alt bei der Interpretation von r eine Variable x (x ∈ / v ) den Wert von f - z.B. dadurch, daß eine f umfassende Funktion h = λx . . . auf f appliziert wird -, so wird in deren Zelle der Inhalt von Zelle f , also die Closure von f , hineinkopiert: .. . x : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vn : .. .
an
v1 : a1 f : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vf k : .. .
wf k
vf 1 :
wf 1 .. . Wertespeicher
W¨ahrend der Interpretation von r k¨ onnen jetzt in die Zellen vf i bzw. in die Zelle f neue Werte eingetragen werden. Dies ist z.B. dadurch m¨oglich, daß wiederum eine f umfassende (aber in h enthaltene) Funktion aufgerufen wird. Nehmen wir an, im Wertespeicher sei damit die folgende Belegung erreicht:
254
4 Implementierungstechniken
.. . x : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vn : .. .
an
v1 : a1 f : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vf k : wfk .. . vf 1 : wf1 .. . Wertespeicher Bei einem weiteren Aufruf von f , z.B. f (b1 , . . . , bn ), m¨ ussen wie im Fall von statischer Variablenbindung alle f¨ ur f relevanten Werte in den Datenkeller gerettet werden, bevor die neuen Werte eingetragen werden k¨onnen. Hierzu geh¨ oren nun auch die alten Werte der vf i ; die neuen Werte dieser freien Variablen werden der Closure von f entnommen: .. . x : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vn : .. .
an
bn
v1 : a1 b1 f : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vf k : wfk wf k .. . vf 1 : wf1 wf 1 .. . Wertespeicher
Funktions-
−→
4.1 Interpretierer
.. . .. . .. . .. . .. . .. .
vf k
vf 1 vn aufruf
−→
255
wfk wf1 an
v1 a1 .. f . f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) ∗∗∗ .. . Datenkeller Der Rumpf r von f wird nun erneut ausgewertet. Trifft man dabei auf eine Applikation x(c1 , . . . , cn )
,
so m¨ ussen die in der Zelle x angegebenen Variablen v und vf sowie der Funktionsname f neue Werte erhalten. Dazu werden zun¨achst die alten Werte gerettet. Anschließend werden in die vi die Werte ci , in die vf i die Werte wf i und in f der Inhalt von x hineingeschrieben: .. . x : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vn : .. .
an
bn
cn
v1 : a1 b1 c1 f : f = λv .r / (vf 1 , wf 1 ) . . . (vf k , wf k ) .. . vf k : wfk wf k wf k .. . vf 1 : wf1 wf 1 wf 1 .. . Wertespeicher
Funktions-
−→
256
4 Implementierungstechniken
vf k
vf 1 vn
aufruf
−→
.. . .. . .. . .. . .. . .. .
wf k wf 1 bn
v1 b1 .. f . f = λv .r/(vf 1 , wf 1 ) . . . (vf k , wf k ) .. . .. vf k . wfk .. . .. vf 1 . wf1 .. . an vn .. . .. v1 . a1 .. f . f = λv .r/(vf 1 , wf 1 ) . . . (vf k , wf k ) ∗∗∗ .. . Datenkeller
Es erfolgt jetzt eine erneute Auswertung des Funktionsrumpfes r. An dieser Stelle wird noch einmal der Unterschied zwischen statischer und dynamischer Variablenbindung deutlich: In dem hier beschriebenen Verfahren f¨ ur statische Variablenbindung wird bei einem Zugriff auf die freie Variable ultiger Wert ermittelt, also derjenige Wert von vf i , der bei vf i jetzt wf i als g¨ der urspr¨ unglichen Definition von f g¨ ultig war. Ein Interpretierer mit dynamischer Variablenbindung w¨ urde statt dessen wfi als Wert von vf i ermitteln, also einen Wert, der u ¨berhaupt nach“ der Definition von f entstanden“ ist. ” ” Wenn schließlich das Ende eines Funktionsrumpfes erreicht ist, so werden die beim Funktionsaufruf im Datenkeller zwischengespeicherten Werte wieder in den Wertespeicher zur¨ uckgeladen. Zusammenfassend l¨ aßt sich das Shallow-Binding-Konzept folgendermaßen charakterisieren:
4.1 Interpretierer
257
1. F¨ ur jeden Namen existiert genau eine Adresse im Wertespeicher. Diese Speicherzelle enth¨ alt bei jedem Zugriff auf einen Namen den aktuellen Wert dieses Namens. 2. Bei einem Funktionsaufruf stellt man zun¨ achst die f¨ ur die Abarbeitung des Funktionsrumpfes relevanten Namen fest. Die ’alten’ Werte dieser Namen in den zugeh¨ origen Speicherzellen werden in einen seperaten Keller gerettet und danach die ’neuen’ Werte in die Speicherzellen eingetragen. Der Rumpf der aufgerufenen Funktion wird anschließend abgearbeitet. 3. Bei einem Funktionsende werden die ’neuen’ Werte im Speicher wieder durch die ’alten’ Werte, die stets als oberster Block im Keller stehen, ersetzt. 4.1.3 Optimierung von einfachen Postrekursionen F¨ ur das oben beschriebene Shallow-Binding-Konzept wurden eine Reihe von Optimierungen entwickelt. Allen gemeinsam ist die Absicht, ein unn¨otiges Retten von alten Werten zu vermeiden. Dies kann immer dann unterbleiben, wenn ein Wert bei der weiteren Interpretation nicht mehr ben¨otigt wird. Eine derartige Situation ist z.B. bei einfachen Postrekursionen gegeben. Definition 4.1-1 Eine rekursive Funktion f = λv .r heißt einfache Postrekursion genau dann, wenn entweder a) r ein rekursiver Aufruf f (a) ist
oder
b) r eine Bedingung ur mindestens ein ei , a) oder b) entspreCond(p1 , e1 ) . . . (pn , en ) ist und f¨ chend gilt. Cond(p1 , e1 ) . . . (pn , en ) entspricht dem bedingten LISP-Ausdruck [p1 → e1 ; . . . ; pn → en ] aus Kap. 3.2.2.5. Der rekursive Aufruf f (a) heißt einfacher postrekursiver Aufruf. Ist bei der Interpretation der einfache postrekursive Aufruf f (a) abgearbeitet, so ist auch der Rumpf r vollst¨ andig interpretiert. Das zu r geh¨orende alte Environment (siehe Kap. 3.2.5) wird nicht mehr ben¨otigt. Somit l¨aßt sich zeigen (Felgentreu 1984):
258
4 Implementierungstechniken
Satz 4.1-1 Bei einem einfachen postrekursiven Aufruf ist eine Rettung der alten Werte in den Datenkeller nicht notwendig. Beispiele 4.1-2 1) Die Funktion last = λx.Cond(null(cdr(x)), car(x)) (T, last(cdr(x))) mit der Hilfsfunktion null aus Kap. 3.2.2.11 ist eine einfache Postrekursion. 2) Die Funktionen equal und member aus Kap. 3.2.2.11 sind einfache Postrekursionen. Das Erkennen eines einfachen postrekursiven Aufrufs kann mit Hilfe des Datenkellers erfolgen. Zu Beginn der Interpretation von r wird in den Datenkeller der momentane Wert von f geschrieben. Danach wird an den in Definition 4.1-1 genannten Stellen die aufgerufene Funktion mit diesem obersten Eintrag ¨ im Datenkeller verglichen. Bei Ubereinstimmung liegt ein einfacher postrekursiver Aufruf vor. Beispiel 4.1-3 Gegeben sei die Funktion f = λv .Cond(p1 , e1 ) . . . (pi , ei ) . . . (pk , ek ) und ein Aufruf f (a). Wie bisher werden zu Beginn der Abarbeitung dieses Aufrufs die alten Werte in den Datenkeller gerettet und die neuen Werte im Wertespeicher eingetragen. Die Belegungen von Wertespeicher und Datenkeller seien jetzt .. . f : f = λv .Cond . . . 2 .. . Wertespeicher
.. . Datenkeller
Der vollst¨ andige Eintrag bei dynamischer Variablenbindung ist f = λv .Cond(p1 , e1 ) . . . (pi , ei , ) . . . (pk , ek ) und bei statischer Variablenbindung f = λv .Cond(p1 , e1 ) . . . (pi , ei , ) . . . (pk , ek ), (vf 1 , wf 1 ) . . . (vf 1 , wf 1 ).
4.1 Interpretierer
259
Zu Beginn der Abarbeitung des Rumpfes r wird zus¨atzlich der momentane Wert von f als oberster Eintrag im Datenkeller gespeichert: .. . f : f = λv .Cond . . . .. .
−→
f = λv .Cond . . . .. . Datenkeller
Wertespeicher
Jetzt erfolgt die Abarbeitung von Cond, d.h. die Pr¨adikate pj , werden sukzessive ausgewertet. Treten hierbei neue Funktionsaufrufe auf, so bewirken sie neue Eintr¨ age im Datenkeller. Ist ein pj vollst¨andig ausgewertet, so ist jedoch f = λv .Cond wieder oberster Eintrag im Datenkeller: Auswerten der pj f = λv .Cond . . . .. .
.. . f : f = λv .Cond . . . .. . Wertespeicher
Datenkeller
Sei pi das erste Pr¨ adikat, welches den Wert T (true) liefert. Der Wert von r ist dann der Wert von ei . Sei ei die Applikation einer Nicht-Standardfunktion (Nicht-Basisfunktion), also e = f ”(b) . Der Inhalt der zu f ”geh¨ orenden Zelle im Wertespeicher wird mit dem obersten Eintrag im Datenkeller verglichen. Genau dann, wenn beide gleich sind, handelt es sich bei ei um einen einfachen postrekursiven Aufruf, d.h. das f ”entpuppt sich als ein f : f = λv .Cond(p1 , e1 ) . . . (pi , f (b)) . . . (pk , ek ) .. . f : f = λv .Cond . . . .. . Wertespeicher
−→
f = λv .Cond . . . .. . Datenkeller
Danach wird der oberste Eintrag im Datenkeller gel¨oscht. Die f¨ ur ei relevanten neuen Werte k¨ onnen im Wertespeicher eingetragen werden, ohne daß die alten Werte vorher in den Datenkeller gerettet werden. Diese Optimierung von einfachen Postrekursionen l¨aßt sich sowohl bei dynamischer als auch bei statischer Variablenbindung anwenden. Abschließend sei bemerkt, daß bei statischer Variablenbindung durch diese Technik beim
260
4 Implementierungstechniken
Aufruf einfacher postrekursiver Funktionen auch die Kelleroperationen f¨ ur die freien Variablen (s. Kap. 4.l.2) eingespart werden, d.h. der Mehraufwand f¨ ur statische Variablenbindung verschwindet hiermit. 4.1.4 Optimierung von verdeckten Postrekursionen Betrachtet man den LISP-Interpretierer aus Kap. 3.2.2.12 als ein typisches applikatives Programm, so findet man neben einfachen Postrekursionen wie bei evcon rekursive Aufrufe, die keine einfachen postrekursiven Aufrufe sind, weil sie als Argumente von Standardfunktionen auftreten. Die einfachen postrekursiven Aufrufe werden lediglich durch Aufrufe von Standardfunktionen u ur sind evlis und pairlis. Derartige Situationen ¨berdeckt. Beispiele hierf¨ k¨ onnen ¨ ahnlich wie einfache Postrekursionen behandelt werden. Definition 4.1-2 Eine rekursive Funktion f = λv .r heißt verdeckte Postrekursion, wenn f¨ ur r einer der folgenden F¨alle zutrifft: a) r ≡ sn (r1 , . . . , rn ), sn ist der Identifikator einer n-stelligen Standardfunktion, und f¨ ur rn gilt entweder a) oder b) aus Definiton 4.1-1 oder a) oder b) oder b) r ≡ Cond(p1 , e1 ) . . . (pn , en ) , und f¨ ur mindestens ein ei , gilt a) oder b). Der rekursive Aufruf f (a) heißt verdeckter postrekursiver Aufruf, und jedes sn aus a) eine (in diesem Kontext) f (a) verdeckende Standardfunktion ist. Beispiele: 4.1-4 1) Die Funktionen evlis und pairlis aus Kap. 3.2.2.12 sind verdeckte Postrekursionen. 2) Die Funktionen subst und append aus Kap. 3.2.2.11 sind verdeckte Postrekursionen. Nach der Abarbeitung des postrekursiven Aufrufs f (a) m¨ ussen lediglich noch die f (a) verdeckenden Standardfunktionen ausgef¨ uhrt werden, bevor der Rumpf r vollst¨ andig interpretiert ist. Hierf¨ ur wird jedoch nur der Wert von f (a) ben¨ otigt sowie die (bereits berechneten) Werte der evtl. u ¨brigen Standardfunktionsargumente. Somit l¨ aßt sich zeigen (Felgentreu 1984): Satz 4.1-2 Bei einem verdeckten postrekursiven Aufruf ist eine Rettung der alten Werte in den Datenkeller nicht notwendig.
4.1 Interpretierer
261
Beim Beweis ist wesentlich, daß bei mehrstelligen Standardfunktionen der verdeckte postrekursive Aufruf in dem bzgl. der Reihenfolge der Abarbeitung letzten Argument auftritt. In Definition 4.1-2 wurde hierbei die u ¨bliche Reihenfolge der Abarbeitung von links nach rechts unterstellt. Tritt ein rekursiver Aufruf z.B. in dem vorletzten Argument auf, so wird u.U. bei der Abarbeitung des letzten Arguments ein Wert des alten Environments ben¨otigt. Deshalb ist in einem derartigen Fall die Rettung der alten Werte notwendig; ein solcher rekursiver Aufruf ist daher nicht verdeckt postrekursiv. Zur Erkennung von verdeckten Postrekursionen m¨ ussen zun¨achst die den rekursiven Aufruf verdeckenden Standardfunktionsidentifikatoren sowie die Werte ihrer ersten n-1 Argumente abgespeichert werden. Da das Erkennen der Rekursionen wie bei einfachen Postrekursionen mit Hilfe des Datenkellers erfolgen soll, benutzt man hierzu zweckm¨ aßigerweise den zus¨atzlich vorhandenen Keller f¨ ur R¨ uckkehradressen. Anschließend stellt man wie in Kap. 4.1.3 fest, ob im letzten Argument eine verdeckte Postrekursion vorliegt. Falls dies der Fall ist, brauchen die alten Werte vor Abarbeitung der verdeckten Postrekursion nicht gerettet zu werden. Der Wert des verdeckten postrekursiven Aufrufs wird bestimmt und danach die im Keller f¨ ur R¨ uckkehradressen abgespeicherten Standardfunktionen sukzessive ausgef¨ uhrt. Danach ist der Rumpf r vollst¨ andig interpretiert. Beispiel 4.1-5 Gegeben sei die Funktion f = λv .Cond(p1 , cdr(car(f (b)))) (p2 , B) und ein Aufruf f (a). Zu Beginn der Abarbeitung dieses Aufrufs werden die alten Werte in den Datenkeller gerettet und die neuen Werte im Wertespeicher eingetragen. Die Belegungen des Wertespeichers und der beiden Keller seien jetzt: .. . f : f = λv .Cond . . . .. . Wertespeicher
.. .
.. .
Datenkeller
Keller f¨ ur R¨ uckkehradressen
Da der Rumpf von f eine Bedingung ist - ersichtlich an Cond - , kann eine Postrekursion vorliegen. Daher wird zu Beginn der Abarbeitung des Rumpfes r der momentane Wert von f als oberster Eintrag im Datenkeller gespeichert:
262
4 Implementierungstechniken
.. . f : f = λv .Cond . . . .. . Wertespeicher
f = λv .Cond . . . .. .
.. .
Datenkeller
Keller f¨ ur R¨ uckkehradressen
Jetzt wird das erste Pr¨ adikat p1 ausgewertet. Hierbei kann es u.U. zu weiteren Eintr¨ agen im Datenkeller bzw. im Keller f¨ ur R¨ uckkehradressen kommen, die jedoch nach der Auswertung von p1 wieder gel¨oscht sind:
.. . f : f = λv .Cond . . . .. . Wertespeicher
Auswertung von p1 f = λv .Cond . . . .. . Datenkeller
Auswertung von p1 .. . Keller f¨ ur R¨ uckkehradressen
Der Wert von p1 sei T . Der Wert des Aufrufs f (a) ist der Wert von e1 . Da e1 mit dem Standardfunktionsidentifikator cdr beginnt, kann eine verdeckte Postrekursion vorliegen. Der Identifikator cdr wird im Keller f¨ ur R¨ uckkehradressen gespeichert, der Datenkeller bleibt unver¨andert: .. . f : f = λv .Cond . . . .. . Wertespeicher
f = λv .Cond . . . .. . Datenkeller
cdr .. . Keller f¨ ur R¨ uckkehradressen
Das Argument von cdr beginnt wieder mit einem Standardfunktionsidentifikator. Auch er wird im Keller f¨ ur R¨ uckkehradressen gespeichert:
.. . f : f = λv .Cond . . . .. . Wertespeicher
f = λv .Cond . . . .. . Datenkeller
car cdr .. . Keller f¨ ur R¨ uckkehradressen
Das Argument von car ist ein Aufruf einer Nicht-Standardfunktion. Die aufgerufene Funktion wird bestimmt und mit dem obersten Eintrag im Datenkeller verglichen:
4.1 Interpretierer
263
f = λv .Cond(p1 , cdr(car(f (b)))) (p2 , B)
.. . f : f = λv .Cond . . . .. .
f = λv .Cond . . . .. . Datenkeller
Wertespeicher
car cdr .. . Keller f¨ ur R¨ uckkehradressen
Genau dann, wenn die beiden Eintr¨ age gleich sind, liegt eine verdeckte Postrekursion vor. Der verdeckte postrekursive Aufruf f (b) wird nun wie in Kap. 4.1.3 interpretiert, auf das Ergebnis die Standardfunktion car angewandt und car im Keller f¨ ur R¨ uckkehradressen gel¨ oscht: .. . f : f = λv .Cond . . . .. .
f = λv .Cond . . . .. .
Wertespeicher
Datenkeller
cdr .. . Keller f¨ ur R¨ uckkehradressen
Ebenso wird die Standardfunktion cdr ausgef¨ uhrt und cdr im Keller f¨ ur R¨ uckkehradressen gel¨ oscht. Damit ist der Wert des Aufrufs f (a) bestimmt, und der Rumpf r ist vollst¨ andig abgearbeitet. Auch die Optimierung von verdeckten Postrekursionen kann sowohl bei dynamischer als auch bei statischer Variablenbindung vorgenommen werden. Wie in Kap 4.1.3 sei darauf hingewiesen, daß bei statischer Variablenbindung durch diese Technik beim Aufruf verdeckter postrekursiver Funktionen auch die Kelleroperationen f¨ ur die freien Variablen vf (siehe Kap. 4.1.2) eingespart werden, d.h. der Mehraufwand f¨ ur statische Variablenbindung verschwindet nun auch hier. Abschließend sei noch bemerkt, daß der oben beschriebene Erkennungsalgorithmus auch sogenannte formale (verdeckt) postrekursive Aufrufe von f erfaßt, also Aufrufe der Form x(b), die sich von (verdeckt) postrekursiven Aufrufen der Funktion f nur dadurch unterscheiden, daß die Variable x erst bei der Interpretation zu (einer Closure) der Funktion f evaluiert (siehe Felgentreu 1986a). Effizienz der Optimierungen Legt man, wie in Felgentreu (Felgentreu 1984), eine fiktive Maschine zugrunde, die zur Ausf¨ uhrung der Operation cons 4 Zeiteinheiten (ZE) ben¨otigt, zur Ausf¨ uhrung einer push- bzw. pop-Operation 2 Zeiteinheiten und zur Ausf¨ uhrung von car, cdr, atom, eq bzw. jump- und move-Operationen je eine Zeiteinheit ben¨ otigt, so ergeben sich durch die obigen Optimierungen im
264
4 Implementierungstechniken
Fall von statischer Variablenbindung die folgenden Interpretationszeiten: Aufruf
Shallow-
Optimierung von einfachen verdeckten Binding Postrekursionen Postrekursionen einer 23n + 22k 23n + 22k 23n + 22k rekursiven Fkt +95 ZE +112 ZE +112 ZE einfachen 23n + 22k 11n + 26 ZE 11n + 26 ZE postrekursiven +95 ZE Fkt. verdeckt 23n + 22k 23n + 22k 11n + 26 ZE postrekursiven +95 ZE +112 ZE Fkt. Hierbei bedeutet n die Anzahl der Variablen und k die Anzahl der freien Variablen der jeweils aufgerufenen Funktion. Zus¨atzlich wird bei einer Optimierung von (verdeckten) Postrekursionen insgesamt weniger Speicherplatz ben¨ otigt: Bei jeder Ausf¨ uhrung eines (verdeckt) postrekursiven Aufrufs werden 2n + 2k + 4 Kellerzellen eingespart. Standardwerke u ¨ber Interpretierer sind Allen (Allen 1978) sowie Steele (Steele 1978). Ein Shallow-Binding- Interpretierer f¨ ur LISP 1.5 ist in Baker (Baker 1978) angegeben. Eine Beschreibung der Optimierung von einfachen Postrekursionen im Falle von dynamischer Variablenbindung findet sich in Greussay (Greussay 1978) und Perrot (Perrot 1978). Statische Variablenbindung und die Optimierung von verdeckten Postrekursionen werden in Felgentreu (Felgentreu 1984), Felgentreu (Felgentreu 1986 a) und Felgentreu (Felgentreu 1978) behandelt. In Felgentreu (Felgentreu 1984) finden sich auch die Beweise zu den S¨ atzen 4-1.1 und 4-1.2. Die Optimierung weiterer Arten von Postrekursionen (z.B. wechselseitige Postrekursionen) wird f¨ ur dynamische Variablenbindung in Saint-James (Saint-James 1984) untersucht. Den bisher behandelten Techniken ist gemeinsam, daß die optimierbaren Funktionsaufrufe erst w¨ ahrend der Interpretation unmittelbar vor ihrer Ausf¨ uhrung als optimierbar erkannt werden ( dynamische Erkennung“). Dem” gegen¨ uber ist in Felgentreu (Felgentreu 1986 b) eine Technik der statischen Erkennung beschrieben: Dort wird die Klasse der sogenannten Low Cost Calls ( LCCs“) definiert, ” eine Klasse von Funktionsaufrufen, die alle postrekursive und verdeckt postrekursive Aufrufe enth¨ alt, und dar¨ uber hinaus bisher nicht betrachtete Aufrufe erfaßt, wie z.B. geschachtelte Rekursionen (Ackermann-Funktion!), rekursive Aufrufe in Bedingungen und eine Vielzahl nicht-rekursiver Aufrufe. Die S¨atze 4.1-1 und 4.1-2 gelten entsprechend f¨ ur LCCs, d.h. jeder Low Cost Call ist im obigen Sinne optimierbar (Felgentreu 1986 b). In Felgentreu (1986 c) wurde außerdem gezeigt, daß mit einer geringf¨ ugigen Modifikation von Shallow-Binding, dem Standardisierten Shallow-Binding, alle Low Cost Calls
¨ 4.2 Ubersetzer
265
in einem gegebenen Programm bereits vor Beginn der Interpretation auf sehr einfache Weise erkannt werden k¨ onnen. Durch entsprechende Markierungen im Programmbaum weiß der Interpretierer dann w¨ahrend der Abarbeitung stets, welche Aufrufe Low Cost Calls sind und ohne Retten der alten Werte ausgef¨ uhrt werden d¨ urfen. Die Kombination von Standardisiertem ShallowBinding und LCC-Optimierung erbringt gegen¨ uber den oben beschriebenen Techniken noch einen betr¨ achtlichen Gewinn an Interpretationszeit und vor allem an Speicherplatz (siehe Beckmann 1987). Eine ausf¨ uhrliche Darstellung der LCC-Technik w¨ urde den Rahmen dieses Kapitels sprengen; es sei deshalb auf die zitierten Arbeiten verwiesen.
¨ 4.2 Ubersetzer 4.2.1 Einleitung W¨ ahrend die Eingabe f¨ ur einen Interpretierer aus Programm und Daten be¨ steht, ist die Eingabe f¨ ur einen Ubersetzer (Compiler) lediglich das Programm selbst. Ein Interpretierer liefert als Ausgabe das Ergebnis der Applikation des ¨ Programms auf die Daten, ein Ubersetzer liefert als Ausgabe ein semantisch aquivalentes Programm in einer anderen Spraache, den sogenannten Code. ¨ ¨ Die Ubersetzung eines Programms ist daher eine rein statische Programmtransformation. Der erzeugte Code ist in den meisten F¨ allen nicht direkt auf der Zielmaschine ablauff¨ ahig. Zur Behandlung von dynamischen Programmstrukturen ben¨ otigt der erzeugte Code die Unterst¨ utzung eines Laufzeitsystems. Trifft man bei der Codeerzeugung auf eine Programmstruktur, die statisch nicht vollst¨ andig u ¨bersetzt werden kann, da sie von den aktuellen Eingabedaten abh¨ angt, so erzeugt man als Code einen Unterprogrammsprung in das Laufzeitsystem. Als Argumente u ugba¨bergibt man die momentan (statisch) verf¨ ren Informationen. Bei der Ausf¨ uhrung des Codes erfolgt an dieser Stelle der Sprung in das Laufzeitsystem, welches die dynamische Behandlung durchf¨ uhrt und anschließend daf¨ ur sorgt, daß an derjenigen Stelle des Codes fortgefahren wird, an der der Sprung in das Laufzeitsystem erfolgte. ¨ Die Ubersetzung funktionaler bzw. applikativer Sprachen unterscheidet ¨ sich von der Ubersetzung imperativer Sprachen im wesentlichen durch eine andere Organisation des Laufzeitsystems, bedingt durch die h¨oheren Funktionale. Zu denjenigen dynamischen Strukturen, die eine Unterst¨ utzung durch ein Laufzeitsystem ben¨ otigen, geh¨ oren auch Funktionsapplikationen. Bei ALGOL-¨ ahnlichen Programmiersprachen, die nur funktionale Argumente, aber keine funktionalen Ergebnisse zulassen, kann die Organisation des Laufzeitspeichers bei Verwendung einer Display - Technik u ¨ber einen reinen Kellermechanismus erfolgen (deletion - Strategie), wie sie z.B. schon in Grau (Grau
266
4 Implementierungstechniken
1967) verwendet wurde. Beim Aufruf einer Funktion t¨atigt man entsprechende Eintr¨ age im Keller, die wieder gel¨ oscht werden, wenn die Ausf¨ uhrung der Funktion beendet ist. In einer Reihe von Untersuchungen, z.B. Berry (Berry 1971), Johnston (Johnston 1971), Simon (Simon 1976) wurde nachgewiesen, daß bei funktionalen Ergebnissen eine einfache kellerartige Organisation des Laufzeitspeichers nicht mehr m¨ oglich ist. Man muß hier zur weniger eleganten retention - Strategie u ugung gestellter Speicherplatz nicht ¨bergehen, bei der einmal zur Verf¨ mehr freigegeben wird. Anstelle eines Kellers (stack) verwendet man eine Halde (heap). Dies erfordert gegebenenfalls spezielle SpeicherbereinigungsRoutinen, wie sie unter dem Begriff garbage collection“ bekannt sind. Aus ” diesen Gr¨ unden erfolgt auch keine vollst¨ andige Behandlung von h¨oheren Funktionalen durch die Compiler der einzelnen LISP-Systeme; deren Behandlung erfolgt durch den entsprechenden Interpretierer. In Honschopp (Honschopp 1983 a,b) wurde jedoch gezeigt, daß man auch im Fall von funktionalen Ergebnissen zu einer wie bei ALGOL-Systemen u ¨blichen kellerartigen Organisation des Laufzeitspeichers gelangen kann. Dieser scheinbare Widerspruch zu den oben zitierten Ergebnissen liegt in einer unterschiedlichen Auffassung u uhrung einer ¨ber den Zustand, mit dem die Ausf¨ Funktionsapplikation beendet wird. Dies sei an der Funktionsdefinition g = λy.λx.cons(x, y) und dem Aufruf g (A) (((A.B).C)) erl¨ autert: F¨ ur diesen Aufruf der Funktion g wird im Laufzeitkeller zun¨achst ein Display D1 angelegt. In ihm steht u.a. die Information, daß der aktuelle Wert der Variablen y das Atom A ist. Danach wird der Rumpf ausgewertet. Da im Rumpf von g keine Applikation auftritt, erh¨ alt man als Ergebnis die Funktion λx.cons(x, y). Sieht man mit dem Erhalt dieses Ergebnisses die Ausf¨ uhrung des Aufrufs von g als beendet an, so wird gem¨ aß deletion-Strategie das Display oscht. F¨ ur den nun entstandenen Aufruf λx.cons(x, y)((A.B).C) D1 wieder gel¨ wird ein neues Display D2 angelegt. In ihm steht u.a. die Information, daß der aktuelle Wert der Variablen x der Ausdruck ((A.B).C) ist. Um den Rumpf auszuwerten muß cons(x, y) berechnet werden. Der Wert von x ist in D2 gespeichert und steht daher zur Verf¨ ugung; der Wert von y dagegen war in D1 gespeichert und ist mit dem L¨ oschen von D1 verloren gegangen. In Honschopp (Honschopp 1983 a,b) wird dagegen die Ausf¨ uhrung des Aufrufs von g erst dann als beendet angesehen, wenn die Ausf¨ uhrung des Aufrufs λx.cons(x, y) ((A.B).C) beendet ist. Damit wird D1 nicht gel¨oscht und sowohl der Wert von x als auch der Wert von y stehen zur Verf¨ ugung. Somit kann cons(x, y) berechnet werden und man erh¨ alt (((A.B).C).A) . Die Ausf¨ uhrung von λx.cons(x, y) ((A.B).C) ist beendet und es wird zun¨ achst D2 und danach D1 gel¨oscht.
¨ 4.2 Ubersetzer
267
In den folgenden beiden Abschnitten wird dieses Laufzeitsystem und eine m¨ ogliche Optimierung ausf¨ uhrlicher beschrieben. 4.2.2 Ein Laufzeitsystem mit kellerartiger Speicherplatzverwaltung ¨ Der in Honschopp (Honschopp 1983 a,b) beschriebene Ubersetzer wurde f¨ ur die experimentelle Sprache LISP/N (Lippe 1979) entwickelt. Seine Prinzipien sind jedoch genereller Natur und f¨ ur alle funktionalen und applikativen Sprachen verwendbar. LISP/N besitzt die gleiche Datenstruktur (S-Ausdr¨ ucke) und die gleichen Standardfunktionen (Basisfunktionen) wie Pure-LISP. Die syntaktische Struktur wurde jedoch der von ALGOL-¨ahnlichen Programmiersprachen angepaßt. Im Unterschied zu LISP besitzt LISP/N in Anlehnung an ALGOL-60 eine ’call by name’-Parameter¨ ubergabe. Alle im folgenden aufgef¨ uhrten Techniken lassen sich ohne Schwierigkeiten auf einen ’call by value’-Mechanismus u ur LISP/N wurde mit Hilfe ei¨bertragen. Die Semantik f¨ ner Kopierregelsemantik definiert. Als Ausgangspunkt diente die ALGOL-60Kopierregel (Naur 1963), die jedoch wegen des vollen funktionalen Konzepts von LISP/N entsprechend modifiziert wurde. Eine ausf¨ uhrliche Beschreibung der Vorgehensweise findet sich in Lippe (Lippe 1980). Wir beschr¨anken uns hier auf die f¨ ur die Laufzeitorganisation wesentliche Angabe der Kopierregel. Anstelle von LISP/N wird hierbei die Notation aus Kapitel 4.1 zugrunde gelegt. Um die weiteren Ausf¨ uhrungen u ¨bersichtlicher zu gestalten, setzen wir voraus, daß alle Nicht-Standardfunktionen benannt sind, Zugriffe auf Funktionen nur u ¨ber den Funktionsidentifikator erfolgen und als Argumente keine bedingten Ausdr¨ ucke auftreten. Diese letzte Einschr¨ ankung ist nicht wesentlich, da ein bedingter Ausdruck τ in einem Aufruf f (. . . , τ, . . . ) wie f (. . . , h(), . . . ) mit der zus¨ atzlichen variablenfreien Hilfsfunktion h = τ u ¨bersetzt wird. Definition 4.2-1 (Kopierregel f¨ ur Programme mit h¨ oheren Funktionalen) Sei π ein korrektes Programm der Form π : . . . f = λx1 , . . . xn .R . . . f (ao1 , . . . , aon ) . . . (ar1 , . . . , arm ) . . . Das semantisch ¨ aquivalente Programm π entsteht aus π durch Anwendung der Kopierregel (π π), indem der Aufruf f (a1 , . . . , an ) . . . (a1 , . . . , am ) in π durch den modifizierten Rumpf R der aufgerufenen Funktion f ersetzt wird: π : . . . f = λx1 , . . . xn .R . . . f (ao1 , . . . , aon ) . . . (ar1 , . . . , arm ) . . . R ... π : . . . f = λx1 , . . . xn .R . . . Die Modifikationen, denen R unterworfen wird, sind
268
4 Implementierungstechniken
1. Substitution Alle Variablen xi , die in R auftreten, werden durch das entsprechende Argument aoi ersetzt. 2. Umbenennung Alle Identifikatoren, die in R selbst gebunden sind (lokale Identifikatoren), werden eindeutig umbenannt. 3. Argument-Weitergabe Im Fall r > 0 wird ein unbedingter Ausdruck R zu R(a11 , . . . , a11 ) . . . (ar1 , . . . , arm ) und ein bedingter Ausdruck Cond(p1 , e1 ) . . . (pk , ek ) zu Cond(p1 , e1 (a11 , . . . , a11 ) . . . (ar1 , . . . , arm )) ... (pk , ek (a11 , . . . , a11 ) . . . (ar1 , . . . , arm )) erweitert. Bemerkungen: 1. Der etwas vage Begriff korrektes Programm“ wurde gew¨ahlt um anzu” deuten, daß u.a. das Programm syntaktisch korrekt, jeder Identifikator ¨ eindeutig definiert und jede Anwendung eines Identifikators in Ubereinstimmung mit seiner Definition erfolgt. Eine exakte Definition findet man unter dem Begriff ’¨ ubersetzbares Programm’ in Lippe (Lippe 1979). 2. Diese Kopierregel entspricht im wesentlichen der β-Reduktion des LambdaKalk¨ uls (Definition 2.2-6). Die Behandlung der zus¨atzlichen Argumentgruppen ist lediglich aus syntaktischen Gr¨ unden etwas komplizierter (bedingte Ausdr¨ ucke). 3. Die Umbenennung vermeidet Namenskonflikte (siehe Bemerkungen zu Definiton 2.2-4). Sie entspricht einer α-Reduktion. Da sie generell f¨ ur alle lokalen Identifikatoren vorgeschrieben ist, ist die Kopierregel nicht so differenziert, wie es die β-Reduktionsregel durch 4. in Definition 2.2-4 ist. 4. Man erkennt, wie durch die Weitergabe der zus¨atzlichen Argumentgruppen funktionale Ausdr¨ ucke zu Applikationen werden. 5. Der Parameter¨ ubergabemechanismus beinhaltet ’call by name’. Die Ko¨ pierregel l¨ aßt sich jedoch auch f¨ ur andere Ubergabemechanismen modifizieren, Standardfunktionen wie cons werden jedoch mit ’call by value’ behandelt. 6. Streicht man aus der Definition alles was sich auf die zus¨atzlichen Argumentgruppen bezieht, so erh¨ alt man exakt die ALGOL-60-Kopierregel aus Naur (Naur 1963). 7. Hat man nicht die oben vorgenommenen Einschr¨ankungen, so k¨onnen auch Aufrufe der Art λx.r(ao ) . . . (ar ) auftreten. Die Kopierregel ist in diesem Fall sinngem¨ aß zu erweitern: Der Aufruf wird ersetzt durch den modifizierten Rumpf R, wobei die Modifikationen mit den unter 1. - 3. angegebenen identisch sind. Ein Beispiel, in dem sowohl diese Situation als auch diejenige aus Definition 4.2-1 auftritt, ist Beispiel 4.2-1.
¨ 4.2 Ubersetzer
269
Ein Aufruf λx.R(ao ) . . . (ar ) wird wie eine Definition λx.R zusammen mit dem Aufruf f (ao ) . . . (ar ) behandelt. Dies garantiert, daß die Funktionsdefinition erhalten bleibt und ein (rekursiver) Zugriff auf f in R wohldefiniert ist. Rekursion l¨ aßt sich unmittelbar syntaktisch ausdr¨ ucken. Beispiel 4.2-1 Gegeben sei die Funktion twice mit twice = λf.λx.f (f (cons(x, D))) und der Aufruf twice (car) ((A.B).C) . Durch zweimalige Anwendung der Kopierregel erh¨alt man . . . twice = λf.λx.f (f (cons(x, D))) . . . twice(car)(((A.B).C) . . . . . . twice = λf.λx.f (f (cons(x, D))) . . . λx.car(car(cons(x.D))) (((A.B).C)) . . . . . . twice = λf.λx.f (f (cons(x, D))) . . . car(car((((A.B).C).D))) . . . Die zweimalige Ausf¨ uhrung der Standardfunktion car liefert . . . twice = λf.λx.f (f (cons(x, D))) . . . (A.B) . . . F¨ ur den folgenden Abschnitt gehen wir davon aus, daß die Display-Technik im Prinzip bekannt ist. Bei der Codeerzeugung werden, im Zusammenhang mit Funktionen, bei den folgenden Situationen Aufrufe des Laufzeitsystems erzeugt. Hierbei bedeutet f ein Funktionsidentifikator und x eine Variable: 1. ’Einfache’ Funktionsaufrufe f (a) bzw. x(a) 2. Funktionsaufrufe mit zus¨ atzlichen Argumentgruppen f (ao ) . . . (ar ) bzw. x(ao ) . . . (ar ) 3. Funktionale Ausdr¨ ucke f bzw. x 4. Ende einer Funktion Gegen¨ uber ALGOL-¨ ahnlichen Sprachen treten zus¨atzlich die Situationen 2. und 3. auf. Bei Verwendung der Display-Technik wird bei einem Laufzeitsystem f¨ ur ALGOL-¨ ahnliche Sprachen ein Laufzeitkeller verwaltet. Bei einem Funktionsaufruf (bzw. Prozeduraufruf) werden auf oberstem Kellerniveau alle die Daten eingetragen, die notwendig sind, um die Werte aller Identifikatoren zu bestimmen, die im Rumpf der Funktion auftreten. Diesen Eintrag nennt man Activation-Record (AR), Display oder Festspeicherblock. Bei einem Funktionsende wird das jeweils oberste ActivationRecord wieder gel¨ oscht. Der prinzipielle Aufbau eines Activation-Records, der bei ALGOL-¨ ahnlichen Sprachen f¨ ur einen Aufruf f (a1 , . . . , an ) angelegt wird, sieht folgendermaßen aus:
270
4 Implementierungstechniken
Laufzeitkeller
Code R¨ ucksprungadresse Anfangsadresse des vorhergehenden Activation-Records statisches Niveau von f ein dynamisches Niveau des statischen Vorg¨ angers von f Beginn des freien Speichers (BFS) a1 .. . an Trennzeichen Bereich f¨ ur lokale Identifikatoren und Hilfsvariablen
¨ Eine ausf¨ uhrliche Beschreibung der Ubersetzung von Funktionen bzw. Prozeduren und der zugeh¨ origen Algorithmen finden sich in den zahlreichen ¨ B¨ uchern u z.B. in dem bereits zitierten Grau (Grau 1967). ¨ber Ubersetzerbau, An diesem Laufzeitsystem sind im Fall von Funktionen mit funktionalen Ergebnissen die folgenden Ver¨ anderungen notwendig: 1. Falls bei einem einfachen Funktionsaufruf f (a) die Funktion f ein funktionales Ergebnis besitzt, k¨ onnen zur Laufzeit gem¨aß 3. der Kopierregel weitere Argumentgruppen anfallen, die in das f¨ ur f (a) anzulegende Activation-Record hineinkopiert werden. Diese stehen als ’zus¨atzliche Argumentgruppen’ im unmittelbar vorher angelegten Activation-Record. 2. Bei einem einfachen Funktionsaufruf x(a) ist das zu x geh¨orende Argument aufgrund der Einschr¨ ankungen und der Vorbemerkungen u ¨ber die ¨ Ubersetzung von bedingten Ausdr¨ ucken stets entweder ein Funktionsidentifikator, so daß im wesentlichen 1. gilt, oder ein Aufruf einer Funktion mit funktionalem Ergebnis, so daß im weiteren 4. gilt.
¨ 4.2 Ubersetzer
.. .
Laufzeitkeller
Code
R¨ ucksprungadresse Anfangsadresse des vorhergehenden Activation-Records statisches Niveau von f ein dynamisches Niveau des statischen Vorg¨ angers von f Beginn des freien Speichers (BFS) Beginn der ’zus¨ atzlichen Argumentgruppen’ (BZA) a01 .. . a0n0 a11 .. . a1n1 Trennzeichen a21 .. . a2n2 .. . evtl. weitere Argumentgruppen aus ’zus¨ atzlichen Argumentgruppen des vorher angelegten AR’ .. . Trennzeichen mente’
’Ende
Argu-
Bereich f¨ ur Hilfsvariablen
271
272
4 Implementierungstechniken
3. Funktionale Ausdr¨ ucke werden wie unvollst¨andige Aufrufe ohne Argumente u utzung m¨ ussen die fehlenden ¨bersetzt. Durch Laufzeitunterst¨ Argumentgruppen gem¨ aß 3. der Kopierregel jeweils in das betreffende Activation-Record hineinkopiert werden. 4. Im Fall von Aufrufen mit zus¨ atzlichen Argumentgruppen werden diese zun¨ achst in das Activation-Record kopiert. Dann werden eventuell im unmittelbar vorher angelegten Activation-Record noch vorhandene ’zus¨atzliche Argumentgruppen’ in das Activation-Record kopiert. 5. Die Laufzeitunterst¨ utzung der Situation ’Funktionsende’ kann unver¨andert u ¨bernommen werden. Man erkennt, daß sich die erweiterte Laufzeitunterst¨ utzung f¨ ur Aufrufe mit funktionalen Ergebnissen auf das korrekte Einf¨ ugen von eventuell anfallenden zus¨ atzlichen Argumentgruppen in das betreffende Activation-Record beschr¨ ankt. Im Fall eines Aufrufs f (a01 , . . . , a0n0 )(a11 , . . . , a1n1 )(a21 , . . . , a2n2 ) erh¨ alt man daher das in der Skizze angegebene Activation-Record als Eintrag in den Laufzeitkeller. Mit der hier skizzierten prinzipiellen Organisation l¨aßt sich auch f¨ ur Sprachen mit vollem funktionalen Konzept eine kellerartige Speicherplatzverwaltung erreichen. Bei der praktischen Realisierung wird man einige Ver¨anderungen vornehmen. So ist es z.B. nicht notwendig, in jedem Activation-Record eine Zelle ’Beginn des freien Speichers’ vorzusehen. Hierzu reicht eine einzige Zelle außerhalb des Kellers aus, die jeweils aktuell geladen wird. Falls es von der Maschine her m¨ oglich ist, kann man die statische Verweiskette in Indexregistern ablegen, um einen schnellen Zugriff auf die Activation-Records im Keller zu erm¨ oglichen. 4.2.3 Optimierungen Die oben angegebene Organisation des Laufzeitsystems erlaubt zwar eine kellerartige Verwaltung der einzelnen Activation-Records, bewirkt aber andererseits, daß in vielen F¨ allen nach der Berechnung des Ergebnisses noch eine Reihe von Spr¨ ungen zwischen Laufzeitsystem und Code anfallen, wobei nach jedem Sprung zus¨ atzliche Befehle ausgef¨ uhrt werden. Beispiel 4.2-2 Gegeben sei die Funktion member (siehe Kap. 3.2.2.11) durch member = λxy.Cond(null(y), F ), (equal(x, car(y)), T )(T, member(x, cdr(y)))
¨ 4.2 Ubersetzer
273
und ein Aufruf member(C, (ABC)) Betrachtet man die sukzessiven Ver¨ anderungen im Laufzeitkeller, so erh¨alt man: ARmember(C,(ABC))
ARmember(C,(ABC)) ARmember(C,(BC))
ARmember(C,(ABC))
ARmember(C,(ABC))
ARmember(C,(BC))
ARmember(C,(BC))
ARmember(C,(C))
ARmember(C,(ABC))
Durch den Aufruf member(C, (ABC)) wird das Laufzeitsystem aktiviert und es wird ein Activation-Record angelegt. Im Rumpf der Funktion member erfolgt wieder ein Aufruf, und zwar member(C, (BC)), ohne daß bereits u ¨ber das Funktionsende gelaufen wurde. Somit muß das Activation-Record f¨ ur member(C, (ABC)) im Keller stehen bleiben und das neue Activation-Record wird zugef¨ ugt. Dieser Prozeß geht solange weiter, bis kein neuer Aufruf erfolgt, sondern ein S-Ausdruck abgeliefert wird. Man gelangt nun an das ’Funktionsende’, l¨ oscht das letzte Activation-Record, l¨adt u.U. die Indexregister um und kehrt zur¨ uck gem¨ aß der R¨ ucksprungadresse. Man gelangt wieder an das ’Funktionsende’ und der gleiche Vorgang wiederholt sich. Dies geschieht solange, bis der Laufzeitkeller leer ist. Zusammenfassend kann man u ¨ber das Laufzeitverhalten dieses Programms folgendes sagen: Zun¨ achst wird der Laufzeitkeller aufgebaut, was in Abh¨angigkeit von den Startwerten u.U. sehr lange dauern kann. Dann f¨allt das Ergebnis an. Zum Schluß erfolgt ein aufwendiger Verwaltungsvorgang, bei dem zwischen Laufzeitsystem und Programm hin- und hergesprungen wird. Ferner
274
4 Implementierungstechniken
werden u.U. dauernd Zwischenberechnungen ausgef¨ uhrt, um die Indexregister neu zu laden. Diese Vorg¨ ange haben jedoch mit der eigentlichen Berechnung des Ergebnisses nichts mehr zu tun, denn es steht bereits fest. Es stellt sich somit die Frage, ob sich diese Ineffizienz nicht beseitigen l¨aßt. Die Idee liegt darin, das sukzessive L¨ oschen zu vermeiden. Hierzu muß man sich zun¨ achst u ¨berlegen, welche Informationen aus den einzelnen Speicherbl¨ ocken noch ben¨ otigt werden. In dem obigen Beispiel ben¨ otigt man lediglich noch die R¨ ucksprungadresse. Im allgemeinen ben¨otigt man aus einem Activation-Record neben der R¨ ucksprungadresse nur noch Hilfsvariablen f¨ ur Zwischenresultate, die bei der Auswertung von Applikationen von mehrstelligen Basisfunktionen anfallen. Nur wegen dieser Informationen wird das ganze Activation-Record im Laufzeitkeller gehalten. Das ist un¨ okonomisch. ¨ Wir f¨ uhren daher zun¨ achst die beiden folgenden Anderungen im Laufzeitsystem ein: Ver¨ anderung 1 Speicherung von R¨ ucksprungadressen und Hilfsvariablen in zwei separaten Kellern, in denen nur push und pop als Operationen zul¨assig sind. Ver¨ anderung 2 Verlagerung des L¨ oschvorgangs von der Situation ’Funktionsende’ nach ’Funktionsaufruf’: - Man legt ein neues Activation-Recor zun¨ achst provisorisch an - sucht anhand der von diesem Activation-Record ausgehenden Verweise in dem Keller nach denjenigen Activation-Records mit der geringsten Distanz zur Komponente BFS des zuletzt gekellerten Activation-Records (Spitze des Kellers), - l¨ oscht den Kellerinhalt bis zu dem im vorigen Schritt gefundenen Activation-Record, - speichert das neue Activation-Record im Keller ab. Wendet man diese neue Strategie auf das Beispiel an, so erh¨alt man folgende Belegungen der Keller (Hilfsvariablen treten in diesem Beispiel nicht auf): Laufzeitkeller
Keller f¨ ur R¨ ucksprungadressen (RA)
ARmember(C,(ABC))
RA(C,(ABC))
¨ 4.2 Ubersetzer
ARmember(C,(ABC))
275
RA(C,(ABC)) RA(C,(AC))
ARmember(C,(C))
RA(C,(ABC)) RA(C,(AC)) RA(C,(C))
Durch Ausf¨ uhrung der eingetragenen R¨ uckspr¨ unge gelangt man an das Ende des Programmes. Da weniger AR’s angelegt werden und R¨ ucksprungadressen wenig Platz ben¨ otigen, ist der geringere Speicherbedarf offensichtlich. Allerdings f¨ uhren diese beiden Ver¨ anderungen allein zu einer fehlerhaften Implementierung: Beim Aufruf einer mehrstelligen Basisfunktion f (a1 , . . . , an ) mit Parametern a1 , . . . an kann es vorkommen, daß man als Folge der durch den Parameter a1 eventuel bewirkten Aufrufe auch das Activation-Record derjenigen Funktion p l¨ oscht, in deren Rumpf der betrachtete Aufruf von f steht. Obwohl R¨ ucksprungadresse und Hilfsvariablen ausgelagert sind, kann es nun sein, daß im Laufe der durch die Argumente a2 , . . . , an bewirkten Aufrufe die Argumente von p ben¨ otigt werden und somit AR infolge der Auswertung des assigerweise gel¨ oscht wurde. Parameters a1 unzul¨ Beispiel 4.2-3 Gegeben seien die Funktion p durch p = λx.cons(g(B), g(x)), die Funktion g durch g = λy.y und den Aufruf p(A). Zun¨ achst wird das Activation-Record f¨ ur den Aufruf p(A) angelegt:
276
4 Implementierungstechniken
ARp(A)
Als n¨ achstes wird das Activation-Record, das durch den Aufruf g(B) initiiert wird, provisorisch angelegt. Da von diesem Activation-Record kein Verweis oscht, und an dessen Stelle ARg(B) eingenach ARp(A) zeigt, wird ARp(A) gel¨ tragen. Man erh¨alt den Laufzeitkeller ARg(B)
Danach muß das Activation-Record f¨ ur den Aufruf g(x) angelegt werden. Der aktuelle Wert der Variablen x war im ARp(A) abgelegt. Dieses ActivationRecord wurde (A) jedoch gel¨ oscht und der aktuelle Wert f¨ ur x kann nicht mehr bestimmt werden. Beim Aufruf einer mehrstelligen Basisfunktion darf somit das Activation-Record, das zu dem Aufruf derjenigen Funktion geh¨ort, in deren Rumpf der Aufruf der Basisfunktion steht, fr¨ uhestens bei der Auswertung des letzten Arguments der Basisfunktion gel¨oscht werden. Man hat hier die Analogie zu den verdeckten Postrekursionen (s. Kap. 4.1.4), bei denen die Optimierung auch nur durchgef¨ uhrt werden kann, wenn die Rekursion auf dem letzten Argument einer Basisfunktion auftritt. Das zu fr¨ uhe L¨oschen verhindert man durch das Einf¨ ugen eines weiteren Verweises in einen Activation-Record, den sogenannten ’generalisierten dynamischen Vorg¨ anger’ (GDV): Ver¨ anderung 3 Einf¨ ugen eines Verweises auf den ’generalisierten dynamischen Vorg¨anger’ (GDV): 1. Sei f (a) der Aufruf einer Nicht-Standardfunktion f , deren Rumpf aus dem Aufruf b(τ1 , . . . , τn ) einer n-stelligen Standardfunktion besteht. 2. Zu Beginn der Berechnung eines τi , 1 ≤ i ≤ n, ist GDV ein Verweis auf ARf (a) . Solange kein neuer GDV in Kraft tritt, bleibt w¨ahrend der Berechnung von τ1 , GDV ein Verweis auf ARf (a) . 3. Zu Beginn der Berechnung von τn weist GDV aus das gleiche ActivationRecord, wie die Komponente GDV in ARf (a) . Dies sei BF SAR(f (a)) . Mit dieser zus¨ atzlichen Modifikation ergeben sich f¨ ur Beispiel 4.2-3 die folgenden Belegungen des Laufzeitkellers: Zun¨ achst wird das Activation-Record f¨ ur den Aufruf p(A) angelegt. Man erh¨ alt:
¨ 4.2 Ubersetzer
277
ARp(A)
Danach wird ARg(B) provisorisch angelegt. Da im Rumpf von p der Aufruf der zweistelligen Basisfunktion cons erfolgt, wird in ARg(B) als GDV ein Verweis auf ARp(A) eingetragen. Da jetzt ein Verweis nach ARp(A) zeigt, wird ARp(A) nicht gel¨ oscht und man erh¨ alt ARp(A) ARg(B)
Mit den Ver¨ anderungen 1 - 3 erh¨ alt man nun den folgenden prinzipiellen Aufbau f¨ ur ein Activation-Record:
GDV statisches Niveau ein dynamisches Niveau des statischen Vorg¨ angers Beginn des freien Speichers (BFS) Beginn der ’zus¨ atzlichen Argumentengruppen’ (BZA) .. . Argumentengruppen .. . Freier Speicher
278
4 Implementierungstechniken
Wie bei der urspr¨ unglichen ALGOL-Strategie ben¨otigt man 5 Verwaltungszellen in jedem Activation-Record, da f¨ ur die neuen Eintr¨age GDV bzw. BZA die frei gewordenen Pl¨ atze f¨ ur die R¨ ucksprungadresse bzw. Anfangsadresse des vorhergehenden Activation-Records genutzt werden k¨onnen. Zus¨atzlich existieren noch die beiden Keller f¨ ur R¨ ucksprungadressen bzw. Hilfsvariablen, die jedoch zu einem Keller zusammengelegt werden k¨onnen. Nicht terminierende Rekursionen lassen sich bei herk¨ommlichen Laufzeitsystemen anhand eines Keller¨ uberlaufs feststellen. In dem oben vorgestellten System kann es jedoch passieren, daß der eigentliche Laufzeitkeller nur eine ¨ feste Anzahl von Activation-Records enth¨ alt; der Uberlauf tritt dann aber im Keller der R¨ ucksprungadressen ein. Die Details der Implementierung eines derartigen Laufzeitsystems f¨ ur die Sprache LISP/N auf einer Siemens 7.760 k¨ onnen aus Honschopp (Honschopp 1983a) entnommen werden.
4.3 Hardware – Unterstu ¨ tzte Implementierungen 4.3.1 Einleitung An Stelle einer reinen Imlementierung durch Software kann man auch eine Rechnerarchitektur realisieren, auf der applikative Programme bzw. wesentliche Programmkonstrukte unmittelbar ausgef¨ uhrt werden k¨onnen. Da der Schwerpunkt dieses Buches auf der Programmierung liegt, beschr¨anken wir uns auf eine exemplarische Beschreibung dieses interessanten Gebietes. Eine ausf¨ uhrliche Beschreibung und eine detaillierte Klassifizierung der einzelnen Konzepte findet man in Treleaven (Treleaven 1982). Obwohl die von-Neumann Rechnerarchitektur nicht speziell auf die Anforderungen der funktionalen und applikativen Programmiersprachen ausgelegt ist, kann man damit, wie in den Kapiteln 4.1 und 4.2 gezeigt, durch aus effiziente Implementierungen vornehmen. Von den ersten großen LISP-Systemen auf Großrechnern wie DEC PDP10 bis hin zu den LISP-Maschinen hat man sich, nicht zuletzt aus Gr¨ unden der Verf¨ ugbarkeit, auf diese klassische Rechnerarchitektur bezogen. Alternative Rechnerarchitekturen sind als Forschungsgegenstand erst n¨aher in Betracht gezogen worden, nachdem ihre prinzipielle Realisierbarkeit durch grundlegende technologische und konzeptionelle Neuerungen belegt worden war. Unter anderem versucht man, die Implementierung h¨oherer Programmiersprachen gezielt durch neue Konzepte in der Hardware zu unterst¨ utzen. Bezogen auf funktionale und applikative Programmiersprachen ist also mit einer Aussch¨ opfung aller M¨ oglichkeiten der effizienten Implementierung erst auf speziellen Rechnerarchitekturen zu rechnen. Von besonderem Interesse erscheinen momentan Reduktionsmaschinen und Datenflußmaschinen.
4.3 Hardware – Unterst¨ utzte Implementierungen
279
4.3.1.1 Reduktionsmaschinen Bei Reduktionsmaschinen wird das zu verarbeitende Programm auf Teilterme (Redices) untersucht, die gem¨ aß vorgegebenen Regeln reduzierbar sind. Diese Teilterme werden jeweils durch das Ergebnis ihrer Reduktion ersetzt. In Kapitel 2.2.7 wurde zwischen einer ’call by name’- und einer ’call by value’-Auswertungsstrategie unterschieden. Diese beiden Strategien lassen sich verallgemeinern zu einer anforderungsgesteuerten Berechnung (demand driven computation) und einer datengesteuerten Berechnung (data driven computation). Bei einer anforderungsgesteuerten Berechnung wird eine Operation (Reduktionsschritt) dann ausgef¨ uhrt, wenn ihr Ergebnis von einer anderen, bereits ausgew¨ ahlten Operation, angefordert wird. Bei einer datengesteuerten Berechnung wird eine Operation, unabh¨angig davon, ob ein Ergebnis ben¨ otigt wird, dann berechnet, wenn alle Operanden als Daten vorhanden sind. Realisiert man die Ausf¨ uhrung eines Reduktionsschrittes durch direkte textuelle Substitution, so spricht man von String-Reduktion. Eine andere Vorgehensweise ist die Graph-Reduktion, bei der man die Variablen im Rumpf der aufgerufenen Funktion durch Verweise auf die zugeh¨origen Argumente ersetzt. Beispiel 4.3-1 Gegeben sei die Funktionsapplikation f (3 + 1, 2, λvw.v + w) und die Funktionsdefinition f = λxyz. if x > y then (x − 2) ∗ z(x, y) else y. 1. Anforderungsgesteuerte Berechnung und String-Reduktion Bei einer anforderungsgesteuerten Berechnung und String-Reduktion ergibt sich der folgende Berechnungsablauf: Schritt 1: Durch Einsetzen der Funktionsdefinition f¨ ur f in die Applikation erh¨alt man das Redex λxyz.if x > y then (x − 2) ∗ z(x, y) else y (3 + 1, 2, λvw.v + w). Schritt 2: Das Redex wird reduziert, indem die Variablen x, y, z durch die zugeh¨origen Argumente ersetzt werden: if 3 + 1 > 2 then ((3 + 1) − 2) ∗ λvw.v + w (3 + 1, 2) else 2.
280
4 Implementierungstechniken
Schritt 3: Da der Wert der Funktionsapplikationf (3 + 1, 2, λvw.v + w) verlangt wird, werden die Werte der im Funktionsrumpf neu entstandenen Redizes angefordert. Die Anwendung der Basisfunktion + und die Ersetzung der Variablen v, w durch die zugeh¨ origen Argumente liefert if 4 > 2 then (4 − 2) ∗ ((3 + 1) + 2) else 2. Schritt 4: Zur weiteren Auswertung werden die Ergebnisse der Applikationen von Basisfunktionen ben¨ otigt. Man erh¨ alt if true then 2 ∗ (4 + 2) else 2. Schritt 5: Die jetzt notwendige Auswertung der Bedingung liefert 2 ∗ (4 + 2). Schritt 6: Jetzt wird die Anwendung der Basisfunktion + ben¨otigt. Sie liefert 2∗6 Schritt 7: Nach Anwendung der Basisfunktion ∗ erh¨ alt man das Resultat 12. 2. Datengesteuerte Berechnung und String-Reduktion Bei einer datengesteuerten Berechnung und String-Reduktion ergibt sich der folgende Berechnungsablauf: Schritt 1: Die einzige Applikation, bei der die Argumente als Daten vorliegen, ist das Argument 3 + 1. Die Ausf¨ uhrung liefert f (4, 2, λvw.v + w). Schritt 2: Bei der so erhaltenen Applikation liegen alle Argumente als Daten, n¨amlich 4 und 2 bzw. als ein nicht weiter reduzierbarer Ausdruck λvw.v + w vor und die Applikation wird ausgef¨ uhrt. Zun¨ achst erh¨alt man durch Einsetzen der
4.3 Hardware – Unterst¨ utzte Implementierungen
281
Funktionsdefinition f¨ ur f das Redex λxyz.if x > y then (x − 2) ∗ z(x, y) else y (4, 2, λvw.v + w). Schritt 3: Die Variablen x, y, z werden durch die zugeh¨origen Argumente ersetzt und man erh¨ alt if 4 > 2 then (4 − 2) ∗ λvw.v + w(4, 2) else 2. Schritt 4: F¨ ur 4 > 2, (4 − 2) und λvw.v + w(4 + 2) stehen jetzt die Argumente als Daten zur Verf¨ ugung. Die drei Reduktionsschritte k¨ onnen im Prinzip parallel erfolgen. Ihre Ausf¨ uhrung liefert if true then 2 ∗ (4 + 2) else 2. Schritt 5: Die einzige Applikation mit Daten als Argumente ist (4 + 2). Die Ausf¨ uhrung liefert if true then 2 ∗ 6 else 2. Schritt 6: Entsprechend Schritt 5 wird 2 ∗ 6 ausgef¨ uhrt und man erh¨alt if true then 12 else 2. Schritt 7: Nun liegen auch alle Argumente des bedingten Ausdrucks als Daten vor, und es ergibt sich das Resultat 12. 3. Anforderungsgesteuerte Berechnung und Graph Reduktion Bei einer anforderungsgesteuerten Berechnung und Graph-Reduktion ergibt sich der folgende Berechnungsablauf: Schritt 1: Durch Einsetzen der Funktionsdefinition f¨ ur f in die Applikation erh¨alt man: λxyz.if x > y then (x − 2) ∗ z(x, y) else y (3 + 1, 2, λvw.v + w)
282
4 Implementierungstechniken
Schritt 2: Das Redex wird reduziert, indem die Variablen x, y, z durch Verweise auf die zugeh¨ origen Argumente ersetzt werden: if > then ( − 2) ∗ ( , ) else (3 + 1, 2, λvw.v + w)
Schritt 3: Wie bei Schritt 3 der anforderungsgesteuerten String-Reduktion werden die Werte der im Funktionsrumpf neu entstandenen Redizes angefordert. Es sind der Redex 3 + 1, auf den mehrere Verweise zeigen, und der Redex λvw.v + w(3 + 1, 2). Beide k¨ onnen im Prinzip wieder parallel ausgewertet werden. Man erh¨ alt: if > then ( − 2) ∗ ( + ) else (4, 2, λvw.v + w)
Schritt 4: Jetzt werden die Basisfunktionen >, −, + angewendet. Man erh¨alt: if true then 2 ∗ 6 else (4, 2, λvw.v + w)
Schritt 5: Durch Auswertung des bedingten Ausdrucks ergibt sich 2∗6 Schritt 6: Nach Anwendung der Basisfunktion ∗ erh¨ alt man das Resultat 12. 4. Datensteuerung Berechnung und Graph-Reduktion Bei der datengesteuerten Berechnung und Graph-Reduktion ergibt sich der folgende Berechnungsablauf: Schritt 1: Zun¨ achst wird die Funktionsapplikation 3 + 1 ausgef¨ uhrt. Man erh¨alt wie bei der datengesteuerten String-Reduktion f (4, 2, λvw.v + w).
4.3 Hardware – Unterst¨ utzte Implementierungen
283
Schritt 2: Durch Einsetzen der Funktionsdefinition entsteht der Redex λxyz.if x > y then (x − 2) + z(x, y) else y (4, 2, λvw.v + w). Schritt 3: Beim Reduzieren werden die Variablen durch Verweise auf die entsprechenden Argumente ersetzt: if > then ( − 2) ∗ ( , ) else (3 + 1, 2, λvw.v + w)
Schritt 4: Sowohl f¨ ur die Basisfunktionen >, − als auch f¨ ur die Funktion λvw.v+w stehen die Argumente als Daten bzw. als nicht weiter reduzierbare Ausdr¨ ucke zur Verf¨ ugung. Die Anwendung dieser Funktionen ist im Prinzip wieder parallel m¨ oglich. Man erh¨ alt: if true then 2 ∗ ( + ) else (4, 2, λvw.v + w)
Schritt 5: Die Auswertung des bedingten Ausdrucks und der Basisfunktion + liefert 2 ∗ 6. Schritt 6: Nach Anwendung der Basisfunktion ∗ erh¨ alt man das Resultat 12. 4.3.1.2 Datenflußmaschinen Bei datengesteuerten Reduktionsmaschinen wird stets das Resultat einer Funktionsapplikation an die Stelle der Funktionsapplikation eingesetzt. Das Resultat steht daher unmittelbar nur derjenigen Funktion zur Verf¨ ugung, in deren Rumpf die Funktionsapplikation steht. Bei Datenflußmaschinen erfolgt die Ausf¨ uhrung eines Programms zwar auch datengesteuert, jedoch kann das Resultat einer Funktionsapplikation an verschiedene andere Funktionen weitergereicht werden. Hierzu verf¨ ugt jede Funktionsapplikation u ¨ber die Information, an welchen Stellen im Programm“(Adressen) das Resultat zu ”
284
4 Implementierungstechniken
u ¨bergeben ist. Jede Adresse besteht aus dem Namen einer Funktion und dem Namen einer ihrer Variablen. Sie ist somit nicht mit der Adresse einer Speicherzelle zu verwechseln. Resultat und Adresse bilden zusammen ein Token. Zur Darstellung von Programmen f¨ ur Datenflußmaschinen hat man spezielle Datenflußsprachen entwickelt. Beispiele hierf¨ ur findet man in Dennis (Dennis 1972, Dennis 1972a, Dennis 1975a), Kosinski (Kosinski 1973), Gell (Gell 1976), Arvind (Arvind 1978), Ackermann (Ackermann 1979), McGraw (McGraw 1983) und Patnaik (Patnaik 1984). Eine weitere M¨oglichkeit ist die Darstellung als Datenflußgraph. Die Knoten des Graphen repr¨asentieren Funktionen und die Kanten die Adressen, an die die Resultate von Funktionsapplikationen u ¨bergeben werden. Sind alle Eingangskanten eines Knotens mit Daten belegt, so wird die im Knoten angegebene Funktion auf diese Daten appliziert, das Resultat der Applikation wird gem¨aß der ausgehenden Kanten an andere Knoten u ¨bergeben und die Daten werden an den Eingabepfeilen gel¨ oscht. Wert und Kante zusammen repr¨ asentieren somit ein Token. Betrachten wir als Beispiel den Datenflußgraphen:
x
y
DUP * +
Er repr¨ asentiert die Funktion λxyz.x + (x − y) ∗ z. Eine Applikation λxyz.x + (x − y) ∗ z (4, 3, 2) f¨ uhrt zu folgenden Token im Graph:
z
4.3 Hardware – Unterst¨ utzte Implementierungen
4 DUP
3
285
2
* +
Zun¨ achst ist nur die Eingangskante des Knotens DUP mit Daten belegt. Dieser Knoten dient lediglich dazu, ankommende Daten zu verdoppeln. Die Ausf¨ uhrung der Applikation DUP(4) liefert: 3 DUP
2
4 *
4 +
Jetzt sind die Eingangskanten des Knotens − mit Daten belegt. Die Applikation −(4, 3) wird ausgef¨ uhrt, das Resultat 1 wird an den Knoten ∗ u ¨bergeben und die Daten an den Eingangskanten des Knotens − werden gel¨oscht: 2 DUP 1 * 4 +
286
4 Implementierungstechniken
Nun sind die Eingangskanten des Knotens ∗ mit Daten belegt, die Applikation ∗(1, 2) wird ausgef¨ uhrt, das Resultat 2 wird an den Knoten + u ¨bergeben und die Daten an den Eingangskanten des Knotens ∗ werden gel¨oscht:
DUP * 4
2
+
Die Eingangskanten des Knotens + sind jetzt mit Daten belegt, die Applikation +(4, 2) wird ausgef¨ uhrt, die Daten an den Eingangskanten des Knotens + werden gel¨ oscht und das Resultat 6 wird ausgegeben.
DUP * + 6 Ein etwas umfangreicherer Datenflußgraph findet sich in Kapitel 4.3.3. In Datenflußmaschinen wird der Berechnungsablauf im allgemeinen durch eine der beiden folgenden Rechnerarchitekturen realisiert: 1. Token-matching“-Maschinen ” Maschinen von diesem Typ besitzen die Struktur
4.3 Hardware – Unterst¨ utzte Implementierungen
287
Token - Tupel
Matching“” Einheit
Applikationseinheit
Funktionsdefinitionen
einzelne Tupel In der Applikationseinheit werden die Funktionsapplikationen ausgef¨ uhrt und als Ergebnis einzelne Token abgegeben. In der Matching“-Einheit ” werden Token gesammelt und diejenigen Token, die zur Bildung einer Applikation ben¨ otigt werden, zu Tupeln zusammengefaßt. Diese Tupel werden sodann von der Matching“-Einheit zur Applikationseinheit abge” sandt. In der Applikationseinheit werden aus den ankommenden Token Tupeln und den Funktionsdefinitionen weitere Funktionsapplikationen gebildet und ausgef¨ uhrt. Beispiele f¨ ur diesen Maschinentyp sind die IrvineDatenflußmaschine, die Newcastle-Maschine und die in Kapitel 4.3.4 n¨aher beschriebene Manchester-Datenflußmaschine. 2. Token storage“-Maschinen ” Maschinen von diesem Typ besitzen die Struktur
Applikationsbildung
Funktionsdefinitionen
Applikationsaus¨ uhrung einzelne Tupel In einem ringf¨ ormigen Tokenbus kreisen die einzelnen Token. Die Einheit zur Applikationsbildung entnimmt diesem Tokenstrom einzelne Token und speichert sie gem¨ aß der angegebenen Adressen in Kopien der Funktionsdefinitionen ab. Sind f¨ ur alle Variablen einer Funktion Token vorhanden, so wird aus Funktionsdefinition und Token eine Funktionsapplikation gebildet und an die Einheit zur Applikationsausf¨ uhrung u ¨bergeben, wo sie ausgef¨ uhrt wird. Als Ergebnis der Applikationsausf¨ uhrung werden neue Token an den Tokenstrom abgegeben. Beispiele f¨ ur diesen Maschinentyp sind die Texas Instruments Datenflußmaschine (Cornish 1979) und die Datenflußmaschine von Dennis (Dennis 1979).
288
4 Implementierungstechniken
Bei beiden Maschinentypen muß durch geeignete Kontrollmechanismen gew¨ ahrleistet werden, daß zur Bildung einer Applikation nur zusammengeh¨ orige Token verwandt werden, also z.B. keine Token aus verschiedenen Durchl¨ aufen einer Schleife. Da in Kapitel 4.3.4 eine Token matching“” Maschine ausf¨ uhrlicher behandelt wird, wollen wir den Berechnungsablauf in einer Token storage“-Maschine an unserem Programm aus Beispiel 4.3-1 ” noch einmal detailliert erl¨ autern. Beispiel 4.3-2 Wir betrachten wieder die Funktionsapplikation f (3 + 1, 2, λvw.v + w) und die Funktionsdefinition f = λxyz.if x > y then (x − 2) ∗ z(x, y) else y aus Beispiel 4.3-1. Zun¨ achst muß dieses Programm in eine datenflußgerechte Form u ¨bersetzt werden. Um jede Funktionsapplikation mit den Adressen zu versorgen, f¨ ur die das Resultat bestimmt ist, wollen wir diese Adressen den einzelnen Funktionsdefinitionen beigeben. Hierzu wird f¨ ur jeden Knoten des Datenflußgraphens, der das Programm repr¨ asentiert, eine Funktionsdefinition angelegt. Wird also eine Funktion f in einem Programm n-mal appliziert, so werden n Definitionen dieser Funktion erzeugt, die sich lediglich durch die Adresse unterscheiden, an die das Resultat der Funktionsapplikation u ¨bergeben wird. Bei konkreten Realisierungen l¨ aßt sich dieser Aufwand jedoch wesentlich reduzieren. Die Adressen werden von der Einheit zur Applikationsbildung mitkopiert und zusammen mit der anzuwendenden Applikation an die Einheit zur Applikationsanwendung weitergegeben, die diese Adressen wiederum den von ihr abgesandten Token mitgibt. Wir wollen ferner in dem Beispiel davon ausgehen, daß das Resultat einer Funktion stets ein elementares Datum und keine Funktion ist. Die datenflußgerechte Form des Programms soll also keine Funktionale enthalten. Diese Einschr¨ ankung ist nicht von prinzipieller Natur. Sie dient lediglich dazu, eine einheitliche Struktur der Token zu erzielen, und gilt daher bei fast allen realisierten Maschinen. Mit diesen Vereinbarungen wird das Programm in die folgende datenflußgerechte Form u ¨bersetzt: g f h : :
= λvw.v + w : (f / z) = λxyz.if x > y then (x − 2) ∗ z else y : (out) = 3 + 1 : (f / x), (g / v) (f / y, 2) (g / w, 2)
4.3 Hardware – Unterst¨ utzte Implementierungen
289
Die Hilfsfunktion g wird ben¨ otigt, um das funktionale Argument in der Applikation von f zu eliminieren. Bei jeder Funktionsdefinition sind durch : die Adressen gekennzeichnet, die im Laufe der Berechnung von der Applikation u ¨bernommen werden. Die Adresse (out) soll anzeigen, daß Applikationen der Funktion f ihr Resultat stets an ein Ausgabemedium abgeben. Die Token (f / y, 2) und (g / w, 2) sind Starttoken. Mit ihnen wird der Tokenstrom initialisiert. Die Einheit zur Applikationsbildung findet zun¨achst im Tokenstrom diese beiden Token und entnimmt sie dem Tokenstrom. Danach kopiert sie sich die Definitionen der Funktionen f bzw. g und verkn¨ upft die Kopien mit den Token. Eine Sonderstellung stellt die Funktion h dar, die variablenfrei ist. Da sie stets appliziert werden kann, wird auch sie kopiert, um direkt an die Einheit zur Applikationsausf¨ uhrung weitergegeben zu werden. Der Zustand der Einheit zur Applikationsbildung l¨ aßt sich jetzt folgendermaßen beschreiben: g = λvw.v + w : (f / z) :: (g / w, 2) f = λxyz.if x > y then (x − 2) ∗ z else y : (out) :: (f / y, 2) h = 3 + 1 : (f / x), (g / v) Hinter :: sind jeweils diejenigen Token aufgelistet, die aus dem Tokenstrom entnommen und mit den zugeh¨ origen Funktionsdefinitionen verkn¨ upft wurden. Die Funktionen f und g k¨ onnen noch nicht appliziert werden, da ihnen jeweils ein Token fehlt. Die Einheit zur Applikationsbildung u ¨bergibt daher lediglich 3 + 1 zusammen mit den Zieladressen (f / x) und (g / v) an die Einheit zur Applikationsausf¨ uhrung und l¨ oscht die Kopie der Definition von h. Von dieser Einheit werden die Token (f / x, 4) und (g / v, 4) an den Tokenstrom abgegeben. Der Zustand der Einheit zur Applikationsbildung ist g = λvw.v + w : (f / z) :: (g / w, 2) f = λxyz.if x > y then (x − 2) ∗ z else y : (out) :: (f / y, 2) und der Inhalt des Tokenstroms ist (f / x, 4) (g / v, 4) Die Einheit zur Applikationsbildung findet im Tokenstrom diese beiden Token, entnimmt sie dem Tokenstrom und verkn¨ upft sie mit den Definitionen von g und f . Ferner kopiert die Einheit zur Applikationsbildung wiederum die Definition der Funktion h. Der Zustand der Einheit zur Applikationsbildung ist g = λvw.v + w : (f / z) :: (g / w, 2), (g / v, 2) f = λxyz.if x > y then (x − 2) ∗ z else y : (out) :: (f / y, 2), (f / x, 2) h = 3 + 1 : (f / x), (g / v) und der Tokenstrom ist leer.
290
4 Implementierungstechniken
Die Funktion g besitzt jetzt f¨ ur jede Variable ein Token. Die Einheit zur Applikationsbildung bildet daher die Funktionsapplikation λvw.v + w(4, 2) : (f / z), u uhrung und l¨oscht die Kopie ¨bergibt sie an die Einheit zur Applikationsausf¨ der Definition von g. Außerdem wird an die Einheit zur Applikationsausf¨ uhrung wiederum 3 + 1 zusammen mit den Zieladressen (f / x) und (g / v) u ¨bergeben und die Kopie der Definition von h wird gel¨oscht. Als Ergebnis der Ausf¨ uhrung der beiden Applikationen gibt die Einheit zur Applikationsausf¨ uhrung die Token (f / z, 6), (f / x, 6) und (g / v, 4) an den Tokenstrom ab. Der Zustand der Applikationsbildungseinheit ist: f = λxyz.if x > y then (x − 2) ∗ z else y : (out) :: (f / y, 2), (f / x, 4) und der Inhalt des Tokenstrom ist: (f / z, 6) (f / x, 6) (g / v, 4) Die Einheit zur Applikationsbildung findet im Tokenstrom das Token (g / v, 4) und entnimmt es dem Tokenstrom. Danach kopiert sie die Definition von g und verkn¨ upft die Kopie mit dem Token. Ferner stellt die Einheit fest, daß im Tokenstrom zwei f¨ ur die Funktion f bestimmte Token vorhanden sind: ein Token mit der Adresse f / z und ein Token mit der Adresse f / x. Andererseits wurde die Kopie der Definition von f bereits mit einem Token mit der Adresse f / x verkn¨ upft. In diesem Fall soll ein solches Token nicht dem Tokenstrom entnommen werden. Wir werden jedoch in Kapitel 4.3.4 sehen, daß derartige Situationen im allgemeinen nicht so einfach zu behandeln sind, sondern einen speziellen Steuerungsmechanismus ben¨otigen. Die Einheit zur Applikationsbildung entnimmt somit dem Tokenstrom nur das Token (f / z, 6) und verkn¨ upft es mit der Kopie der Definition von f . Kopiert wird außerdem wiederum die Definition der Funktion h. Der Zustand der Einheit zur Applikationsbildung ist: f = λxyz. if x > y then (x − 2) ∗ z else y : (out) :: (f / y, 2), (f / x, 4), (f / z, 6) g = λvw.v + w : (f / z) :: (g / w, 4) h = 3 + 1 : (f / x), (g / v) und der Inhalt des Tokenstroms ist: (f / x, 4)
4.3 Hardware – Unterst¨ utzte Implementierungen
291
Die Funktion f besitzt jetzt f¨ ur jede Variable ein Token. Die Einheit zur Applikationsbildung bildet die Applikation λxyz.if x > y then (x − 2) ∗ z else y : (out), u uhrung und l¨oscht die Ko¨bergibt sie an die Einheit zur Applikationsausf¨ pie der Definition von f . Außerdem wird an die Einheit zur Applikationsausf¨ uhrung wiederum 3 + 1 zusammen mit den Zieladressen (f / x) und (g / v) u ¨bergeben und die Kopie der Definition von h gel¨oscht. Als Ergebnis der Ausf¨ uhrung der beiden Applikationen gibt die Einheit zur Applikationsausf¨ uhrung die Token (out, 12), (f / x, 4) und (g / v, 4) an den Tokenstrom ab. Vom Ausgabemedium wird das Token (out, 12) aus dem Tokenstrom entnommen und das Resultat 12 ausgegeben. In den folgenden Kapiteln werden je eine konkrete String-Reduktions-, GraphReduktions- und Datenflußmaschine n¨ aher erl¨ autert. Informationen u ¨ber weitere String-Reduktionsmaschinen findet man in Treleaven (Treleaven 1980a, Treleaven 1980b), Mago (Mago 1979, Mago 1980), u ¨ber weitere GraphReduktionsmaschinen in Keller (Keller 1978, Keller 1979), Clarke (Clarke 1980) und Darlington (Darlington 1981), u ¨ber weitere Datenflußmaschinen in Dennis (Dennis 1974b, Dennis 1975b), Cornish (Cornish 1979), Davis (Davis 1978, Davis 1979b), Arvind (Arvind 1978, Arvind 1980), Gostelow (Gostelow 1979a, Gostelow 1979b), Comte (Comte 1979), JIPD (JIPD 1981), IEEE Comput. 15 (IEEE 1982), Hiraki (Hiraki 1984), Shimada (Shimada 1986), Amamiya (Amamiya 1986) und Ungerer (Ungerer 1993). 4.3.2 Die GMD-Reduktionsmaschine (Berkling-Maschine) 4.3.2.1 Einleitung Bei der Beschreibung der Sprache BRL in Kapitel 3.3.5 wurde bereits darauf hingewiesen, daß die Entwicklung dieser Sprache in engem Zusammenhang mit der Entwicklung einer Reduktionsmaschine stand, auf der BRL- Programme unmittelbar ausgef¨ uhrt werden k¨ onnen. Die Arbeiten an der Reduktionsmaschine begannen etwa 1972 und wurden 1975 publiziert (Berkling 197S). 1974 beschrieb F. Hommes in seiner Diplomarbeit den ersten Simulator. Zwischen 1976 und 1978 wurde, haupts¨achlich von W.E. Kluge, unter Verwendung von Standardbausteinen und TTL-Logik eine Hardware-Implementierung realisiert. Obwohl die GMD-Reduktionsmaschine eine String -Reduktionsmaschine ist, betrachten wir zur Veranschaulichung ihrer Arbeitsweise zun¨achst die Darstellung eines BRL-Ausdrucks als bin¨ arer Baum, bei dem die Operatoren die Knoten und die elementaren Werte die Bl¨atter bilden. Zur Reduktion
292
4 Implementierungstechniken
des Ausdrucks durchl¨ auft man die Baumstruktur systematisch von oben nach unten und von links nach rechts. Hierbei werden reduzierbare Teilausdr¨ ucke durch das Ergebnis ihrer Reduktion ersetzt. Eine derartige Reduktion ist jeweils dann m¨ oglich, wenn ein Operator auf seine Argumente unmittelbar anwendbar ist. Das Prinzip sei am Beispiel eines einfachen arithmetischen Ausdrucks erl¨ autert: Beispiel 4.3-3 Gegeben sei der Ausdruck ((5 + 3) ∗ (5 − 3))
.
Die Darstellung dieses Ausdrucks als bin¨ arer Baum ist *
-
+
5
3
5
3
Da die Abarbeitung von oben nach unten und von links nach rechts erfolgt, erh¨ alt man die folgende Reihenfolge (1..10) f¨ ur die einzelnen Phasen der Abarbeitung: * 1
5
10
6
-
+
2 5
3
4
7
3
5
8
9 3
Ein Reduktionsschritt kann jedesmal dann erfolgen, wenn ein Operator als Unterb¨ aume zwei Zahlen besitzt. Dies ist zum ersten Mal bei Phase 4 der Fall. Hier wird erkannt, daß + als Unterb¨ aume die Zahlen 5 und 3 besitzt.
4.3 Hardware – Unterst¨ utzte Implementierungen
293
Somit kann die Addition + ausgef¨ uhrt werden, das Ergebnis ist 8, und der Unterbaum +
5
3
wird durch 8 ersetzt. Man erh¨ alt den Baum *
-
8
5
3
Der n¨ achste Reduktionsschritt erfolgt bei Phase 9: *
8
2
Nun sind die Unterb¨ aume des Operators ∗ Zahlen und ∗ kann ausgef¨ uhrt werden. Man erh¨ alt das Endergebnis 16. 4.3.2.2 Der interne Aufbau der GMD-Maschine Die GMD-Reduktionsmaschine arbeitet nicht auf Baumstrukturen, sondern auf linearen Zeichenreihen ( strings“). Bei der detaillierten Erl¨auterung ihrer ” Arbeitsweise gehen wir von der Darstellung eines BRL-Ausdrucks in Pr¨afixnotation aus. Diese Notation entspricht nicht exakt der internen Darstellung in der GMD-Reduktionsmaschine. Sie wurde jedoch hier zur Vereinfachung gew¨ ahlt.
294
4 Implementierungstechniken
Das in Beispiel 4.3-3 erl¨ auterte Prinzip wird in der GMD-Reduktionsmaschine mit Hilfe mehrerer Keller realisiert. Das Verhalten der Maschine ist lediglich von den jeweils obersten Eintr¨ agen in den Kellern abh¨angig. Die wichtigsten Keller sind der Quellkeller (E-stack), der Zielkeller (A-stack) und der Operatorenkeller (M-stack). Zu Beginn der Berechnung steht der Ausdruck im Quellkeller, die u ¨brigen Keller sind leer. Der Ausdruck wird nun zeichenweise vom Quellkeller in den Zielkeller transportiert, wobei die Operatoren im Operatorenkeller abgelegt werden. Ergeben hierbei die obersten Eintragungen in den drei Kellern eine ausf¨ uhrbare Operation, so wird sie ausgef¨ uhrt. Terminiert die Reduktion des Programms korrekt, so steht im allgemeinen nur noch ein elementarer Wert im Zielkeller und die u ¨brigen beiden Keller sind leer. Dieses Zusammenwirken der drei Keller sei an der Abarbeitung des Ausdrucks aus Beispiel 4.3-3 illustriert: Beispiel 4.3-3 (Fortsetzung): 1. Schritt: Regel 1: Zu Beginn steht der zu reduzierende Ausdruck in E. ∗+53−53
A:
:E
:M
2. Schritt: Regel 2: Steht in E ein Operator1 , so wird der Operator mit ’ “ markiert ” und in M abgelegt. +53−53
A:
∗
:E
:M
3. Schritt: Anwendung von Regel 2. 1
Genauer: Ist der oberste Eintrag in E ein Operator“. Aus Gr¨ unden der Verein” fachung wird jedoch hier und im Folgenden diese Formulierung benutzt.
4.3 Hardware – Unterst¨ utzte Implementierungen
53−53
A:
+ ∗
295
:E
:M
4. Schritt: Regel 3: Steht in E ein elementarer Wert, in M ein bin¨arer Operator und ist A leer, so wird der elementare Wert nach A transportiert. A:
3−53
5
+ ∗
:E
:M
5. Schritt: Regel 4: Steht in M ein bin¨ arer Operator, in E und A jeweils ein elementarer Wert und wurde im vorhergehenden Schritt kein Operator in M gespeichert, so erfolgt eine Reduktion (Anwendung des Operators). Das Ergebnis wird in A abgelegt, falls M den Operator als einziges Element enth¨ alt. Das Ergebnis wird in A abgelegt, falls der n¨achste Operator (hier ∗) in M markiert ist, sonst in E. Der n¨ achste Operator in M wird ummarkiert (Op → Op, Op → Op). A:
−53
8
∗
:E
:M
6. Schritt: Regel 5: Stehen in E und in M ein Operator und in A ein elementarer Wert, so wird der Operator aus E in M abgelegt. A:
53
8
− ∗
:M
:E
296
4 Implementierungstechniken
7. Schritt: Regel 6: Steht in M ein bin¨ arer Operator, in E und A jeweils ein elementarer Wert und wurde im vorhergehenden Schritt ein Operator in M gespeichert, dann wird der Wert aus E in A abgelegt. A:
85
− ∗
3
:E
2
:E
:M
8. Schritt: Anwendung von Regel 4. A:
8
∗
:M
9. Schritt: Anwendung von Regel 4. Der Quellkeller E ist nun leer, und eine eventuell notwendige weitere Abarbeitung w¨ urde jetzt von A nach E erfolgen. A:
16
:E
:M
Damit ist die prinzipielle Funktionsweise der Maschine am Beispiel von Ausdr¨ ucken mit bin¨aren Operatoren gezeigt worden. Zur Reduktion von BRLProgrammen treten weitere, spezielle Regeln f¨ ur die u ¨brigen Sprachelemente hinzu. Als Beispiel wird die Funktionsapplikation erkl¨art. Hierbei sind drei Probleme zu l¨ osen: - Herstellung der korrekten Bindungen zwischen Variablen und Argumenten, - Erkennen derjenigen Stellen im Funktionsrumpf, an dennen die Variablen auftreten,
4.3 Hardware – Unterst¨ utzte Implementierungen
297
- Ersetzen der Variablen im Funktionsrumpf durch die entsprechenden Argumente. Die L¨ osung erfolgt mit Hilfe zweier zus¨ atzlicher Keller: dem Variablenkeller, in dem die Variablen, und dem Argumentkeller , in dem die Argumente abgelegt werden. Zun¨ achst wird das Programm vom Quellkeller zum Zielkeller transportiert, wobei die durch sub gekennzeichneten Variablen im Variablenkeller und die durch to gekennzeichneten Argumente im Argumentkeller abgespeichert werden. Die korrekte Bindung zwischen Variablen und Argumenten ergibt sich durch die gleiche Position im Variablen- bzw. Argumentkeller. Der aktuelle Wert der i-ten Variablen ist der i-te Eintrag im Argumentkeller. Danach wird das Programm vom Zielkeller zur¨ uck zum Quellkeller transportiert und hierbei bei jedem Zeichen u uft, ob es mit dem obersten Eintrag im Variablen¨berpr¨ keller u ¨bereinstimmt. Ist dies der Fall, so wird an seiner Stelle eine Kopie des obersten Eintrags im Argumentkeller in den Quellkeller eingetragen. Ist der Zielkeller leer, so wird im Variablen- und Argumentkeller jeweils der oberste Eintrag gel¨ oscht. Befinden sich jetzt im Variablen- und Argumentkeller noch Eintragungen, so liegt der Aufruf einer mehrstelligen Funktion vor, und die Ersetzung von Variablen durch Argumente wird solange wiederholt, bis diese beiden Keller leer sind. Ist nur einer der beiden Keller leer, so liegt ein inkorrekter Aufruf vor. Danach steht im Quellkeller der modifizierte Funktionsrumpf zur weiteren Reduktion bereit. Beispiel 4.3-4 Gegeben sei das BRL-Programm apply apply sub x in sub y in((x + y) ∗ (x − y)) to 5 to 3 Zur Veranschaulichung die Darstellung als bin¨ arer Baum: apply apply sub x
3 5
sub ∗
y
−
+ x
y
x
y
Die Reduktion des Programms geschieht gem¨ aß der folgenden Schritte:
298
4 Implementierungstechniken
1. Schritt: Zu Beginn der Reduktion stehe das Programm in Pr¨afix-Notation im Quellkeller E (Regel 1). Variablenkeller:
Argumentkeller:
apply apply sub x in sub y in ∗ + x y − x y to 5 to 3
A:
:E
:M 2. Schritt: Anwendung von Regel 2. Variablenkeller:
Argumentkeller:
apply sub x in sub y in ∗ + x y − x y to 5 to 3
A:
apply
:E
:M
3. Schritt: Anwendung von Regel 2. Variablenkeller:
Argumentkeller:
sub x in sub y in ∗ + x y − x y to 5 to 3
A: apply apply
:E
:M
4. Schritt: Regel 7: Steht in E sub und in M apply; so wird sub gel¨oscht, die auf sub folgende Variable im Variablenkeller abgelegt und das auf die Variable oscht. folgende in gel¨
4.3 Hardware – Unterst¨ utzte Implementierungen
Variablenkeller: x
299
Argumentkeller:
sub y in ∗ + x y − x y to 5 to 3
A: apply apply
:E
:M
5. Schritt: Anwendung von Regel 7. Variablenkeller: xy
Argumentkeller:
A:
∗ + x y − x y to 5 to 3 apply apply
:E
:M
6. Schritt: Regel 8: Ist das letzte sub verarbeitet, so wird der Rumpf der Funktion von E nach A transportiert. Das Ende des Rumpfes ist an dem entsprechenden to zu erkennen. Variablenkeller: xy A:
Argumentkeller:
∗+xy−xy
to 5 to 3 apply apply
:E
:M
7. Schritt: Regel 9: Steht in E to und in M apply ; so wird to gel¨oscht und im Fall von call by name“ der auf to folgende Ausdruck im Argumentkeller gespei” chert. Bei der Programmierung der GMD-Reduktionsmaschine benutzten Variante von BRL kann der Benutzer wahlweise eine call by name“- oder ” eine call by value“- Parameter¨ ubergabe vorsehen.. ”
300
4 Implementierungstechniken
Variablenkeller: xy A:
Argumentkeller: 5
∗+xy−xy
to 3 apply apply
8. Schritt: Anwendung von Regel 9. Variablenkeller: xy A:
:E
:M
Argumentkeller: 35
∗+xy−xy
:E
apply apply
:M
9. Schritt: Regel 10: Ist E (bzw. A) leer und steht in M apply,’ so wird der Ausdruck in A (bzw. E) nach E (bzw. A) transportiert. Stimmt hierbei das zu transportierende Zeichen mit der Variablen im Variablenkeller u ¨berein, so wird an Stelle der Variablen aus A (bzw. E) eine Kopie des Arguments (oberster Ausdruck im Argumentkeller) nach E (bzw. A) transportiert. Variablenkeller: Argumentkeller: xy 35 A:
∗+x3−x3
apply apply
:E
:M
10. Schritt: Regel 11: Nach Ausf¨ uhrung von Regel 10 wird der jeweils oberste Eintrag im Variablenkeller, im Argumentkeller und in M gel¨oscht.
4.3 Hardware – Unterst¨ utzte Implementierungen
Variablenkeller: x
Argumentkeller: 5
A:
∗+x3−x3
apply 11. Schritt: Anwendung von Regel 10. Variablenkeller: x A:
:E
:M
Argumentkeller: 5
∗+53−53
:E
apply 12. Schritt: Anwendung von Regel 11. Variablenkeller:
A:
301
:M
Argumentkeller:
∗+53−53
:E
:M
13. Schritt: Regel 12: Steht nach Ausf¨ uhrung von Regel 11 in M kein apply’ und steht der zu reduzierende Ausdruck in A, so wird der Ausdruck nach E transportiert.
302
4 Implementierungstechniken
Variablenkeller:
A:
Argumentkeller:
∗+53−53
:E
:M
Der jetzt in E stehende Ausdruck wird nun gem¨aß Beispiel 4.3-3 weiter reduziert. M¨ ochte man eine call by value“-Parameter¨ ubergabe realisieren, so m¨ ussen die ” Argumente vor ihrer Abspeicherung im Argumentkeller ausgewertet werden. Dies kann mit den in den Beispielen beschriebenen Techniken erfolgen. Nach dieser schematischen Darstellung soll die Architektur der GMD-Reduktionsmaschinen noch etwas detaillierter dargestellt werden: Die Maschine besitzt insgesamt sieben Keller: E, A, M, U, V, S, B. Der Keller S dient lediglich der Steuerung des internen Ablaufs, die u ¨brigen sechs zur Reduktion des zu verarbeitenden Programms. Das Reduzieren wird durch die Reduction Limit, die aus vier Modulen besteht, gesteuert: - TRANSport TRANS ist f¨ ur den Transport, den Vergleich und das Kopieren von Zeichen zust¨ andig; - REDuction RECognition REDREC ist f¨ ur das Erkennen von ausf¨ uhrbaren Reduktionsschritten zust¨ andig. Wird ein ausf¨ uhrbarer Reduktionsschritt erkannt, so wird TRANS deaktiviert, REDEX aktiviert und in S ein Unterprogramm zur Steuerung des Reduktionschrittes geladen; - REDuction EXecution REDEX ist f¨ ur die Ausf¨ uhrung von Reduktionsschritten zust¨andig. Nach erfolgter Reduktion wird wieder TRANS aktiviert. Ist der auszuf¨ uhrende Reduktionsschritt eine arithmetische Operation, so wird REDEX unterst¨ utzt von ARITH; - ARITHmetic ARITH ist f¨ ur die Ausf¨ uhrung von arithmetischen Operationen zust¨andig. Eine ausf¨ uhrliche Beschreibung der Architektur und der Wirkungsweise der GMD-Reduktionsmaschine findet sich in Hommes (Hommes 1977a), Kluge ¨ (Kluge 1979, Kluge 1980). Uber Versuche, Teile der GMD-Reduktionsma-
4.3 Hardware – Unterst¨ utzte Implementierungen
303
schine in VLSI-Technologie zu realisieren, wird in Berkling (Berkling 1983b) berichtet. 4.3.2.3 Kooperierende Reduktionsmaschinen Die von Berkling entwickelte GMD-Reduktionsmaschine ist eine sequentielle Maschine. Eine parallele Reduktion unabh¨ angig voneinander reduzierbarer Teilausdr¨ ucke ist nicht vorgesehen, obwohl die parallele Evaluation der Argumente in Funktionsaufrufen m¨ oglich ist. Zur parallelen Reduktion von BRL-Programmen entwickelte Kluge das Konzept der kooperierenden Reduktionsmaschinen und testete es im Rahmen einer Simulation auf vier PDP-11-Rechnern. Ausf¨ uhrlichere Beschreibungen finden sich in Kluge (Kluge 1983b) und Kluge (Kluge 1983c). Das Konzept beruht auf einem Reduktionsschritt, der der Umkehrung der β-Reduktion des Lambda-Kalk¨ uls entspricht. Ein BRL-Ausdruck τ [t] mit gewissen Vorkommen des Teilausdrucks τ wird zu apply ∗ sub t∗ in τ [t∗ ] to t reduziert, wobei alle Vorkommen von t in τ [t] durch die Variable t∗ ersetzt werden. Hierbei entspricht die Bedeutung von apply ∗ derjenigen von apply; der Zusatz ∗“ dient lediglich zur Kennzeichnung dieses speziellen Redukti” onsschritts. Die Variable t∗ kommt in τ [t] selbst nicht vor. Dieser Reduktionsschritt gestattet die parallele Reduktion von BRLProgrammen durch Maschinen, die nach dem Master-Slave-Prinzip miteinander gekoppelt sind. Die Master-Maschine reduziert einen Ausdruck τ [t] zu apply ∗ sub t∗ in τ [t∗ ] to t und u ¨bergibt den Ausdruck t zur Evaluation an eine Slave-Maschine. Diese Maschine wertet t aus und gibt das Ergebnis an die Master-Maschine zur¨ uck, die es an t∗ bindet und in t[t∗ ] einsetzt. Besitzt ein Ausdruck τ [t1, t2] nun zwei Teilausdr¨ ucke t1 und t2, die parallel evaluiert werden k¨ onnen, so erfolgt die Reduktion mit einer Master-Maschine M O und zwei Slave-Maschinen M 1 und M 2, die t1 und t2 parallel reduzieren. Der Ausdruck τ [t1, t2] wird von M O in zwei Schritten zu apply ∗ apply ∗ sub t1∗ in sub t2∗ in τ [t1∗ , t2∗ ] to t1 to t2 reduziert. Danach u ¨bergibt M O das Tupel (M O, t1∗ , t1) an die Maschine M 1 und das Tupel (M O, t2∗ , t2) an die Maschine M 2. M 1 wertet t1 zu V al(t1) uck. Entsprechend liefert M 2 aus und gibt an M O das Paar (t1∗ , V al(t1)) zur¨ das Paar (t2∗ , V al(t2)) an M O ab. Die erste Komponente eines Paars gibt jeweils an, an welche Variable jeweils die zweite Komponente gebunden ist, wenn M O diese in τ [t1∗ , t2∗ ] einsetzt.
304
4 Implementierungstechniken
Abb. 4.1. Architektur der GMD - Reduktionsmaschine
4.3 Hardware – Unterst¨ utzte Implementierungen
305
Beispiel 4.3-5 Gegeben sei in einer Reduktionsmaschine M O das BRL-Programm apply sub BIN OM in apply apply BIN OM to 5 to 3 + apply apply BIN OM to 6 to 2 to sub x in sub y in ((x + y) ∗ (x − y)). Nach der Ausf¨ uhrung des ersten apply, d.h. nach dem Ersetzen der Variablen BIN OM durch das Argument, erh¨ alt man apply apply sub x in sub y in ((x + y) ∗ (x − y)) to 5 to 3 + t1
apply apply sub x in sub y in ((x + y) ∗ (x − y)) to 6 to 2 . t2
Da die zwei Unterausdr¨ ucke t1 und t2 parallel evaluiert werden k¨onnen, wendet M O zweimal den oben beschriebenen Reduktionsschritt an. Man erh¨alt apply ∗ apply ∗ sub t1∗ in sub t2∗ in τ [t1∗ + t2∗ ] to t1 to t2. MO u ¨bergibt nun an M 1 das Tupel (M O, t1∗ , t1) und an M 2 das Tupel (M O, t2∗ , t2). M O arbeitet als Master-Maschine und M 1 und M 2 als SlaveMaschinen. M O wartet, w¨ ahrend M 1 und M 2 parallel die Ausdr¨ ucke t1 und t2 reduzieren. M 1 reduziert t1 u ¨ber die Zwischenschritte apply sub x in ((x + 3) ∗ (x − 3)) to 5 ((5 + 3) ∗ (5 − 3)) (8 ∗ 2) zu 16 und liefert an M O das Paar (t1∗ , 16) ab. M 2 reduziert t2 u ¨ber die Zwischenschritte apply sub x in ((x + 2) ∗ (x − 2)) to 6 ((6 + 2) ∗ (6 − 2)) (8 ∗ 4) zu 32 und liefert an M O das Paar (t2∗ , 32) ab. M O beendet das Warten und ersetzt den Ausdruck t1 durch 16, entsprechend t2 durch 32 und die beiden apply durch apply ∗ . Man erh¨alt apply apply sub t1∗ in sub t2∗ in t1∗ + t2∗ to 16 to 32 und die Reduktion dieses Ausdrucks gem¨ aß Beispiel 4.3-4 liefert als Endergebnis den Wert 48.
306
4 Implementierungstechniken
Dieses Konzept der kooperierenden Reduktionsmaschinen kann auch mit virtuellen Maschinen realisiert werden: Wir betrachten dazu eine modifizierte Version des BRL-Programms aus Beispiel 4.3-5. Es sind zwei Applikationen eingef¨ ugt worden, die als Wert 5 bzw. 3 liefern apply sub BIN OM in apply apply BIN OM to (apply sub z in (z + 3) to 2) to (apply sub z in (z + 2) to 1) + apply apply BIN OM to 6 to 2 to sub x in sub y in((x + y) ∗ (x − y)) Nach Ausf¨ uhrung des ersten apply, erh¨ alt man: apply apply sub x in sub y in ((x + y) ∗ (x − y)) to (apply sub z in (z + 3) to 2) to (apply sub z in (z + 2) to 1) + apply apply sub x in sub y in ((x + y) ∗ (x − y)) to 6 to 2 und nach zweimaliger Anwendung des Reduktionsschritts, durch den apply ∗ eingef¨ uhrt wird, (∗) apply ∗ apply ∗ sub t1∗ in sub t2∗ in t1∗ + t2∗ to t1 to t2 mit t1 = apply apply sub x in sub y in ((x + y) ∗ (x − y)) to (apply sub z in (z + 3) to 2) to (apply sub z in (z + 2) to 1) t2 = apply apply sub x in sub y in ((x + y) ∗ (x − y)) to 6 to 2 Dieser Ausdruck steht nun im Quellkeller E(M O) der Master-Maschine M O. MO u ¨bergibt an die Slave-Maschine M 1 das Tupel (M O, t1∗ , t1) und an die Slave-Maschine M 2 das Tupel (M O, t2∗ , t2). M 2 verh¨alt sich wie in Beispiel 4.3-5. M 1 dagegen verh¨ alt sich bei der Auswertung von t1 wie eine MasterMaschine und setzt zwei neue Slave-Maschinen ein. Zuvor f¨ uhrt M 1 zun¨achst uhrt wird. Dies zweimal den Reduktionsschritt aus, durch den apply ∗ eingef¨ liefert apply ∗ apply ∗ sub t1∗∗ in sub t2∗∗ in (apply apply sub x in sub y in ((x + y) ∗ (x − y)) to (apply sub z in (z + 3) to 2) to (apply sub z in (z + 2) to 1)
4.3 Hardware – Unterst¨ utzte Implementierungen
307
M O ruht zur Zeit, da die Ergebnisse von M 1 und M 2 noch nicht vorliegen. Als eine dieser Slave-Maschinen kann deswegen wieder M O genommen werden. Wird o.B.d.A. M O mit der Auswertung von (apply sub z . . . 2) beauftragt, so tr¨ agt M O zun¨ achst ein Trennsymbol # in seinen Kellern ein und danach in E (apply sub z . . . 2). Der Inhalt von E lautet somit (apply sub z . . . 2) # apply ∗ apply ∗ . . . to t1 to t2 . (∗)
Die Reduktion von (apply sub z . . . 2) erfolgt wie in Beispiel 4.3-4 beschrieben, wobei der Inhalt von E nur bis zum Trennsymbol # zum Zielkeller A transportiert wird. Am Ende der Reduktion steht im Zielkeller 5. M O liefert oscht im Zielkeller 5 und in E das Trennsymbol M 1 das Paar (t1∗∗ , 5) ab und l¨ #. M O wartet bis sowohl von M 1 als auch von M 2 die Ergebnisse abgeliefert werden, und verf¨ ahrt dann wie in Beispiel 4.3-5. Man erh¨alt wieder das Endergebnis 48. M O beginnt somit mit der Reduktion von (∗), unterbricht dann diese und reduziert (apply sub z . . . 2). Nachdem (apply sub z . . . 2) vollst¨andig reduziert ist, wird die Reduktion von (∗) beendet. Ordnet man die Reduktion (∗) einer Maschine M O’ zu und die Reduktion von (apply sub z . . . 2) einer Maschine M O” so verh¨ alt sich M O zun¨ achst wie M O’, dann wie M O“ und zum Schluß wieder wie M O’. Auf M O laufen daher die beiden virtuellen“ Maschinen ” M O’ und M O“ ab. Mit Hilfe dieser Technik k¨ onnen auf einer realen Maschine mehrere virtuelle Maschinen ablaufen. Die Koordination ist so geschickt gew¨ahlt, daß man auf der realen Maschine die Keller der dort ablaufenden virtuellen Maschinen in der aufgezeigten Weise einfach u ¨bereinander legen kann. Man ben¨otigt nur den obersten Teil des realen Kellers bis zum Zeichen tt. Ein Zugriff auf die darunterliegenden Zeichenreihen tritt bei der gew¨ahlten Koordination der virtuellen kooperierenden Reduktionsmaschinen nie auf. Die zugeh¨origen virtuellen Maschinen warten, bis die oberste“ virtuelle Maschine mit dem Re” duzieren fertig ist und das Ergebnis an ihre Master-Maschine abgeliefert hat. 4.3.3 Die S-K-I-Graph-Reduktionsmaschine von Turner 4.3.3.1 Einleitung F¨ ur eine Variante der in Kapitel 3.3.2 vorgestellten Sprache SASL wurde von D.A. Turner das Modell einer Graph-Reduktionsmaschine entwickelt: ein SASL-Programm π wird zun¨ achst in einen kombinatorischen Term α u ¨bersetzt und danach α durch einen Graph-Reduktionsmechanismus reduziert. Die ersten Arbeiten hierzu stammen aus dem Jahr 1975 (Turner 1976). Allgemein zug¨ anglich ist Turner (Turner 1979). Dort sind die Grundlagen ausf¨ uhrlich beschrieben. Die S-K-I-Graph-Reduktionsmaschine von Turner ist bisher noch nicht hardwarem¨aßig realisiert
308
4 Implementierungstechniken
worden; es existieren lediglich Simulationsprogramme. Sie wurde dennoch als Beispiel f¨ ur eine Graph-Reduktionsmaschine gew¨ahlt, weil sie der Ausgangspunkt f¨ ur viele weitere Entwicklungen war, und weil sie ohne Schwierigkeiten auch f¨ ur andere Sprachen verwendet werden kann. So lassen sich z.B. mit ihr ¨ FP-Programme mit Hilfe des in Kapitel 3.1.8 angegebenen Ubersetzungsverfahrens ausf¨ uhren. Die Implementierung der (getypten) Sprache MIRANDA, die auf der hier skizzierten S-K-I-Graph-Reduktionsmaschine aufbaut, jedoch andere Kombinatoren benutzt, ist detailliert in (Peyton Jones, Simon 1987) beschrieben. Die f¨ ur die Graph-Reduktionsmaschine benutzte Variante von SASL ist eine Zwischenstufe zwischen SASL und ihrer Nachfolgesprache KRC (Kapitel 3.3.3). Wie in KRC schreibt der Benutzer die von ihm definierten Funktionen in Form eines Gleichungssystems auf, wobei jede Gleichung mit dem Sonderzeichen def beginnt. Die Notation der Fakult¨atsfunktion ist def F akult¨ at n = n = 0 → 1; n × F akult¨ at(n − 1), an Stelle von at n be n = 0 → 1; n × F akult¨ at(n − 1). let F akult¨ An Stelle der lokalen Benennung eines Objekts durch einen let-Ausdruck let x = Obj1 in Obj2 tritt ein where-Ausdruck Obj2 where x = Obj1. Die Verwendung der Basisfunktionen hd und tl kann durch eine Strukturierung der Variablen einer Funktion ersetzt werden, d.h. die Funktionsdefinition def f x : y = . . . x . . . entspricht def f x = . . . hd x . . . und die Funktionsdefinition def f x : y = . . . y . . . entspricht def f x = . . . tl x . . . ¨ 4.3.3.2 Ubersetzung von SASL-Programmen M¨ochte man SASL-Programme in kombinatorische Terme u ¨bersetzen, so sieht man sich zun¨ achst mit folgendem Problem konfrontiert: SASL ist eine (im strengen Sinn) applikative Sprache, d.h. sie besitzt Variablen, w¨ahrend kom-
4.3 Hardware – Unterst¨ utzte Implementierungen
309
binatorische Terme variablenfrei sind. Die L¨ osung des Problems wurde bereits in Beispiel 2.3-1 gezeigt: sukzessive werden die Funktionen zun¨achst einparametrig gemacht (currifiziert), so daß die Variable nur noch ganz rechts steht, und danach die Funktion durch einen entsprechenden Kombinator ersetzt. Das Vorgehen sei zun¨ achst am Beispiel der Nachfolgerfunktion erl¨autert: Beispiel 4.3-6 Gegeben sei die Funktionsdefinition def suc x = x + 1 Im ersten Schritt wird die Funktion plus als currifizierte Addition eingef¨ uhrt und man erh¨ alt def suc x = plus 1 x. Im zweiten Schritt eliminiert man die Variable x und erh¨alt def suc = plus 1 . Diese Vorgehensweise soll nun verallgemeinert und begr¨ undet werden: Gegeben sei die Funktionsdefinition def f x = E Aus Kapitel 2.2.2 ist bekannt, daß sich f in die ¨aquivalente Funktion def fcurry x = Ecurry u uhren l¨ aßt. ¨berf¨ Wegen des Extensionalit¨ atsprinzips (s. Kapitel 2.2.2.3) l¨aßt sich die Variable x mit Hilfe der Abstraktion (Definition 2.3-5) eliminieren, und man erh¨alt def fcurry = [X] Ecurry , da ([x] Ecurry )x = Subxx Ecurry = Ecurry . Ausgangspunkt f¨ ur die Abstraktionsregeln sind die Basiskombinatoren S, K, I. Turner benutzt eine noch einfachere Abstraktionsbildung, als die in Definition 2.3-5 angegebene: [x]x = I [x]M = KM falls x ∈ / F V (M ) [x]U V = S([x]U ) ([x]V ) sonst Mit Hilfe dieser Abstraktionsregel ergibt sich f¨ ur das Beispiel:
310
4 Implementierungstechniken
Beispiel 4.3-6 (Fortsetzung) def suc = [x]((plus 1) x) = S([x] (plus 1)) ([x] x) = S(S([x] plus) ([x] 1))([x] x) = S(S(K plus) (K 1))I Der so mit Hilfe der oben angegebenen Abstraktionsregeln entstandene Term ist wesentlich komplexer als plus 1, jedoch funktional ¨aquivalent zu diesem. Das Beispiel zeigt, daß die angegebenen Regeln f¨ ur S,K,I-Abstraktionen zwar zu einem kombinatorischen Term f¨ uhren, dieser jedoch nicht unbedingt optimal ist. Es stellt sich damit die Frage nach optimalen Kombinatoren bzw. Abstraktionsregeln. Es ist einsichtig, daß man bei einer Beschr¨ankung auf fest vorgegebene Kombinatoren jeweils ein Beispiel angeben kann, f¨ ur das sich kein optimaler kombinatorischer Term erzeugen l¨ aßt. Dies war der Grund f¨ ur die Entwicklung der Superkombinatoren (Hughes 1982). Turner f¨ uhrt zur Optimierung zus¨ atzlich die Kombinatoren B und C ein, die definiert sind durch B f g x = f (gx) Cf gx=f xg ¨ Der Ubersetzer optimiert die erzeugten kombinatorischen Terme unter Verwendung der Optimierungsregeln: 1. S(KU ) (KV ) 2. S(KM )I 3. S(KU )V 4. SU (KV )
= K(U V ) =M = BU V falls weder 1. noch 2. anwendbar ist = CU V falls weder 1. - 3. anwendbar ist .
Die Anwendung der Optimierungsregeln erfolgt hierbei Hand in Hand mit der Anwendung der Abstraktionsregeln, damit auch zwischenzeitlich kein zu langer Code entsteht. Die Anwendung der Optimierungsregeln auf das Beispiel liefert: Beispiel 4.3-6 (Fortsetzung) def suc = [x] ((plus 1)x) = S([x] (plus 1)) ([x]x) = S(S([x] plus) ([x] 1)) ([x]x) = S(S(K plus) (K 1)) I = S(K(plus 1)) I = plus 1
(Opt.1) (Opt.2)
¨ Es folgt eine Aufstellung der Ubersetzerhandlungen f¨ ur die wichtigsten Konstrukte von SASL. 1. Funktionsdefinitionen Die Definition einer einstelligen Funktion
4.3 Hardware – Unterst¨ utzte Implementierungen
311
def f x = E wird, wie oben erl¨ autert, behandelt wie def f = [x]E , ¨ von E ist. wobei E die Ubersetzung Die Definition einer n-stelligen Funktion def f x1 x2 . . . xn = E wird behandelt wie def f = [x1]([x2](. . . ([xn]E ) . . . )). ucke 2. W here-Ausdr¨ Ein where-Ausdruck E1 where x = E2 wird behandelt wie ([x] E1 )E2 . Eine Funktionsdefinition im where-Teil . . . f . . . where f x = E wird gem¨ aß 1. behandelt wie ([f ] (. . . f . . .)) ) ([x]E ). Eine lokale Rekursion im where-Teil E1 where x = . . . x . . . wird behandelt wie ([x]E1 ) (Y ([x] (. . . x . . .) )), wobei Y der Fixpunktkombinator aus Satz 2.2-12 ist. Eine wechselseitige Rekursion im where-Teil E1 where f x = . . . g . . . and g y = . . . f . . . wird behandelt wie ([f, g]E1 ) (Y ([f, g] ([x](. . . g . . .) , [y] (. . . f . . .) ))). 3. Daten ¨ Atome bilden Konstanten in dem vom Ubersetzer erzeugten kombinatorischen Term, wobei f¨ ur die leere Liste die Konstante N IL eingesetzt wird. Listen x1, x2, . . . , xn werden u ¨bersetzt zu P x1 (P x2 (. . . (P xn N IL) . . .)),
312
4 Implementierungstechniken
wobei P ein spezieller Kombinator ist, der die currifizierte Version des Listenbildungsoperators darstellt (s. auch Definition 3.1-6). Zur Behandlung von strukturierten Variablen dient die zus¨atzliche Abstraktionsregel [x : y] E = U ([x] ([y]E)), wobei U ein weiterer spezieller Kombinator ist, f¨ ur den U f (P xy) = f xy gilt. 4. Programme Ein Programm def f1 x1 = E1 def f2 x2 = E2 (fi2 ) .. . def fn−1 xn−1 = En−1 (fin−1 ) def fn xn = En fn z where z = fn−1 y wird unter Beachtung von evtl. auftretenden lokalen oder wechselseitigen Rekursionen in where-Teilen behandelt wie def fn xn = En fn z where z = fn−1 y where fn−1 xn−1 = En−1 . . . (fin−1 ) . . . where f1 x1 = E1 . Beispiele 4.3-7 1. Ausgehend von dem Ausdruck suc 2 where suc x = 1 + x ¨ erzeugt der Ubersetzer u ¨ber die Zwischenschritte ([suc] (suc 2)) ([x](1 + x)) ([plus 1] plus 1 2) (plus 1) S([plus 1] plus 1) ([plus 1] 2) (plus 1) SI(K 2) (plus 1) den Code CI2(plus1) 2. F¨ ur den Ausdruck E1 where a : b = E2 wird u ¨ber ([a : b]E1 ) E2
4.3 Hardware – Unterst¨ utzte Implementierungen
313
der Code U ([a] ([b]E1 )) E2 erzeugt. 3. F¨ ur die Funktionsdeklaration def F akult¨ at n = n = 0 → 1; n ∗ F akult¨ at (n − 1) wird der Code def F akult¨ at = S(C(B cond(eq 0)) 1)(S mal (B F akult¨ at (C minus 1))) erzeugt, wobei die Konstanten cond, mal, minus und eq die currifizierten Versionen der Funktion cond zur Bildung von bedingten Ausdr¨ ucken und der ∗ und − Operatoren bzw. des Pr¨ adikats = sind. 4.3.3.3 Der Graph Reduktionsmechanismus ¨ Der durch die Ubersetzung entstandene kombinatorische Term wird intern als Graph (Codegraph) abgespeichert. Der Aufbau des Codegraphen ist ¨ahnlich wie die interne Darstellung von S-Ausdr¨ ucken in LISP. Die in einem Term auftretenden Funktionsnamen werden hierbei durch Verweise auf den entsprechenden Codegraphen dieser Funktion ersetzt. Dadurch k¨onnen bei den Graphen Zyklen auftreten. Beispiel 4.3-8 1) Der Codegraph f¨ ur die Fakult¨ atsfunktion (siehe Beispiele 4.3-7) ist
S
1
C
S mal
1
B
C minus
B cond
eq 0
314
4 Implementierungstechniken
2) Gegeben sei das Programm def suc x = 1 + x def dupl a = a + a dupl y where y = suc 2. ¨ Dieses Programm wird vom Ubersetzer behandelt wie das Programm def dupl a = a + a dupl y where y = suc 2 where suc x = 1 + x. Die Funktionsdefinition def dupl a = a + a wird u ¨ber die Zwischenschritte [a] (plus a a) S ([a] (plus a)) ([a] a) S (S ([a] plus) ([a] a)) ([a] a) S (S ([a] plus) I) I S (S (K plus) I) I u ¨bersetzt in den Code S plus I. Der Funktionsaufruf dupl y where y = suc 2 where suc x = 1 + x wird u ¨ber die Zwischenschritte ([y] (dupl y)) ([y] (dupl y)) S (K dupl) I dupl (S I (K dupl (S I (K
(suc 2 where suc x = 1 + x) (([suc] (suc 2)) ([x] (plus 1 x))) (S I (K 2) (S (S (K plus) (K 1)) I)) 2) (S (K (plus 1)) I)) 2) (plus 1))
u ¨bersetzt in den Code dupl (C I 2 (plus 1)). Der Codegraph f¨ ur den so entstandenen Code def dupl = S plus I dupl (C I 2 (plus 1)) ist
4.3 Hardware – Unterst¨ utzte Implementierungen
315
I
S plus
plus 1
2
C I ¨ Der vom Ubersetzer abgelieferte Codegraph wird gem¨aß den im folgenden angegebenen Graph-Reduktionsregeln durch die S-K-I-Graph-Reduktionsmaschine solange manipuliert, bis als Ergebnis ein Atom oder eine Liste entsteht, falls das Reduzieren terminiert. Sf gx ⇒ f x(gx) Kgy ⇒ g Ix ⇒ x Y h ⇒ h(Y h) Cf gx ⇒ (f x)g Bf gx ⇒ f (gx) U f (P xy) ⇒ f xy cond true xy ⇒ x cond f alse xy ⇒ y plus mn ⇒ m + n, wobei m und n bereits zu Zahlen reduziert sein m¨ ussen. Hinzu treten noch Graph-Reduktionsregeln f¨ ur minus, mal usw. . . Bei der Anwendung der Regeln ist zu beachten, daß ihre Anwendung eine Ver¨ anderung der Verweise innerhalb des Codegraphen bewirken. So hat die Anwendung der Regel Sf gx ⇒ f x(gx) keine textuelle Verdoppelung von x zur Folge, sondern an Stelle des einen Verweises auf x existieren jetzt zwei Verweise auf x: x
g
S f
g x
f
x
316
4 Implementierungstechniken
Die durchgezogenen Pfeile zeigen den Graphen vor Anwendung der Regel, die durchbrochenen Pfeile den Graphen nach Anwendung der Regel. f, g und x sind entweder Atome oder Verweise auf andere Codegraphen. Eine Besonderheit stellt die Regel Y h = h(Y h) ¨ dar. Ihre Anwendung bewirkt die Anderung: Y h wird zu h Beispiel 4.3-9 Gegeben sei der Ausdruck suc 2 where suc x = 1 + x. ¨ Gem¨ aß 1) aus Beispiel 4.3-7 erzeugt der Ubersetzer hieraus den Code C I 2(plus 1). Die S-K-I-Graph-Reduktionsmaschine reduziert diesen Code u ¨ber die Zwischenschritte I (plus 1) 2 plus 1 2 zu 3. Die sukzessiven Ver¨ anderungen des Codegraphen sind: 1. Anwendung der Regel f¨ ur C: 2
plus 1
2
C I
I
4.3 Hardware – Unterst¨ utzte Implementierungen
317
2. Anwendung der Regel f¨ ur I: 2
I
plus 1 3. Anwendung der Regel f¨ ur plus: 2 ⇒
3
plus 1 Die Auswahl unter den m¨ oglichen Reduktionsschritten wird so getroffen, daß eine Normalfolge (Definition 2.2-17) entsteht, d.h. bei jedem Reduktionsschritt wird das am weitesten links stehende Redex reduziert. Die Steuerung erfolgt durch einen zus¨ atzlichen Keller: 1. Vor Beginn der Anwendung der Graph-Reduktionsregeln enth¨alt der Keller lediglich einen Verweis auf die Wurzel des Codegraphen. 2. Zeigt der oberste Eintrag im Keller auf einen Knoten, dessen erste Komponente ein Kombinator ist, so wird der Kombinator in den Keller eingetragen. 3. Zeigt der oberste Eintrag im Keller auf einen Knoten, dessen erste Komponente ein Verweis auf einen anderen Knoten ist, so wird im Keller ein Verweis auf diesen anderen Knoten eingetragen. 4. Ist der oberste Eintrag im Keller ein Kombinator, so wird die entsprechende Graph-Reduktionsregel angewandt und die obersten Eintr¨age im Keller entsprechend der Reduktionsregel ge¨andert. 5. Ist die Reduktionsregel noch nicht anwendbar, da z.B. die Argumente von plus noch nicht zu Zahlen reduziert wurden, so wird entweder mit dem Untergraphen, auf den die zweite Komponente des Knotens auf den der vorletzte Eintrag im Keller zeigt, oder, falls die zweite Komponente kein Verweis ist, mit dem Untergraphen, auf den die zweite Komponente des Knotens auf den der davorliegende Eintrag im Keller zeigt, entsprechend 1. - 4. verfahren.
318
4 Implementierungstechniken
Dieser Steuerungsmechanismus gew¨ ahrleistet, daß bei jeder Anwendung eines Reduktionsschrittes gem¨ aß 2., die obersten Verweise des Kellers auf die Argumente des Kombinators zeigen. Da es sich um eine Graph-Reduktion handelt, wird die mehrfache Reduzierung von identischen Redizes, wie sie bei den u ¨blichen ’call-by-name’-Methoden auftreten kann, vermieden. Die Details seien den folgenden beiden Beispielen entnommen: Beispiel 4.3-10 1. Wir betrachten den ersten Schritt aus Beispiel 4.3-9: ¨ Vom Ubersetzer wird der Codegraph
2
plus 1
C I zur Reduktion u ¨bergeben. Die S-K-I-Graph-Reduktionsmaschine erzeugt hieraus gem¨aß 1. zun¨achst Keller
2
plus 1
2
plus 1
C I und danach gem¨ aß 3.: Keller
C I
4.3 Hardware – Unterst¨ utzte Implementierungen
319
und nach nochmaliger Anwendung von 3.: Keller
plus 1
2
C I Gem¨ aß 2. wird als n¨ achstes in den Keller C eingetragen: Keller
C
plus 1
2
C I Gem¨ aß 4. wird jetzt die Graph-Reduktionsregel f¨ ur C angewandt und man erh¨ alt: Keller 2 I
plus 1
2. F¨ ur das Programm def suc x = 1 + x def dupl a = a + a dupl y where y = suc 2
320
4 Implementierungstechniken
wird gem¨ aß Beispiel 4.3-8 der Codegraph
I
S plus
2
plus 1
C I u ¨bergeben. Die S-K-I-Graph-Reduktionsmaschine erzeugt gem¨aß 1., 3. und 2, zun¨ achst: Keller
S
I
S plus
plus 1
2
C I Die Anwendung der Graph-Reduktionsregel f¨ ur S und die entsprechende Modifikation des Kellers liefert: Keller
plus
I
2
C I
plus 1
4.3 Hardware – Unterst¨ utzte Implementierungen
321
Gem¨ aß 2., 4. und 3. werden die Eintr¨ age im Keller erweitert zu Keller plus plus
I
C
plus 1
2
C I Die Anwendung der Graph-Reduktionsregel f¨ ur C und die entsprechende Modifikation des Kellers liefert: Keller plus plus
I
I 2
I
plus 1 Die Erweiterung des Kellers gem¨ aß 2., die Anwendung der Graph-Reduktionsregel f¨ ur I und die entsprechende Modifikation des Kellers liefert: Keller plus plus
I
2
plus 1
322
4 Implementierungstechniken
Die Erweiterung des Kellers gem¨ aß 3. und 2. liefert: Keller plus plus
I
plus 2
plus 1 Da jetzt die Argumente von plus Zahlen sind, kann die Graph-Reduktionsregel f¨ ur plus angewandt werden. ¨ Uber den Zwischenschritt Keller plus plus
I
I 3
erh¨ alt man nach dem Eintrag von I in den Keller gem¨aß 2. und der Anwendung der Graph-Reduktionsregel f¨ ur I: Keller plus plus 3
I
I 3
An dieser Stelle zeigt sich deutlich der Steuerungsmechanismus der einzelnen Reduktionsschritte durch den Keller:
4.3 Hardware – Unterst¨ utzte Implementierungen
323
Auf den Knoten I 3 zeigen Verweise von zwei anderen Knoten. Die durch Anwendung eines Reduktionsschritts bewirkten Ver¨ anderungen beziehen sich jedoch stets nur auf diejenigen Knoten, auf die die obersten Eintr¨age im Keller zeigen. Alle u andert. Dies ist auch der Grund, warum ¨brigen Knoten bleiben unver¨ die Graph-Reduktionsregel plus m n ⇒ m + n intern als plus m n ⇒ I (m + n) realisiert wird. In den Keller wird jetzt gem¨ aß 4. und 2. eingetragen:
Keller plus I
plus 3
I
I 3
Die Anwendung der Graph-Reduktionsregel f¨ ur I und die entsprechende Modifikation des Kellers liefert:
Keller plus plus 3
I 3
324
4 Implementierungstechniken
Gem¨ aß 4. und 2. wird in den Keller eingetragen: Keller plus plus 3
I
I 3
Die nochmalige Anwendung der Graph-Reduktionsregel f¨ ur I und die entsprechende Modifikation des Kellers liefert: Keller
3
plus plus 3
Die Anwendung der Graph-Reduktionsregel f¨ ur plus liefert: Keller
6
Das Endergebnis ist: 6 4.3.4 Die Manchester-Datenflußmaschine 4.3.4.1 Einleitung Die Arbeiten zur Manchester-Datenflußmaschine begannen im Jahr 1975. Zun¨ achst wurde das Modell f¨ ur eine Datenflußmaschine, eine h¨ohere Daten¨ flußsprache (genannt LAPSE) und ein Ubersetzer f¨ ur LAPSE-Programme entwickelt. F¨ ur das Maschinenmodell wurde danach ein Simulator erstellt und ausf¨ uhrlich getestet. Beschreibungen des Maschinenmodells findet man in Treleaven (Treleaven 1978), Gurd (Gurd 1978) und Watson (Watson 1979). Mit der hardwarem¨ aßigen Realisierung wurde 1979 begonnen. Die ersten Programme konnten hierauf Anfang 1982 ausgef¨ uhrt werden. Beschreibungen der Hardware und Leistungsvergleiche mit existierenden von-Neumann-Rechnern
4.3 Hardware – Unterst¨ utzte Implementierungen
325
findet man in Gurd (Gurd 1983, Gurd 1985a, b) und Barahona (Barahona 1986). Die Programmierung der Manchester-Datenflußmaschine erfolgt inzwischen in der h¨ oheren Datensprache SISAL, der Nachfolgesprache von LAPSE. Eine Beschreibung von SISAL findet man in McGraw (McGraw 1983). SISALProgramme werden zun¨ achst in Programme der Zwischensprache TASS und danach in Maschinenbefehle u ¨bersetzt. Alle Maschinenbefehle ben¨ otigen zur Ausf¨ uhrung ein oder zwei Token und erzeugen auch jeweils ein oder zwei Token. Beispiele f¨ ur Maschinenbefehle sind: DUP (Duplicate) Der Befehl DUP ben¨ otigt ein Eingabetoken und erzeugt zwei Ausgabetoken, die den gleichen Wert wie das Eingabetoken besitzen. In einem Datenflußgraphen entspricht er einem Knoten x DUP x x CGR (Compare Floating point) Der Befehl CGR entspricht einem Knoten x ∈ F LOAT
y ∈ F LOAT
CGR mit
x true falls x > 0 z= f alse sonst. BRR (Branch)
Der Befehl BRR entspricht einem Knoten x ∈ {true, f alse}
y
BRR y falls x = true
y falls x = f alse
Der Wert von y wird in Abh¨ angigkeit vom (Booleschen) Wert von x auf eine der beiden Ausgabekanten weitergegeben.
326
4 Implementierungstechniken
Arithmetische Operationen Es gibt die u ¨blichen arithmetischen Operationen wie ADR (Add floating-point values), MLR (multiply floating-point values) usw.. I/O-Befehle Die I/O-Befehle dienen zur Kommunikation mit der Peripherie ADL (Add to iteration level), SIL (Set iteration level) Der ADL-Befehl und der SIL-Befehl dienen zur Steuerung der einzelnen Iterations- bzw. Rekursionsschritte eines Programms. Sie werden in dem folgenden Beispiel n¨ aher erl¨ autert. Beispiel 4.3-11 Es soll die Fl¨ ache unter der Kurve y = x2 im Intervall [0, 1] nach der Trapezregel berechnet werden. Das entsprechende SISAL-Programm lautet: export Integrate function Integrate (returns real) for initial int := 0.0; y := 0.0; x := 0.02 while x < 1.0 repeat int := 0.01 ∗ (old y + y); y := old x ∗ old x x := old x + 0.02 returns value of sum int end for end function ¨ Der folgende Graph zeigt den durch die Ubersetzung erzeugten Maschinencode. Die danach folgenden Graphen zeigen sukzessive die ersten Schritte der Ausf¨ uhrung des Programms. Diejenigen Maschinenbefehle, die jeweils ausgef¨ uhrt werden k¨onnen, sind voll gezeichnet. Bemerkenswert ist vor allem die Situation (g). Hier wird zum ersten Mal ein Befehl ADL ausgef¨ uhrt. Dadurch wird die Iterations- bzw. Rekursionsstufe des Programms um 1 erh¨ oht. In der dritten Komponente eines jeden Tokens wird diese Information gespeichert, damit nur Token aus derselben Iterationsbzw. Rekursionsstufe zu einer Applikation zusammengefaßt werden. Der Befehl ADL (x, i) erh¨ oht die dritte Komponente des Tokens x um i. Durch SIL (x, j) kann diese mit dem Wert j vorbesetzt werden. Im Beispiel wird davon ausgegangen, daß die dritten Komponenten von x, y und int mit 0 vorbesetzt sind.
4.3 Hardware – Unterst¨ utzte Implementierungen
Startwerte int = 0.0
y = 0.0
x = 0.02
1.0
x = 0.02
CGR
DUP BRR F
DUP
T BRR T F
F
BRR T DUP
SIL
0
DUP
ADR
0.02
MLR
ADL
1
DUP DUP ADR
0.01
ADL 1 MLR
ADR
ADL
Endresultat
1
327
328
4 Implementierungstechniken Startwerte
1.0
Startwerte
1.0
CGR 0.0
CGR
0.0
BRR F
0.02
TRUE DUP
DUP BRR
DUP
T
F BRR T F
F
DUP
T BRR T F
BRR T
F
DUP
DUP
0
SIL
BRR T
DUP
ADR
0.02
DUP
ADR
0.02
MLR
ADL
1
MLR
ADL
1
0
SIL
DUP
DUP DUP
DUP ADR
0.01
ADR
0.01
ADL 1
ADL 1 MLR
MLR
ADR
ADR
ADL
ADL
1
Endresultat
1
Endresultat (b)
(a) Startwerte
1.0 0.0
Startwerte
1.0
CGR
0.0
0.02
CGR
0.0
0.02
DUP TRUE BRR F
DUP
DUP
TRUE
BRR F
T BRR T F
F
DUP
T BRR T F
BRR T
TRUE
TRUE BRR T F
DUP
SIL
0
DUP
DUP
ADR
0.02
MLR
ADL
1
0.0
SIL
DUP
0
DUP
ADR
0.02
MLR
ADL
1
DUP
DUP
DUP
ADR
0.01
ADR
0.01 ADL 1
ADL 1
MLR
MLR
ADR
ADR
ADL
ADL
1
Endresultat
1
Endresultat (c)
(d)
4.3 Hardware – Unterst¨ utzte Implementierungen Startwerte
1.0
Startwerte
1.0
CGR
CGR
DUP
DUP BRR F
BRR
DUP
F
T BRR T F
F
DUP
T BRR T F
BRR T 0.02
F
DUP
DUP
DUP
ADR
0.02
0.0
0.02
0.02
DUP
ADR
0.02
MLR
ADL
1
0.0 MLR
0.0
0
SIL
BRR T
ADL
1
0.0
0
SIL
DUP
DUP DUP
DUP ADR
0.01
ADR
0.01
ADL 1
ADL 1 MLR
MLR
ADR
ADR
ADL
ADL
1
Endresultat
1
Endresultat (f)
(e) Startwerte
1.0
Startwerte
1.0
CGR
CGR
DUP BRR F
DUP DUP
BRR
T
F BRR T F
F
DUP
T BRR T F
BRR T
F
DUP
DUP 0.0
0.02 MLR
0.0
DUP
ADR 0.02
BRR T
0.02
0.04 ADL
DUP
ADR
0.02
MLR
ADL
1
0.0
1
0.0
0.0004 SIL
0
SIL
DUP
0
DUP
DUP
DUP
ADR
0.01
ADR
0.01 ADL 1
ADL 1
MLR
MLR
ADR
ADR
ADL
ADL
1
Endresultat
1
Endresultat (g)
0.04
(h)
329
330
4 Implementierungstechniken Startwerte
1.0
CGR
DUP BRR F
DUP
T BRR T F
F
BRR T DUP
DUP
ADR
0.02
MLR
ADL
1
0.0 0.0
0
SIL
DUP 0.0004
DUP 0.0004
ADR
0.01
0.04
ADL 1 0.04
MLR
ADR
ADL
1
Endresultat (i)
4.3.4.2 Rechnerstruktur Der Rechner besteht aus f¨ unf Einheiten - I/O-Switch - Token Queue - Matching Unit (mit einer zus¨ atzlichen Overflow Unit) - Instruction Store - Processing Unit. Diese Einheiten sind ringf¨ ormig durch Busse verbunden: Das Zusammenwirken der Basiseinheiten und ihre interne Struktur soll im folgenden n¨ aher erl¨ autert werden. Zus¨ atzliche Informationen findet man in Gurd (Gurd 1985)
4.3 Hardware – Unterst¨ utzte Implementierungen
331
zum Host(168 Kbytes/sec max.) (14 Ktokens/sec max.)
token packets Token Queue token packets Matching Unit I/O-Switch
Overflow Unit
token-pair packets Instruction executable packets Processing Unit token packets
vom Host(168 Kbytes/sec max.) (14 Ktokens/sec max.) I/O -Switch Die Ein- und Ausgabe der Datenflußmaschine erfolgt u ¨ber einen separaten Host-Rechner. Das I/O—Switch u ¨bergibt die von der Processing Unit abgesandten Ausgabetoken an den Host-Rechner. Die u ¨brigen Token werden an die Einheit Token Queue weitergeleitet. Vom Host-Rechner u ¨bersandte Eingabetoken werden in diesem Tokenstrom eingef¨ ugt. Token Queue Diese Einheit besteht aus einer einfachen FIFA-Warteschlange und dient lediglich als Puffer f¨ ur die Matching Unit. Sie gew¨ahrleistet, daß die Matching Unit gleichm¨ aßiger mit Token beliefert wird. Die weitergeleiteten Token be-
332
4 Implementierungstechniken
sitzen die Gestalt < W ert (37 bit), M arkierung (36 bit), Zieladresse (22 bit), Systembit > . Die Komponente Markierung“ dient haupts¨ achlich zur Angabe der Iterations” bzw. Rekursionsstufe. Dar¨ uber hinaus k¨ onnen hier Informationen gespeichert werden, die es erlauben, komplexe Datenstrukturen, wie Felder, in Teilkomponenten zu zerlegen, seperat zu bearbeiten und anschließend wieder zusammenzusetzen. Das Systembit dient zur Kennzeichnung spezieller System“-Token ” und ist f¨ ur die weitere Beschreibung hier nicht wesentlich. 96-bit tokens
von Switch (52.44 Mbytes/sec max.)
Token Queue Input Buffer
Fifo Store
32 Ktokens
Token Queue Store Buffer
Token Queue Output Buffer 96-bit tokens
(52.44 Mbytes/sec max.) zur Matching Uni
Matching.Unit: Die Matching Unit faßt je zwei Token, die zur Bildung einer zweiparametrigen Applikation ben¨ otigt werden, zu einem Paar zusammen. Token, die f¨ ur einparametrige Applikationen bestimmt sind, passieren die Matching Unit unver¨ andert. Token-Paare besitzen die Gestalt: < W ert1 (37 bit), W ert2 (37 bit), M arkierung (36 bit), Zieladresse (22 bit), Systembit > .
4.3 Hardware – Unterst¨ utzte Implementierungen
333
¨ Die Uberpr¨ ufung, ob zwei Token zu einem Paar zusammengefaßt werden k¨onnen, erfolgt durch einen Hash-Mechanismus. Die Hash-Tabelle besteht aus 16 parallelen Untertabellen (hash table boards). In jeder Untertabelle k¨onnen bis zu einer Millionen Token abgespeichert werden. F¨ ur jedes Token wird beim Passieren des Matching Unit Hash Buffers aus einer Markierung und seiner Zieladresse eine 16-bit Adresse berechnet. In jeder Untertabelle wird unter dieser Adresse nachgeschaut, ob dort bereits ein Token mit gleicher Markierung und Zieladresse abgespeichert ist. Ist dies der Fall, so wird aus beiden Token ein Tokenpaar gebildet. Im anderen Fall wird das ankommende Token unter der berechneten Adresse in einer der Untertabellen gespeichert, falls diese Adresse in einer Untertabelle noch nicht mit einem Token belegt ist, oder an die Overflow Unit zur Zwischenspeicherung weitergegeben.
vom Token Queue (52.44 Mbytes/sec max.) Matching Unit Input Buffer Matching Unit Merge Buffer
Overflow Return Buffer
Overflow bus
Matching Unit Hash Buffer Parallel Hash Table
64 K1.25 M tokens
Overflow I/O Control Overflow Data Store
Matching Unit Store Buffer Matching Unit Split Buffer
Overflow Link Store
Overflow Send Buffer
Matching Unit Output Buffer 133-bit token pairs
zum Instruction Store (74.29 Mbytes/sec max.)
Instruction Store Der Instruction Store enth¨ alt den Maschinencode des auszuf¨ uhrenden Programms. Der Zugriff auf die einzelnen Instruktionen erfolgt zweistufig. Hierzu ist der Instruktionsspeicher in 64 Segmente eingeteilt. In einer Segment Ta-
334
4 Implementierungstechniken
ble ist f¨ ur jedes Segment die Angabe seiner Anfangsadresse und seiner L¨ange gespeichert. Die Zieladresse eines Token-Paares enth¨alt u.a. eine 6-bit-lange Segmentadresse und eine 12-bit-lange Relativadresse (offset). Die Segmentadresse bezieht sich auf einen Eintrag in Segment Table. Aus ihr wird die Anfangsadresse des betreffenden Tokens entnommen und zu ihr die Relativadresse addiert, falls diese nicht gr¨ oßer als die L¨ange des Segments ist. Das Ergebnis ist die Absolutadresse der Instruktion, f¨ ur die das Tokenpaar bestimmt ist. Das Token-Paar wird mit einer Kopie der Instruktion zu einer ausf¨ uhrbaren Applikation (executable package) zusammengefaßt. Einparametrige Instruktionen sind in dem Format < Instruktion (10 bit), Zieladresse (22 bit)>, zweiparametrige Instruktionen wie ADL in dem Format < Instruktion (10 bit), Zieladresse (22 bit), Konstante (37 bit)> und Instruktionen wie DUP oder Verzweigungen in dem Format < Instruktion (10 bit), Zieladresse (22 bit), Konstante (22 bit)> abgespeichert. Das an die Processing Unit u ¨bergebene Executable Package hat somit das Format < Wert (37 bit), Wert (37 bit), Instruktion (10 bit), Markierung (36 bit), Zieladresse (22 bit), Zieladresse (22 bit (optional)), Systembit> .
96-bit token pairs
Matching Unit (74.29 Mbytes(sec max.)
Instruction Store Input Buffer Segment Table
64 Entries
Instruction Store
64K Instruction
Instruction Store Ouput Buffer 166-bit executable packages
zur Processing Unit (91.77 Mbytes/sec max.)
4.3 Hardware – Unterst¨ utzte Implementierungen
335
Processing Unit In der Processing Unit werden die Applikationen ausgef¨ uhrt. Hierzu verf¨ ugt sie u ¨ber bis zu 20 identische und parallel angeordnete Applikationseinheiten (Function Units). Jede Funktionseinheit ist mikroprogrammiert und enth¨alt einen 24-bit-Prozessor, I/0-Puffer, 51 Register und 4K Speicher. Ankommende Executable Pakkages werden an die n¨ achste freie Function Unit zur Ausf¨ uhrung u ¨bergeben. Die Ergebnistoken besitzen das Format < Wert (37 bit), Markierung (36 bit), Zieladresse (22 bit), Systembit> und werden an das I/O-Switch weitergeleitet. 133-bit executable packages
von Instruction Store (91.77 Mbytes(sec max.)
Function InputBuffer Preprocessor Preprocessor Output Buffer Function Unit Distribute Buffer arbitration bus (52.4 Mbytes/ sec max.) Function Unit Function Unit .. . Function Unit Function Unit Arbitration Buffer (91.77 Mbytes/ sec max.) distribution bus Processing Unit Ouput Buffer
zum Switch (52.44 Mbytes/sec max.)
96-bit tokens
336
4 Implementierungstechniken
Das an der Universit¨ at von Manchester realisierte Projekt zum Bau einer Datenflußmaschine hat gezeigt, daß Datenflußkonzepte auch praktisch realisiert werden k¨ onnen. Erste Experimente und Benchmark-Tests haben ergeben, daß die Manchester-Datenflußmaschine durchaus mit konventionellen vonNeumann-Maschinen konkurrieren kann. Ihr Vorteil zeigt sich vor allem bei der Bearbeitung von Parallel-Algorithmen. Andererseits haben sich auch die Schwachstellen der Maschine herauskristallisiert. Hier ist vor allem die Matching Unit zu nennen, die zu zeitaufwendig arbeitet. Die weiteren Arbeiten an der Manchester-Datenflußmaschine besch¨ aftigten sich daher u ¨berwiegend mit Optimierungen des bestehenden Konzepts.
Literaturverzeichnis
Abelson, H., Jay Sussman, G. (1996) Structure and Interpretation of Computer Programs (SICP), MIT-Press, zweite Auflage Ackermann, W. (1928) Zum Hilbertschen Aufbau der reellen Zahlen Math. Ann. 99, S. 118-133 Ackermann, D. (1984) Eine baumartige Reduktionsmaschine f¨ ur kombinatorische Terme. Diplomarbeit, Institut f¨ ur Informatik und Praktische Mathematik, Universit¨ at Kiel Allen, J. (1978) Anatomy of LISP. McGraw-Hill Book Company, Computer Science Series Amamya, M., Takesue, M., Hasegawa, R., Mikami, H. (1986) Implementation and Evaluation of a List-Processing-Oriented Data Flow Machine. Proc. 13th Annual International Symposium on Computer Architecture Computer Architecture News, vol. 14, No. 2, pp 10-19 Armstrong, J., Williams, M., Virding, R., Wikstroem, C. (1996) Concurrent programming in Erlang, 2nd edition, Prentice Hall Appel, A. W (1992) Compiling with Continuations, Cambridge University Press Arvind, G.K.P., Plouffe, W. (1980) An Asynchronous Programming Language and Computing Machine Techn. Report TR 114a, Dept. of Information and Computer Science, Univ. of California, Irvine Augustsson, L., Haskell, B. (1993) User’s manual version 0.999.4 Backus, J. (1972) Reduction languages and variable-free programming IBM Research Report, RJ 1010 Backus, J. (1973) Programming Language Semantics and Closed Applicative Languages Proc. ACM Symposium on Principles of Program. Languages, Boston/Mass. Backus, J. (1978) Can Programming be liberated from the von Neumann Style? - A Functional Style and its Algebra of Programs CACM, vol. 21, No. 8, pp 613-641 Backus, J.(1981) The Algebra of Functional Programs: Function Level Reasoning, Linear Equations and Extended Definitions. Proc. Intern. Colloquium on Formalization of Programming Concepts, Lecture Notes in Computer Science, vol. 107, pp 2-41
338
Literaturverzeichnis
Backus, J. (1981b) Function Level Programs as Mathematical Objects Proc. ACM Conference on Functional Programming Languages and Computer Architecture. Portsmouth, pp 1-10 Baker, H.G. (1978) Shallow Binding in LISP. 1.5 CACM, vol. 21, No. 7, pp 565-569 Barahona, P.M.C.C., Gurd, J.R. (1986) Processor Allocation in a Multi-ring Dataflow Machine. Journal of Parallel and Distributed Computing, vol. 3, No. 3, pp 305-327 Barendregt, H.P. (1971) Some Extensional Term Models for Combinatory Logics and Lambda-calculi. Dissertation, Universit¨ at Utrecht Barendregt, H.P. (1977) The Type Free Lambda Calculus in J. Barwise et al. (Hrsg.): Handbook of Mathematical Logic, Studies in Logic and the Foundations of Mathematics, vol. 90, North-Holland Barendregt, H.P. (1981) The Lambda Calculus - Its Syntax and Semantics Barlett, J. (1993) Scheme (Scheme to C compiler) ver. 15mar1993. Technical report, Digital Western Research Laboratory J. Barwise et al. (Hrsg.): Studies in Logic and the Foundations of Mathematics, vol. 103, North-Holland Bauchrowitz, N. (1980) Vergleich einer operationellen mit einer denotationellen Semantik f¨ ur LISP. Diplomarbeit, Institut f¨ ur Informatik und Praktische Mathematik, Universit¨ at Kiel Bauer, F.L., W¨ ossner, H. (1981) Algorithmische Sprache und Programmentwicklung. Springer-Verlag Beckmann (1987) Ein LISP-Interpretierer mit LCC-Optimierung - Implementation und Benchmarktests. Diplomarbeit, Institut f¨ ur Numerische und Instrumentelle Mathematik-Informatik, Universit¨ at M¨ unster Bellegarde, F. (1984) Rewriting Systems on FP-expressions that reduce the number of sequences they yield. Proc. ACM Symposium LISP and Functional Programming, pp 63-73 Berkling, K.J. (1971) A Computing Machine based on Tree Structures. IEEE Trans. on Computers, vol. C-20, No. 4, pp 404-418 Berkling, K.J. (1975) Reduction Languages for Reduction Machines. Proc. 2nd Annual Symposium on Computer Architecture, ACM/IEEE 7SCH0916- 7C, pp 20-22 Berkling, K.J. (1976a) Reduction Languages for Reduction Machines. Interner Bericht ISF-76-8, GMD, St. Augustin Berkling, K.J. (1976b) A Symmetric Complement to the Lambda Calculus. Interner Bericht ISF-76-7, GMD, St. Augustin Berkling K.J. (1978) Computer Architecture for Correct Programming. Proc. Sth Annual Symp. on Computer Architecture, ACM/IEEE 78CH 1284-9C, pp 78-84 Berkling K.J., Fehr, E.A. (1982) A Modification of the Lambda-Calculus as a Base for Functional Programming Languages. ICALP 1982, LNCS vol. 140, pp 35-47 Berkling, K.J. (1983a) GMD-Reduktionsmaschine Arbeitspapier zum Tutorium Nichtprozedurale Sprachen“. Jahrestagung der GI, Hamburg ” Berkling, K.J. (1983b) Experiences with Integrated Parts of the GMD-ReductionLanguage Machine in B. Randell. P.C. Treleaven (Hrsg.): VLSI-Architecture, Prentice Hall, London Berry, D.M. (1971) Block Structure: Retention or Deletion. Proc. Third Annual ACM Symposium on Theory of Computing Berry, G. / Levy, J.J. (1977) Minimal and optimal computations of recursive programms. 4th ACM-SIGACT-SIGPLAN-POPL, Santa Monica, USA
Literaturverzeichnis
339
Bird, R. / Wadler, Ph. (1998) Introduction to Functional Programming using Haskell Prentice Hall B¨ ohm, C. (1968) Alcune proprieta della forma ß-eta-normali del Lambda- K-Calcolo. Publ. IAC-CNR Nr. 696, Roma B¨ ohm, C. (Ed.) (1975) Lambda-Calculus and Computer Science LNCS vol. 37, Springer-Verlag, Berlin B¨ ohm, C. (1982) Combinatory Foundation of Functional Programming. Proc. ACM Symposium on LISP and Functional Programming, pp 29-36 Boley, H. (1982) Artificial Intelligence Languages and Machines. Bericht No. 94, Fachbereich Informatik, Univ. Hamburg Boolos, G.S., Jeffrey, R.C. (1980) Compatability and Logic. 2nd Edition, Cambridge University Press Bossi, A., Ghezzi, C. (1984) Using FP as a Query Language for Relational DataBases. Computer Languages, vol. 9, No. 1, pp 25-37 Burge, W. (1978) Recursive Techniques. Addison-Wesley, Reading MA Bretthauer, H., Christaleler, T., Friedrich, H., Goerigk, W., Ehicking, W., Hoffmann, U., Kind, A., Klude, B., Knutzen, H., Kopp, J., Kriegel, E. U., Mohr, I., Rosenm¨ uller, R., Simon, F. ( 1994) Von der APPLY-Methodik zum System. Christian-Albrechts-Universit¨ at Kiel Buchanan, B. Shortliffe, E. H. (1984) Rule-based Expert Systems: The MYCIN Experiments of the Stanford Heuristic Programming Project, Addison-Wesley Campbell, J.A. (1984) Implementations of PROLOG Ellis Harwood Series Artificial Intelligence. Chichester, England Chailloux, J. (1980) Le Modele VLISP: Description, Implementation et Evaluation. Bericht Nr 80-20, Laboratoire Informatique Theorique et Programmation, Univ. Pierre et Marie Curie, Paris 6 Chakravarty, M. M. T., Keller, G. C. (2004) Einf¨ uhrung in die Programmierung mit Haskell. Pearson Studium Church, A. (1932) A Set of Postulates for the Foundation of Logic Annals of Math. (2) 33 (1932), pp 346-366 und (2) 34, pp 839-864 Church, A. (1936a) A Note on the Entscheidungsproblem. Journal of Symbolic Logic 1, pp 40-1, pp 101-2 Church, A. (1936b) An Unsolvable Problem of Elementary Number Theory. American Journal of Mathematics 58, pp 345-63 Church, A. (1941) The Calculi of Lambda-conversion. Princeton University Press Clarke T.J.W., Gladstone, P.J.S., McLean, C.D., Norman, A.C. (1980) SKIM - the S, K, I Reduction Machine. Proc. 1980 LISP Conference, Stanford/CA, pp 128135 Clocksin, W.F., Mellish, C.S. (1981) Programming in PROLOG. Springer-Verlag Comte, D., Hifdi, N. (1979) LAU Multiprocessor: Microfunctional Description and Technological Choices. Proc. Ist European Conference Parallel and Distributed Processing, Toulouse, pp 8-15 Cornish, M. (1979) The TI Data Flow Architectures: The Power of Concurrency for Avionics. Proc. 3rd Conference Digital Avionics Systems, Fort Worth, Texas, IEEE, New York, pp 19-25 Curry, H.B. (1930) Grundlagen der kombinatorischen Logik. Amer. J. Math. vol. 52, pp 509-536 and pp 789-834 Curry, H.B., Feys, R., Craig, W. (1958) Combinatory Logic vol. I, North-Holland Co.
340
Literaturverzeichnis
Curry, H.B., Seldin, J., Hindley, R. (1971) Combinatory Logic. vol. II, North-Holland Co. Darlington, J., Reeve, M. (1981) ALICE: A Multi-processor Reduction Machine for the Parallel Evaluation of Applicative Languages. Proc. ACM Conference on Functional Programming Languages and Computer Architecture, pp 65-76 Darlington, J. et al. (1982) Functional Programming and its Application. Cambridge University Press Davenport, Y.S., Torunier, E. (1988) Computer Algebra: Systems and Algorithms for Algebraic Computation, Academic Press Davie, A., (1992) An Introduction into Functional Programming System Using Haskell. Cambridge University Press Davis, A.L. (1978) The Architecture and System Method of DDM1: A recursively structured Data Driven Machine. Proc. 5th ACM Symposium on Computer Architecture, SIGARCH Newsletter, vol. 6, No. 7, pp 210-215 Davis, A.L. (1979a) DDDN’s - A Low Level Program Schema for Fully Distributed Systems. Proc. Ist European Conference Parallel and Distributed Processing, Toulouse, pp 1-7 Davis, A.L. (1979b) A Data Flow Evaluation System Based on the Concept of Recursive Locality. Proc. 1979 National Computer Conference, vol. 48, AFIPS Press, pp 1079-1086 Davis A.L., Keller R.M. (1982) Data Flow Program Graphs. IEEE Computer, vol. 15, No. 2, pp 26-41 Davey, B. A., Priesley, H. A. (1990) Introduction to Lattices and Order. Cambridge University Press Dennis, J.B., Fosseen, J.B., Lindermann, J.P. (1972) Data Flow Schemas. Proc. Symposium on Theoretical Programming, Novosibirsk, UDSSR, pp 187-216 Dennis, J.B., Fosseen, J.B., Linderman, J.P. (1974a) Data Flow Schemas LNCS, vol. S Dennis, J.B. (1974b) First Version of a Data Flow Procedure Language LNCS, vol. 19 Dennis, J.B. (1975a) First Version of a Data Flow Procedure Language Computation Structures, Group Memo 93, Project MAC, MIT Dennis, J.B., Misunas, D.P. (1975b) A Preliminary Architecture for Basic Data Flow Processors. Proc. 2nd Ann. Symp. on Computer Architecture, IEEE, New York, pp 126-132 Dennis, J.P. (1975c) Packet Communication Architecture. Proc.1975 Sagamore Computer Conf. on Parallel Processing, pp 224-229 Di Cosmo, R. (1995) Isomorphisms of types: from lambda-calculus to information retrieval and language design. Progress in theoretical computer science, Birkhauser Diller, A. (1988) Compiling Functional Languages. John Wiley & Sons Ltd ¨ Doberkat, E.-E., Fox, D. (1990) Praktischer Ubersetzerbau, B. G. Teubner Stuttgart Dosch, W., M¨ oller, B. (1983) An Algebraic Semantics for Backus’ Functional Programming Language with Infinite Objects. Informatik Fachberichte Bd. 73, (1. Kupka, Ed.), pp 67-85 Dosch, W., M¨ oller, B. (1984) Busy and Lazy FP with Infinite Objects. Proc. ACM Symposium on LISP and Functional Programming, Austin, Texas Dosch, W. (1989) Funktionale und Logische Programmierungs-Sprachen, Methoden, Implementationen. Bericht u alischen ¨ber das Arbeitstreffen der Rheinisch-Westf¨
Literaturverzeichnis
341
Technischen Hochschule Aachen-Christian-Albrecht-Universit¨ at Kiel — Technischen Universit¨ at M¨ unchen im S¨ ollerhaus in Hirschegg, Kleinwalsertal Eick, A., Fehr, E. (1983) Inconsistencies of Pure LISP LNCS, vol. 14S, pp 101-110 Fehr, E. (1982) A Modification of the Lambda-Calculus as a base for Functional Programming Languages. ICALP 82, LNCS 140, pp 35-47, Springer-Verlag Fehr, E. (1984) Dokumentation eines PROLOG-Interpretierers implemen- tiert in der funktionalen Sprache BRL. Arbeitsbericht der GMD Nr. 122, GMD-FIP, St. Augustin Fehr, E. (1989) Semantik von Programmiersprachen. Springer Verlag Felgentreu, K.-U. (1984) Implementierung eines schnellen LISP-Interpretierers Diplomarbeit, Institut f¨ ur Informatik und Praktische Mathematik, Universit¨ at Kiel Felgentreu, K.-U. (1985) Decidability Problems concerning the Optimization of Function Calls. Bericht Nr. 3/85-I, Reihe Angewandte Mathematik und Informatik, FB 15, Universit¨ at M¨ unster Felgentreu, K.-U., Lippe, W.-M. (1986a) Dynamic Optimization of Covered Tail Recursive Functions in Applicative Languages. Proc. 1986 ACM Computer Science Conference, Cincinnati/Ohio, pp 293-299 Felgentreu, K.-U., Lippe, W.-M. (1986b) A General Approach to the Optimization of Function Calls. Proc. European Symposium on Programming 1986, LNCS, vol. 213, pp 41-52 Felgentreu, K.-U., Lippe, W.-M. (1986c) Low Cost Environment Changing in a Shaddow Binding System. New Generation Computing, vol. 4, No. 3, pp 245-272 Felgentreu, K.-U. (1986d) Ein optimierter Static Scope-Interpretierer f¨ ur funktionale Sprachen auf der Basis ALGOL-¨ ahnlicher Laufzeitkeller 16. Jahrestagung der GI, Informatik Fachberichte, Band 126, S. 165-179 Felgentreu, K.-U., Lippe, W.-M. (1987) Optimizing Static Scope LISP by Repetitive Interpretation of Recursive Function Calls. IEEE Transactions on Software Engineering, Juni/July Field, A. J., Harrison, P. G. (1988) Functional Programming, Addison-Wesley Fischer, A. E., Grodzinsky, F. S. (1993) The anatomy of Programming Languages, Prentice Hall Fleck, A.C. (1986) Structuring FP-style Functional Programs Computer Languages, vol. 11, No. 2, pp 55-63 Friedman, D.P., Wise, D.S. (1976) CONS should not evaluate its arguments in S. Michaelson, R. Milner (Eds.): Automata, languages and programming, pp 257-285, Edinburgh University Press, Edingburgh Gabriel, R.P., Masinter, L.M. (1982) Performance of LISP-Systems. Proc. 1982 ACM Symposium on LISP and Functional Programming, Pittsburgh, Pennsylvania, pp 123-142 Gaudiot, J.L., Ercegovac, M.D. (1985) Performance Evaluation of a Simulated DataFlow Computer with Low-Resolution Actors. Journal of Parallel and Distributed Computing, vol. 2, No. 4 Gehlot, V., Srikant, Y.N. (1986) An Interpreter for SLIPS - An Applicative Language based on Lambda-Calculus Computer Languages, vol. 11, No. 1, pp 1-13 Gell, O. et al. (1976) LAU Software System: A High Level Data Driven Language for Parallel Programming. Proc. 1976 International Conference on Parallel Processing Gibert, J., Shepherd, J. (1983a) From Algebra to Compiler: A Combinator-Based Implementation of Functional Programming in Proc. 3rd Conference on Foun-
342
Literaturverzeichnis
dations of Software Technology and Theorectical Computer Science, Bangalore, India, pp 290-314 Gibert, J. (1983b) Functional Programming with Combinators Technical Report, Monash University, Melbourne, Australia Giloi, W.K. (1982) Rechnerarchitektur - heute und morgen. 12. GI-Jahrestagung, Informatik Fachberichte Nr. 57, Springer-Verlag, S. 1-29 Glaser, H., Hankin, C., Till, D. (1984) Principles of Functional Programming. Prentice-Hall Glaser, H., Hayes, S. (1986) Another Implementation Technique for Applicative Languages. Proc. European Symposium on Programming 1986, LNCS, vol. 213, pp 70-81 Gloger, M. (1993) Implementierung funktionaler Programmiersprachen. Deutscher Universit¨ ats Verlag ¨ G¨ odel, K. (1931) Uber formal unentscheidbare S¨ atze der Principa Mathematica und verwandter Systeme I. Monatshefte f¨ ur Mathematik u. Physik 38, pp 172-198 G¨ odel, K. (1934) On Undecidable Propositions of Formal Mathematical Systems. Bericht Institute for Advanced Study, Princeton, N.J. Goerik, W., Borey, H., Hoffmann, U., Perling, M., Sintek, M. (1996) Komplettkom¨ pilation von lisp: eine Studie zur Ubersetzung von lisp-software f¨ ur c-umgebungen. In K¨ unstliche Intelligenz, 20. Deutsche Jahrestagung f¨ ur K¨ unstliche Intelligenz, pp 31-33 Gordon, M.J.C. (1973) Evaluation and Denotation of Pure LISP: A worked Example in Semantics. Dissertation, University of Edinburgh Goos, G., Zimmermann, W. (2006) Vorlesungen u ¨ber Informatik - Grundlagen und funktionales Programmieren. 4. Auflage, Springer-Verlag Gordon, M.J.C. (1979) The Denotational Description of Programming Languages. Springer-Verlag Gostelow K.P., Thomas R.E. (1979a) A View of Data Flow. Proc. National Computer Conference, vol. 48, AFIPS Press, pp 629-636 Gostelow, K.P., Thomas, R.E. (1979b) Performance of a Data Flow Computer. Techn. Report Nr. 127a, Dept. Information and Computer Science, University of California, Irvine Graham, P. (1994) ON LISP, Advanced Techniques for Common Lisp, Prentice Hall Gram, Chr., Organick, E.I. (1980a) An easy functional programming language. Bericht Nr. ID 923, Instituttet for Datateknik, Danmarks Teknishe Hojshole, Lyngby Gram, Chr., Organick, E.I. (1980b) Semantics and Algebra of an Easy Functional Programming Language. Bericht Nr. ID 930, Instituttet for Datateknik, Danmarks Teknishe Hojshole, Lyngby Gram, Chr., Organick, E.I. (1980c) Characteristics of a Functional Programming Language. Techn. Report UUCS-80-103, University of Utah, Salt Lake City Grau, A.A., Hill, U., Langmaack, H. (1967) Translation of ALGOL 60 Handbook for Automatic Computation, vol. 1, Part b, Springer-Verlag Greussay, P. (1977) Contribution a la definition interpretative et a 1’implementation des Lambda-languages. These des Sciences, Universite Paris VII Greussay, P. (1978) Iterative Interpretation of Tail-Recursive LISP Procedures Ecole de la Recherche, Universite Paris Gurd, J.R., Watson, I., Glauert, J.R.W. (1978) A Multilayered Data Flow Computer Architecture. Interner Report, Dept. of Computer Science, University of Manchester
Literaturverzeichnis
343
Gurd, J.R., Glauert, J.R.W., Kirkham, C.C. (1981) Generation of dataflow graphical object code for the LAPSE programming language. Proc. CONPAR’81, LNCS, vol. 111, pp 155-168 Gurd, J.R., Watson, I. (1982) A Practical Data Flow Computer. IEEE Computer, vol. 1S, No. 2, pp 51-57 Gurd, J.R., Watson, I. (1983) A Preliminary Evaluation of a Prototype Data Flow Computer. Proc. 9th IFIPS World Computer Congress, Ed. Elsevier NorthHolland Gurd, J.R., Kirkham, C.C., Watson, I. (1985) The Manchester Prototype Dataflow Computer. CACM, vol. 28, No. 1 Hamann, Ch.-M. (1982) Einf¨ uhrung in das Programmieren in LISP de GruyterVerlag, Berlin Hartimo, I. et al. (1986) DFSP: A Data Flow Signal Processor. IEEE Transactions on Computers, vol. C-35, No. 1, pp 23-32 Hemmendinger, D. (1985) Lazy Evaluation and Cancelation of Computations. Proc. 1985 Conference on Parallel Processing, pp 840-842 Henderson, R., Morris, J.H. (1976) A lazy evaluator. Proc. 3rd ACM Symposium on Principles of Programming Languages, pp 95-103 Henderson, P. (1980) Functional Programming - Application and Implementation. Prentice Hall, International Series in Computer Science Herbrand, J. (1931) Sur la non-contradiction de l’Arithmetique. Journal Reine und angewandte Mathematik 166, pp 1-8 Hermes, H. (1961) Aufz¨ ahlbarkeit, Entscheidbarkeit, Berechenbarkeit. SpringerVerlag, Berlin, Bd. 109 Hindley, J.R., Lercher, B., Seldin, J.P. (1972) Introduction to Combinatory Logic. London Mathematical Society, Lecture Note Series 7, Cambridge Univ.Press Hinze, R. (1992) Einf¨ uhrung in die funktionale Programmierung mit Miranda, B. G. Teubner Stuttart Hinze, R. (1992) Einf¨ uhrung in die funktionale Programmierung mit Miranda, B. G. Teubner Stuttgart Hommes, F. (1975) Simulation einer Reduktionsmaschine. Diplomarbeit, Universit¨ at Bonn Hommes, F. (1977a) The Internal Structure of the Reduction Machine. Interner Bericht, ISF-GMD-77-3, GMD, St. Augustin Hommes, F. (1977b) How to use the Reduction Machine. Interner Bericht 77-2, GMD, St. Augustin Hommes, F. (1977c) The Transformation of LISP Programs into Programs written in the Reduction Language. Interner Bericht 77-4, GMD, St. Augustin Hommes, F. (1980a) An Expression oriented Editor for Languages with a Constructor Syntax. Proc. of the International Workshop on High-Level Language Computer Architecture, Fort Lauderdale, Fla. Hommes, F., Kluge, W., Schl¨ utter, H. (1980b) A Reduction Machine Architecture and Expression oriented Editing. Interner Bericht ISF-80-04, GMD, St. Augustin Hommes, F., Schl¨ utter, H. (1980c) Reduction Machine System User’s Guide. Interner Bericht, GMD, St. Augustin Hommes, F. (1982) The Heap/Substitution Concept - An Implementation of Functional Operations on Data Structures for a Reduction Machine. Proc. of the 9th Annual Symposium on Computer Architecture, Austin/Texas, ISSN 0149-7111
344
Literaturverzeichnis
Hommes, F. (1983) Reduction Machine Simulator Reference Manual. Interner Bericht, GMD, St. Augustin Honschopp, U. (1983a) Implementation der funktionalen Programmiersprache LISP/N. Diplomarbeit, Institut f¨ ur Informatik und Praktische Mathematik, Universit¨ at Kiel Honschopp, U., Lippe, W.-M., Simon, F. (1983b) Compiling Functional Languages for von Neumann Machines. Proc. SIGPLAN ’83 Symposium on Programming Language Issues in Software Systems, San Francisco, Ca, SIGPLAN Notices, Vol. 18, No. 6, pp 22-27 Honschopp, U., Lippe, W.-M., Simon, F. (1983) Laufzeitunterst¨ utzung f¨ ur Programme mit h¨ oheren Funktionalen. Institut f¨ ur Informatik und Praktische Mathmatik, Christian-Albrechts-Universit¨ at Kiel Howe, D. (1992) Miranda to Haskell Compiler. Technical report, Department of Computing, Imperial College, London UK Hudak, P., (1989) Conception, evolution, and application of functional programming languages. ACM Computing Surveys, 21(3):359-411 Huet, G. (1977) Confluent Reduction: Abstract Properties and Applications to Term Rewriting Systems. 18th Annual Symp. on Foundations of Computer Science, IEEE, pp 30-45 Huet, G. (1990) Logical foundations of functional programming. Addison Wesley Hughes, R.J.M. (1982) Super Combinators: A New Implementation Method for Applicative Languages. Proc. 1982 ACM Symposium on LISP an Functional Languages, pp 1-10 Hyland, J. M. E. (1975) A survey of some useful partial order relations on terms of the Lambda-calculus. B¨ ohm, Co. (Ed): Lambda-Calculus and Computer Science, LNCS, vol. 37 Hyland, J.M.E. (1976) A syntactic characterisation of the equality in some models of the Lambda-calculus. Journal London Math. Soc., vol. 12, No. 2, pp 361-370 IEEE (1991) IEEE Standard for the Scheme Programming Language 1178-1990 Iverson, K. (1962) A Programming Language, Wiley, New York Jeuring, J., Meijer, E., eds. (1995) Advanced functional programming. Lecture Notes in Computer Science 925, Springer-Verlag Johnston, J.B. (1971) The Countour Model of Block Structured Prozesses. SIGPLAN Notices, vol. 6, No. 2, pp 55-82 Jones, S. P., (1999) Report on the Programming Language Haskell 98, A Non-strict Purely Functional Language. Yale University, Department of Computer Science Tech Report YALEU/DCS/RR-1106 Jones, S. P., (1999) The Haskell 98 Library Report. Yale University, Department of Computer Science Tech Report YALEU/DCS/RR-1105 Joy, M.S., Rayward-Smith, V.R., Burton, F.W. (1985) Efficient Combinator Code Computer Languages, vol. 10, No. 3/4, pp 211-224 ¨ J¨ urgensen, H. (1974) Zur Ubersetzbarkeit von Programmiersprachen B. Schlender (Ed.): 3. Fachtagung u ¨ber Programmiersprachen der GI, LNCS, vol. 7, pp 34-44 Keller, R.M., Patil, S., Lindstrom, G. (1978) An Architecture for a Loosely-Coupled Parallel Processor. Techn. Report UUCS-78-105, Dept. Computer Science, Univ. of Utah Keller, R.M., Lindstrom, G., Patil, S. (1979) A Loosely-coupled Applicative Multiprocessing System. Proc. 1979 National Computer Conference, pp 613-622
Literaturverzeichnis
345
Kerns, J.R., Long, A.N., Thoreson, S.A. (1986) Performance of Three Dataflow Computers. Proc. 1986 ACM Computer Science Conference, pp 93-100 Kiburtz, R.B. (1981) Transformations on FP Program Schemes. Proc. ACM Conference on Functional Programming Languages and Computer Architecture Portsmouth, N.H., pp 41-48 Kleene, S.C. (1936a) General recursive functions of natural numbers Math. Ann. 112, pp 727-742 Kleene, S.C. (1936b) Lambda-definability and Recursiveness Duke Math. Journal 2, pp 340-353 Kluge, W.E. (1979) The Architecture of a Reduction Language Machine Hardware Model. Interner Bericht, ISF-79-94, GMD, St. Augustin Kluge, W.E., Schl¨ utter, H.S. (1980) An Architecture for Direct Execution of Reduction Languages. Proc. of the Internat. Workshop on High-Level Language Comp. Architecture, Fort Lauderdale, Fla., pp 174-180 Kluge, W.E., Schl¨ utter, H.S. (1983a) Petri-Net Models for the Evaluation of Applicative Programs Based on Lambda-Expressions. IEEE-TSE, vol. SE-9, No. 3 Kluge, W.E. (1983b) A Concept for Cooperating Reduction Machines. Proc. 2nd International Workshop on High-Level Computer Architecture, Fort Lauderdale, Fla. Kluge, W.E. (1983c) Cooperating Reduction Machines. IEEE-TC, vol. C-32 Kluge, W.E., Schmittgen, C. (1983d) A System Architecture for the Concurrent Evaluation of Applicative Program Expression. Proc. 10th Annual Symposium on Computer Architectures, Stockholm, ACM 0149-7111, pp 356-362 Kluge, W.E. (1984) Datenverarbeitung durch Reduktion. GMD-Spiegel, 1/84, GMD, St. Augustin, pp 27-31 Kluge, W. (1992) The organization of reduction, data flow, and control flow systems. MIT Press Kolb, D. (1980) Siemens-Interlisp Benutzerhandbuch. Siemens AG, M¨ unchen Kosinski, P.R. (1973) A Data Flow Language for Operating Systems Programming. ACM SIGPLAN Notices, vol. 8, No. 9, pp 89-94 Kuchen, H. (1998) Workshop on Functional and Logic Programming Proceedings. Working Paper No. 63, University of M¨ unster, Institute of Business Informatics. Landin, P.J. (1965) A Correspondence between ALGOL 60 and Church’s LambdaNotation. CACM, vol. 8, Part I, pp 89-101, Part II, pp 158-165 Langmaack, H. (1975) On Cons-free Programming in LISP. Bericht Nr. 7503, Institut f¨ ur Informatik und Praktische Mathematik, Univ. Kiel Leszczylowski, J. (1980) Theory of FP Systems in EDINBURGH LCF. Internal Report No. CSR-61-80, Dept. of Computer Science, University of Edinburgh Levy, J.J. (1976) An Algebraic interpretation of the Lambda-Beta-k-calculus and an application of a labelled Lambda-calculus TCS, vol. 2, No. 1, pp 97-114 Levy, J.J. (1977) Reductions correctes et optimales dans le Lambda-calcul. These de doctorat d’etat, Universite Paris VII Lippe, W.-M., Simon, F. (1979) LISP/N-Basic Definitions and properties Bericht 4/79 des Instituts f¨ ur Informatik und Praktische Mathematik der Universit¨ at Kiel Lippe, W.-M, Simon, F. (1980) Semantics for LISP without Reference to an Interpreter. Proc. Intern. Symposium an Programming, Paris, LNCS, vol. 83, pp 240-256 Lippe W.-M., Simon, F. (1981) Einf¨ uhrung in die Kopierregelsemantik. Proc. Kolloquium u ¨ber denotationelle und Kopierregel-Semantik
346
Literaturverzeichnis
W.-M. Lippe, F. Simon (Hrsg.): Bericht Nr. 8103, Institut f¨ ur Informatik und Praktische Mathematik, Universit¨ at Kiel, S. 1-27 Lippe, W.-M., Simon, F. (1983) Applikatives und Funktionales Programmieren. Institut f¨ ur Informatik und Praktische Mathematik, Christian-AlbrechtsUniversit¨ at, Kiel Loogen, R. (1990) Parallele Implementierung funktionaler Programmiersprachen. Informatik-Fachberichte 232, Springer Verlag Loogen, R. (1993) Funktional-logische Programmiersprachen — Semantik und Implementierung. Von der Mathematisch-Naturwissenschaftlichen Fakult¨ at der Rheinisch-Westf¨ alischen Technischen Hochschule Aachen genehmigte Habilitationsschrift zur Erlangung der venia legendi Mago, G.A. (1979) A network of microprocessors to execute reduction languages. International Journal on Computer and Information Systems, vol. 8, No. 5, pp 349-385 and vol. 8, No. 6, pp 435-471 Mago, G.A. (1980) A Cellular Computer Architecture for Functional Programming. Proc. IEEE COMPCON 80, IEEE, pp 179-187 Mago, G.A.(1981) Program execution on a cellular computer: some Matrix Algorithms. Technical Report, Dept. of Computer Science, Univ. of North Carolina Manna, Z. (1974) Mathematical theory of computation. McGraw-Hill computer science series, McGraw-Hill Book Company Markov, A.H. (1954) Theorie der Algorithmen (russ.). Akad. Nank SSR, Matem. Inst., Moskau/Leningrad Martelli, A., Montanari, U. (1982) An Efficient Unification Algorithm. ACM TOPLAS, vol. 4, No. 2, pp 258-282 Maurer, H. (1969) Theoretische Grundlagen der Programmiersprachen Reihe Informatik, Bd. 404/404a, BI, Mannheim Mayer, O. (1998) Programmieren in Common LISP. 2. Auflage. Spektrum Akademischer Verlag McCarthy, J. (1960) Recursive Functions of Symbolic Expressions and their Computation by Machine. ACM, vol. 3, pp 184-195 McCarthy, J. et al. (1962) LISP 1.5 Programmer’s Manual. MIT Press, Cambridge, Mass. McCarthy, J. (1963) A Basis for a Mathematical Theory of Computation in Braffert, Hirschberg (Ed.): Computer Programming and Formal Systems North-Holland, Amsterdam McCarthy, J. (1977) History of LISP. Proc. of the ACM-Conference in the History of Programming Languages Los Angeles McGraw, J. et al. (1983) SISAL - Streams and Iteration in a Single-Assignment Language. Language Reference Manual (Version 1.0), Lawrence Livermore National Laboratory, Livermore, Calif., July Mechan, J.R. (1979) The New UCI-LISP Manual. Lawrence Erlbaum Ass., Hillsdale, New Jersey Meyer, A.R. (1982) What is a Model of the Lambda Calculus? Information and Control 52, pp 87-122 Miklosko, J., Kotov, V.E. (1984) Algorithms, Software and Hardware of Parallel Computers. Springer-Verlag Milner, R. (1978) Theory of type polymorphism in programming. J. of Computer and System Sciences 17, pp 348-375
Literaturverzeichnis
347
Milner, R. (1984) A Proposal for Standard ML. Proc. 1984 ACM Symposium on LISP and Functional Programming, Austin, Texas, pp 184-197 Milner, R., Tofte, M., Harper, R., MacQueen, D. (1997) MIT-Press Morris, J.-H. (1968) Lambda Calculus Models of Programming Languages Disseration, MIT Muchnick, S., Janes, N.D. (1982) A Fixed-Program Machine for Combinator Expression Evaluation. Proc. ACM Symposium on LISP and Functional Programming, pp 11-20 Norwig, P.(1991) Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp, Morgan Kauffmann publishers Naur, P. et al. (1963) Revised Report on the Algorithmic Language ALGOL 60 Num. Math., vol. 4, pp 420-453 Naur, P. et al. (1963) Revised Report on the Algorithmic Language ALGOL 60, Communications of the ACM 6(1) 1 - 17 Oberhauser, H.-G., Wilhelm, R. (1984) Flow Analysis in Combinator Implementation of Functional Programming Languages. Interner Bericht 04/1984, FB 10Informatik, Universit¨ at des Saarlandes, Saarbr¨ ucken Oberschelp, A. (1981) Berechenbarkeit und Entscheidbarkeit. Vorlesungsskript WS 1977/78, Universit¨ at Kiel ODonnel, M. (1986) Equational Logic as a programming language. MIT Press Okasaki, C. (1996) Purely functional data structures, Cambridge University Press Organick, E.I. (1979) New Directions in Computer Systems Architecture Euromicro Journal, vol. S, No. 4, pp 190-202 Pape, D. (2000) Striktheitsanalysen funktionaler Sprachen. Der Andere Verlag, Osnabr¨ uck PARS (1995) PARS-Workshop Stuttgart. Gesellschaft f¨ ur Informatik e.V., ParallelAlgorithmen, - Rechnerstrukturen und Systemsoftware, Informationstechnische Gesellschaft im VDE Patnaik, L.M., Bhattacharya, P., Ganesh, R. (1984) DFL: A Data Flow Language. Computer Languages, vol. 9, No. 2, pp 97-106 Paulson, L. C. (1996) ML for the working programmer, 2nd edition, Cambridge University Press Pepper, P., Hofstedt, P. (2006) Grundlagen und Konzepte zu strikten und nichtstrikten funktionales Programmiersprachen. Haskell, ML und Opal werden parallel besprochen Perrot, J.F. (1978) Principes d’Implementation de Processus Recursifs. Ecole de la Recherche, Univ. Paris Perrot, J.F. (1979) LISP et Lambda-calcul In Robinet, B. (Ed.): Lambda Calcul et Semantique Formelle des Langages de Programmation. LITP, Universite Paris VII, Paris Peyton Jones, S.L. (1987) The Implementation of Functional Programming Languages. Prentice Hall Plasmeijer, R., von Eckelen, M. Functional programming and parallel graph rewriting, Addison Wesley, International Computer Science Series Plotkin, G.D. (1975) Call-by-name, call-by-value and the Lambda-calculus TCS, vol. 1, pp 125-159 Plotkin, G.D. (1977) LCF as a programming language. TCS, vol. 5, pp 223-257 Post, E.L. (1936) Finite combinatory process-formulation. J. Symbolic Logic I, pp 103-105
348
Literaturverzeichnis
Post, E.L. (1943) Formal Reductions of the General Combinatorial Decision Problem. Am. J. Math. 65, pp 197-215 Rabhi, F., Lapalme, G. (1999) Algorithms: A functional programming approach, Addison-Wesley ¨ Raulefs, P. (1982) Methoden der K¨ unstlichen Intelligenz: Ubersicht und Anwendungen in Experten-systemen 12. Jahrestagung der GI, Informatik-Fachberichte Nr. 57, pp 170-187 Rees, J. and Clinger, W. (1986) The reviesed report on the algorithmic language Scheme. SIG-PLAN Notices, 21(12):37-79 Reynolds, J.C. (1970) GEDANKEN - A simple typeless language based on principle of completeness and reference concept. CACM vol. 13, No. 5, pp 308-319 Robinet, B. (1980a) Programmation sans Variables ou la logique cominatoire a la Backus. Univ. Paris 7, LITP, Rapport No. 80-6 Robinet, B. (1980b) Un modele logico-combinatoire des Systems de Backus. Univ. Paris 7, LITP, Rapport No. 80-21 Robinson, J.H. (1965) A Machine-oriented Logic Based on the Resolution Principle. JACM, vol. 12, pp 23-41 Rogers, H. (1967) Theory of Recursive Functions and Effective Computability. McGraw-Hill Book Company Rosser, J.B. (1935) A Mathematical Logic without Variables. Annals of Math. (2), 36, pp 127-150, 1935 and Duke Math. Journal 1, pp 328-355 Rosser, J.B. (1982) Highlights of the History of the Lambda-Calculus. Proc. 1982 ACM Symposium on LISP and Functional Programming, pp 216-225 Runciman, c., Wakeling, C. (1995) Applications of Functional Programming. UCL Press London Saint-James, E. (1984) Recursion is more efficient than Iteration. Proc. 1984 ACM Symposium on LISP and Functional Programming Sander, H. P. (1992) A Logic of Functional Programs with an Application to Concurrency. Department of Computer Sciences University of G¨ oteborg and Chalmers University of Technology. Sargeant, J., Kirkham, C.C. (1986) Stored Data Structures on the Manchester Dataflow Machine. Proc. 13th Annual International Symposium on Computer Architecture 1986, Computer Architecture News, vol. 14, No. 2, pp 235-242 Schl¨ utter, H. (1983) Introduction to the Reduction Machine Simulator. Interner Bericht ISF, GMD, St. Augustin Schnitter, H. (1983) Introduction to the Reduction Machine Simulator. Interner Bericht ISF, GMD, St. Augustin ¨ Sch¨ onfinkel, M. (1924) Uber die Bausteine der mathematischen Logik. Math. Annalen 92, pp 305-316 Scott, D.S. (1969) Models for the Lambda-calculus. unpublished, 53 pp Scott, D.S., Strachey, C. (1971) Towards a Mathematical Semantics for Computer Languages. Technical Monograph PRG-6 Programming Research Group, University of Oxford Scott, D.S. (1972) Continuous Lattices. Laurere, F.W. (Ed.): Toposes, Algebraic Geometry and Logic Lecture Notes in Mathematics 274, Springer-Verlag Scott, D.S. (1976) Data types as Lattices. SIAM J. Computer 5, pp 522-587 Seldin, J.P., Hindley, J.R. (1980) To H.B. Curry: Essays on Combinatory Logic, Lambda Calculus and Formalism Shoenfield, J.R. (1967) Mathematical Logic. Addison-Wesley Publishing Company
Literaturverzeichnis
349
Siekmann, J. (1979) Matching under Commutativity. LNCS, vol. 72, pp 531-557 Simon, F. (1976) Cons-freies Programmieren in LISP unter Deletion Strategie. Proc. 4. Fachtagung der GI u ¨ber Programmiersprachen, Informatik-Fachberichte, Springer-Verlag, pp 111-123 Simon, F. (1978) Zur Charakterisierung von LISP als ALGOL-¨ ahnliche Programmiersprache mit einem strikt nach dem Kellerprinzip arbeitenden Laufzeitsystem. Bericht Nr. 2178 des Instituts f¨ ur Informatik und Praktische Mathematik, Universit¨ at Kiel Simon, F. (1980) Lambda Calculus and LISP. Bericht Nr. 8006 des Instituts f¨ ur Informatik und Praktische Mathematik, Universit¨ at Kiel Simon, F. (1986) Implementierung von funktionalen und logischen Programmiersprachen. Institut f¨ ur Informatik und Praktische Mathematik, ChristianAlbrechts-Universit¨ at, Kiel Sleep, M. R., Plasmeijer, M. J., Eekelen, M. J., M.C.J.D. (1993) Term graph rewriting. Wiley Stallmann, R.M. (1981) EMACS Manual for its Users. Al Memo No. 554, MIT, Artificial Intelligence Laboratory Steele, G.J. Jr., Sussman, G.J. (1978) The Art of the Interpreter or, The Modularity Complex. Al Memo No. 453, MIT Steele, G. Jr., (1990) Common Lisp the Language, Digital Press; zweite Auflage Stoy, J.E. (1977) Denotational Semantics: The Scott-Strachey Approach to Programming Language. Theory, The MIT Press, Cambridge, Mass. Stoyan, H. (1980) LISP-Anwendungsgebiete, Grundbegriffe, Geschichte AkademieVerlag, Berlin Stoyan, H., G¨ orz, G. (1984) LISP - Eine Einf¨ uhrung in die Programmierung. Springer-Verlag Thiemann, P. (1994) Grundlagen der funktionalen Programmierung. B. G. Teubner, Stuttgart Thompson, S. (1999) Haskell: The craft of functional programming, 2nd edition, Addison-Wesley Treleaven, P.C., Mole, G.F. (1980a) A Multi-Processor Reduction Machine for UserDefined. Reduction Languages Proc. 7th Int. Symp. an Computer Architecture, IEEE, New York, pp 121-130 Treleaven, PC., Hopkins, R.P. (1980b) A Recursive (VLSI) Computer Architecture. Techn. Report 161, Computing Lab., Univ. of Newcastle upon Tyne Treleaven, PC., Brownbridge, D.R., Hopkins, R.P. (1982) Data-Driven and DemandDriven Computer Architecture, ACM Comp. Surveys, vol. 14, No. 1, pp 93-143 Treleaven, P. C. (1990) Parallel Computers - Object-Oriented, Functional, Logic. John Wiley & Sons Ltd., Series in Parallel Computing Turing, A.M. (1936) On computable numbers wich an application to the Entscheidungsproblem. Proc. London Math. Soc., Ser. 2, 42, pp 230-265, 1937 und 43, pp 544-546 Turing, A.M. (1937) Computability and Lambda-Definability. J. Symbolic Logic 2, pp 153-163 Turner, D.A. (1976) SASL Language Manual. St. Andrews University, Scotland, Technical Report Turner, D.A. (1979) A New Implementation Technique for Applicative Languages Software-Practice and Experience, vol. 9, pp 31-49
350
Literaturverzeichnis
Turner, D.A. (1979a) Another Algorithm for Bracket Abstraction. Journal of Symbolic Logic, vol. 2 Turner, D.A., Abramson, H.(1980) SASL Reference Manual. Department of Computer Science, University of British Columbia Vancouver, Canada, Technical Manual 26 Turner, D.A. (1981a) Aspects of the Implementation of Programming Languages. P.D. Theses, Oxford University Turner, D.A. (1981b) The Semantic Elegance of Applicative languages. ACM 81, pp 85-92 Turner, D. A. (1986) An overview of Miranda, SIGPLAN Notices vo. 21, pp 158-166 Ungerer, Th. (1989) Innovative Rechnerarchitekturen - Bestandsaufnahme, Trends, M¨ oglichkeiten. McGraw-Hill, Hamburg Ungerer, Th. (1993) Datenflußrechner. B. G. Teubner Stuttgart van der Poel, W.L., Sharp, C.E., van der Mey, G. (1980) New Arithmetical Operators in the Theory of Combinators. Indag.Math. 42 Vuillemin, J. (1975) Syntaxe, semantique et axiomatique d’un langage de programmation simple. Birkh¨ auser Verlag, Stuttgart Wadsworth, C.P. (1971) Semantics and Pragmatics of the Lambda-Calculus. Dissertation, Oxford University Wadsworth, C.P. (1976) The relation between computational and denotational properties for Scott’s D-models of the Lambda-calculus. SIAM Journal, vol. 5, No. 3, pp 488-529 Wand, M. (1982) Deriving Target Code from Continuation Semantics ACM Transactions on Programming Languages and Systems vol. 4, No. 3 Wikstr¨ om, A. (1991) Functional programming using Standard ML. Prentice Hall Wijngaarden, A. van, et.al. (1976) Revised Report on the Algorithmic Language ALGOL 68. Springer-Verlag, Berlin/Heidelberg Williams, J.H. 1981) Formal Representations for Recursively Defined Functional Programs. LNCS, vol. 107, pp 460-470 Williams, J.H. (1982) On the development of the Algebra of Functional Programs. ACM Transactions on Programming Languages, vol. 4, pp 733-757 Winston, P.H., Horn B.K.P. (1981) LISP. Addison-Wesley Publishing Company
Sachverzeichnis
λ-Kalk¨ ul, 2, 22, 23, 62, 63, 68 λ-definierbare Funktion, 9, 44 μ-rekursiv, 10, 11, 18, 44 A-Liste, 119, 143 A-stack, 294 Abstraktion, 98, 113, 146, 161, 174 Acht-Damen-Problem, 204 Ackermann, 18 Activation-Record, 269, 276 Ad-hoc-Polymorphie, 60, 217 Algebra, 74, 86, 208 ALGOL, 108, 134 ALGOL-60, 11, 54 Anfragesprache, 109 Applikation, 73, 104, 124, 145, 157, 174, 227 Applikativ, 85, 108 applikativ, 1–4, 6, 7, 11, 19, 39, 40, 44, 54, 67, 70 APPLY, 79, 92, 94, 121, 149, 174, 184 Aquivalenz, 9 Aquivalenzrelation, 32, 65 Argument-Weitergabe, 268 Argumentengruppen, 277 Argumentkeller, 297 Array, 127, 140 Assemblersprache, 141 Assoziationsliste, 247 Atom, 73, 74, 93, 94, 98, 109 Aufz¨ ahlungstypen, 206 Ausgabetoken, 325 Auswertung, 115, 142, 157, 169, 204, 209, 223, 235
Axiom, 12, 29 Axiomenschema, 30, 33, 70 Backtracking, 236 Backus, 11 Basisfunktion, 11, 53, 73, 75, 111, 155, 156, 169 Basiskombinator, 62, 63, 97 Basistyp, 55, 204, 212, 224 Bedarfsauswertung, 207 berechenbare Funktion, 9, 18 Berechenbarkeit, 9–11 Beweisbarkeit, 30, 57 Beweissystem, 55 Bindungsrelation, 29 BRL, 154, 172 C, 1 car, 111 Cayenne, 239 cdr, 111 Charakteristik, 129 Church, 9 Church’sche These, 10 Church-Rosser-Eigenschaft, 50, 53, 67, 107 COBOL, 1 Codegraph, 313 Colossus-Rechner, 10 CommonLISP, 109 Compiler, 109, 140, 207, 227 Constraint-Programmiersprache, 242 Constraint-System, 242 Curry, 26, 48, 62
352
Sachverzeichnis
Datenkeller, 249 Datentypen, 21, 55, 57 Deduktionsregel, 29, 33, 64 Define-Deklaration, 189 Display-Technik, 269
Inklusion, 18 Inkonsistenz, 38 INTERLISP, 127 Interpretierer, 108, 247 JAVA, 1
E-stack, 294 EFPL, 154, 169 Eingabetoken, 325 Environment, 146 Ergebnistoken, 335 Erlang, 240 Escher, 241 Evaluation, 93, 134, 145 Expertensysteme, 1 Extensionalitat, 30, 33, 76 F-Kalk¨ ul, 55 Fakult¨ at, 15, 90 FALCON, 241 FFP-System, 92 Fixpunkt, 24, 43, 48 Fixpunktkombinator, 43, 48, 65, 99, 106, 108, 114 Fixpunkttheorem, 48, 71 FORTRAN, 1, 11, 54 FP, 73 FP-Basisfunktionen, 73 FUNARG, 108, 121 FUNCTION, 117 Funktion, 2–7 funktional, 1–4, 6, 7, 11, 24, 44, 54, 70 Funktionsende, 251 Funktionsrumpf, 193 G¨ odelisierung, 21 Gleichungssystem, 18, 19, 50 Gleichheit, 27, 30 Gleichheitsrelation, 29 GMD-Maschine, 293 Graph-Reduktion, 279, 291 Graph-Reduktionsregel, 315 Haskell, 206 Hope, 229 I-Kalk¨ ul, 30 Implikation, 15, 70 Indexregister, 272
K-Kalk¨ ul, 30 K¨ unstlichen Intelligenz, 1 Kalk¨ ul, 9 Klauseln, 113 Kombinator, 28 Kombinatorische Logik, 2, 22, 54, 62, 63, 69–71 Kopfnormalform, 46 Kopfredizes, 47 Kopierregel, 29, 267 KRC, 160 Label-Deklaration, 117 LABEL-Funktionen, 154 LAMBDA, 116 Lambda-Ausdruck, 56 Laufzeitkeller, 266 Laufzeitsystem, 265 LCC-Optimierung, 265 LISP, 1, 2, 7, 54, 108 LISP-Interpretierer, 122 LISP-Zelle, 127 Liste, 110 logisch, 1 LOGLISP-System, 109 M-Sprache, 116 MACLISP, 109 Manchester-Datenfluß-Maschine, 287, 324 Master-Slave-Prinzip, 303 Metakomposition, 92 MetaLanguage, 223 MIRANDA, 201 ML, 222 Namenskonflikte, 268 Narrowing, 234 Normalfolge, 51, 52 Normalform, 34 objektorientiert, 1, 2, 61 OCaml, 239
Sachverzeichnis Optimierungsregeln, 310 Paradigma, 24, 39 Paradoxon, 62 Parameter¨ ubergabe, 52, 267 partiell rekursiv, 16, 53, 69 partielle Funktion, 18, 21, 47 PASCAL, 1, 2, 11, 54 Pattern-Matching, 234 Polymorphie, 55, 60, 223 Postrekursion, 196, 257, 260 pr¨ adikativ, 1, 2 primitiv-rekursiv, 11, 19, 22, 69 Programmieren, 3, 7 Programmiersprachen, 7 PROLOG, 1 prozedural, 1, 2, 26, 29, 31, 40 Prozess, 246 Prozesskommunikation, 241 Pseudofunktion, 139 Punktschreibweise, 111 Pure-LISP, 149, 175 Quantoren, 15, 30 QUOTE, 136 R¨ uckkehradressen, 261 Rechnerarchitekturen, 7 Redex, 34, 50, 64 Reduktion, 31 Reduktionsfolge, 34, 50 Reduktionsmaschine, 278, 279 Reduktionsregel, 31, 38 Reduktionsschritt, 31, 50 Reduktionssemantik, 38 Reduktionsstrategie, 50, 53 Reduzierbarkeit, 15, 32 Rekursionsschema, 19 Rekursionstheorie, 24 rekursiv, 2
353
Residuation, 234 Resolutionsmethode, 179 S-Ausdruck, 108 S-K-I-Graph-Reduktionsmaschine, 307 SASL, 155 Scheme, 109, 187 Selbstapplikation, 24 Selektion, 76 Semantik, 11, 24, 28, 38, 47, 53 Shallow-Binding, 249 Signatur, 57, 212 Slave-Maschine, 303 Sorten, 58 Standardreduktionsfolge, 51 Starttoken, 289 String-Reduktion, 279 String-Reduktionsmaschinen, 291 Substitution, 28, 39 Superkombinatoren, 97 Token, 284 Tokenstrom, 287 totale Funktion, 22, 44 Turing-berechenbar, 10, 16 Turing-Berechenbarkeit, 11 Turing-Maschine, 10 Typ, 19, 54 Typ-Anpassung, 60, 131 Typ-Klassen, 237 u ahlbar, 20 ¨berabz¨ Umbenennen, 29, 62 Unifikation, 179 Variablenbindung, 247 Vererbung, 60, 217 Von-Neumann-Rechner, 7, 24 Wertebereich, 21, 57