Logische und Methodische Grundlagen der Programm- und Systementwicklung: Datenstrukturen, funktionale, sequenzielle und objektorientierte Programmierung - Unter Mitarbeit von Alexander Malkis [1. Aufl.] 978-3-658-26301-0;978-3-658-26302-7

Eignen Sie sich mit Hilfe dieses Buchs die wichtigsten Grundlagen der Programm- und Systementwicklung an Geht man beim E

662 65 5MB

German Pages XIII, 451 [460] Year 2019

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Logische und Methodische Grundlagen der Programm- und Systementwicklung: Datenstrukturen, funktionale, sequenzielle und objektorientierte Programmierung - Unter Mitarbeit von Alexander Malkis [1. Aufl.]
 978-3-658-26301-0;978-3-658-26302-7

Table of contents :
Front Matter ....Pages i-xiii
Einführung in die Grundlagen der Softwareentwicklung (Manfred Broy)....Pages 1-32
Rechen- und Datenstrukturen (Manfred Broy)....Pages 33-79
Algebraische Datenmodellierung (Manfred Broy)....Pages 81-185
Funktionale Programmierung (Manfred Broy)....Pages 187-256
Anweisungsorientierte, sequenzielle Programme (Manfred Broy)....Pages 257-309
Referenzen, Zeiger und organisierter Speicher (Manfred Broy)....Pages 311-341
Verfeinerung (Manfred Broy)....Pages 343-380
Grundlagen der Objektorientierung (Manfred Broy)....Pages 381-433
Ausblick: parallel ablaufende, verteilte, kooperierende Systeme (Manfred Broy)....Pages 435-436
Back Matter ....Pages 437-451

Citation preview

Manfred Broy

Logische und Methodische Grundlagen der Programmund Systementwicklung Datenstrukturen, funktionale, sequenzielle und objektorientierte Programmierung – Unter Mitarbeit von Alexander Malkis

Logische und Methodische Grundlagen der Programm- und Systementwicklung

Lizenz zum Wissen. Sichern Sie sich umfassendes Technikwissen mit Sofortzugriff auf tausende Fachbücher und Fachzeitschriften aus den Bereichen: Automobiltechnik, Maschinenbau, Energie + Umwelt, E-Technik, Informatik + IT und Bauwesen. Exklusiv für Leser von Springer-Fachbüchern: Testen Sie Springer für Professionals 30 Tage unverbindlich. Nutzen Sie dazu im Bestellverlauf Ihren persönlichen Aktionscode C0005406 auf www.springerprofessional.de/buchaktion/

www.ATZonline.de

Automobiltechnische Zeitschrift

03

03

März 2012 | 114. Jahrgang

FormoPtimierung in der Fahrzeugentwicklung Leichte und geräuschoptimierte Festsattelbremse geräuschwahrnehmung von

11

Elektroautos

|

2012

www.jot-oberflaeche.de

/// BEGEGNUNGEN

Walter Reithmaier TÜV Süd Automotive

/// INTERVIEW

Claudio Santoni

McLaren

PersPektive Leichtbau Werkstoffe optimieren

Optimale Energiebilanz im Lackierprozess issn 0001-2785 10810

Jetzt 30 Tage testen!

Springer für Professionals. Digitale Fachbibliothek. Themen-Scout. Knowledge-Manager. Zugriff auf tausende von Fachbüchern und Fachzeitschriften Selektion, Komprimierung und Verknüpfung relevanter Themen durch Fachredaktionen Tools zur persönlichen Wissensorganisation und Vernetzung www.entschieden-intelligenter.de

Springer für Professionals

Manfred Broy

Logische und Methodische Grundlagen der Programmund Systementwicklung Datenstrukturen, funktionale, sequenzielle und objektorientierte Programmierung – Unter Mitarbeit von Alexander Malkis

Manfred Broy Institut für Informatik I04 Technische Universität München Garching, Deutschland Unter Mitarbeit von Alexander Malkis

ISBN 978-3-658-26301-0 ISBN 978-3-658-26302-7  (eBook) https://doi.org/10.1007/978-3-658-26302-7 Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. Springer Vieweg © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019 Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Jede Verwertung, die nicht ausdrücklich vom Urheberrechtsgesetz zugelassen ist, bedarf der vorherigen Zustimmung des Verlags. Das gilt insbesondere für Vervielfältigungen, Bearbeitungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen. Die Wiedergabe von allgemein beschreibenden Bezeichnungen, Marken, Unternehmensnamen etc. in diesem Werk bedeutet nicht, dass diese frei durch jedermann benutzt werden dürfen. Die Berechtigung zur Benutzung unterliegt, auch ohne gesonderten Hinweis hierzu, den Regeln des Markenrechts. Die Rechte des jeweiligen Zeicheninhabers sind zu beachten. Der Verlag, die Autoren und die Herausgeber gehen davon aus, dass die Angaben und Informationen in diesem Werk zum Zeitpunkt der Veröffentlichung vollständig und korrekt sind. Weder der Verlag, noch die Autoren oder die Herausgeber übernehmen, ausdrücklich oder implizit, Gewähr für den Inhalt des Werkes, etwaige Fehler oder Äußerungen. Der Verlag bleibt im Hinblick auf geografische Zuordnungen und Gebietsbezeichnungen in veröffentlichten Karten und Institutionsadressen neutral. Springer Vieweg ist ein Imprint der eingetragenen Gesellschaft Springer Fachmedien Wiesbaden GmbH und ist ein Teil von Springer Nature Die Anschrift der Gesellschaft ist: Abraham-Lincoln-Str. 46, 65189 Wiesbaden, Germany

Vorwort

Die Erstellung von Programmen – bei umfangreichen Programmen sprechen wir auch von Software und Softwaresystemen – für elektronische Rechenanlagen ist eine aufwändige, anspruchsvolle und nicht zuletzt fehleranfällige Tätigkeit. Zwei Arten von Fehlern sind zu unterscheiden: Fehler beim Erfassen der Anforderungen sowie Fehler bei der Umsetzung der Anforderungen in eine Implementierung. Eine sorgfältig geplante, ingenieurmäßige Vorgehensweise kann die Entwicklung und Qualität des Ergebnisses deutlich verbessern und dabei helfen, Fehler zu vermeiden. Ingenieurmäßiges Vorgehen erfordert naturgemäß wissenschaftliche Fundierung. Viele Konzepte der Programmentwicklung scheinen allerdings auch heute noch nicht ausreichend wissenschaftlich erschlossen, sodass in der Praxis die Erstellung eines Softwaresystems oftmals weniger durch wissenschaftlich gesicherte ingenieurmäßige Verfahren, sondern auf Basis gewachsener Erfahrung erfolgt, was oft eine aufwändige Folge von Versuch und Irrtum, Programmierung, Test und Fehlerbeseitigung bedeutet. Verstärkt wird dies noch dadurch, dass Programme in der Regel in komplexen Hardware-/Softwareumgebungen ablaufen, die oft unzureichend dokumentiert sind und nicht zuletzt selbst Fehler enthalten. Die Programmerstellung schließt Erprobung und Test dieser Umgebung zwangsläufig ein sowie das Finden von Konstruktionen („Work-Arounds“) zum Umgehen etwaiger schwer zu beseitigender oder in ihren Ursachen kaum diagnostizierbarer Fehler und Unzulänglichkeiten. Diese nüchterne Sicht auf die heutige Praxis der Softwareentwicklung darf jedoch nicht den Blick für die Tatsache verstellen, dass die Konzepte, Strukturen und Methoden der Programmierung einer präzisen wissenschaftlichen Behandlung zugänglich sind. Eine solche wissenschaftliche Fundierung ist eine unabdingbare Voraussetzung dafür, die Entwicklung von Software in eine wissenschaftlich begründete Konstruktionsaufgabe einer Ingenieurdisziplin zu verwandeln. Die Entwicklung großer komplexer Softwaresysteme führt auf eine Vielzahl unterschiedlicher Aufgaben. Entsprechend müssen in den Teams, die solche Entwicklungen durchführen, sehr unterschiedliche Fähigkeiten und Kompe-

v

vi

Vorwort

tenzen vertreten sein. Neben Fragen der Organisation und des Managements, die auch Wirtschaftlichkeitsbetrachtungen einschließen und eine Vielzahl von Kenntnissen zur Rechnertechnik, der vorhandenen Softwareinfrastrukturen und auch des speziellen Anwendungsgebietes, ist eine zentrale Kernkompetenz der Softwareentwicklung die Beherrschung der grundlegenden Strukturen und Modelle im Zusammenhang mit Programmen und Softwaresystemen. Dies schließt Kenntnisse über Algorithmen ein und auch das Wissen über die Darstellung von Algorithmen und Datenstrukturen, über deren Modellierung, Analyse und den zielgerichteten Einsatz in der Programm- und Softwareentwicklung. Naturgemäß entstehen viele Methoden und Vorgehensweisen in der Softwareentwicklung weitgehend intuitiv und nicht wissenschaftlich fundiert. Dies hat mit dem schnellen Wachstum des Faches, der sich schnell verändernden Technologie und der immer noch andauernden rasanten Entwicklung seiner Anwendungsfelder in der Praxis zu tun. Trotzdem bildet sich immer deutlicher ein gut abgrenzbarer Kern von Notationen, Konzepten, Modellen, Methoden und Theorien heraus, der die wissenschaftliche Grundlage für die Programmund Systementwicklung bildet. Kritisch ist bei der Konzeption einer Lehrveranstaltung zu dem Themenkomplex Datenmodellierung, Programmierung, Software Engineering und entsprechenden Grundlagen die Zielsetzung und die Zielgruppe. Zum einen besteht bei Vorlesungen an Universitäten über dieses Gebiet ein klarer wissenschaftlicher Anspruch der Vermittlung von Theorie mit praktischer Relevanz. Für viele wissenschaftliche Arbeiten gerade im Umfeld der methodischen Bewältigung der Software- und Systementwicklung ist die Beherrschung der Grundlagen der Programm- und Systementwicklung unabdingbar. Zum anderen ist auch das Gebiet selbst, die Theorie der Programm- und Systementwicklung, immer noch ein äußerst aktives und relevantes Gebiet der wissenschaftlichen Forschung. Viele Beiträge sind zu diesem Gebiet in den letzten 30 bis 40 Jahren erbracht worden und nur wer diese Beiträge kennt, sie einordnen und zueinander in Beziehung setzen kann, wird auf dem Gebiet der Grundlagen der Softwareentwicklung selbst qualifizierte wissenschaftliche Arbeit leisten können. Schwierig ist dieses Gebiet vor allem aufgrund der zahllosen Publikationen zu Einzelthemen bei gleichzeitigem weitgehendem Fehlen von Überblicksarbeiten, die es leisten könnten, unterschiedliche Ansätze zueinander in Beziehung zu setzen, zu werten und wichtige Ergebnisse von denen zu trennen, die eher weniger Relevanz haben. Auch hierzu soll dieses Werk einen Beitrag liefern. Die Grundlagen der Programm-, Software- und Systementwicklung sind aber nicht nur für den Wissenschaftler und Forscher auf dem Gebiet der Softwaretechnik ein wesentlicher Wissensbaustein. Auch Softwareentwickler in der Praxis, die für sich in Anspruch nehmen, ihr Fach zu beherrschen, sollten die wichtigsten Theorien und Grundlagen der Programm- und Systementwicklung kennen und verstehen, an welchen Stellen sie gewinnbringend einsetzbar sind. Diese Softwareentwickler bilden die zweite Zielgruppe für dieses Buch.

Vorwort

vii

Das vorliegende Buch verfolgt somit zwei unterschiedliche, nicht immer ohne Weiteres miteinander in Einklang zu bringende Ziele: • Grundlagen der Programm- und Systementwicklung für den praktizierenden und den zukünftigen Softwareingenieur zu legen, als Basis und Handwerkszeug für die methodische, praktische und technische Bewältigung dieser Aufgabe; • Darstellung und Diskussion der wissenschaftlichen Ansätze und Beiträge in diesem Bereich, ihrer Vor- und Nachteile und ihres Potentials als Basis für den Methodiker und Wissenschaftler für die Forschung und wissenschaftliche Weiterentwicklung des Gebietes. Natürlich ergeben sich aus den unterschiedlichen Zielgruppen Interessenkonflikte bei der Auswahl und Gestaltung des Stoffes, die notgedrungen durch Kompromisse gelöst werden müssen. Als Konsequenz versucht diese Abhandlung sich stark auf die wesentlichen Grundlagen zu konzentrieren, dabei insbesondere die Bedürfnisse zukünftiger Softwareingenieure anzugehen und ihnen dadurch das notwendige wissenschaftliche Fundament für ihre Arbeiten und ihr Verständnis der Strukturen zu geben. Darüber hinaus wird an geeigneten Stellen die so präsentierte Theorie um Bemerkungen und Beiträge ergänzt, die eher für diejenigen interessant sind, die sich wissenschaftliche Forschung zum Ziel gesetzt haben. Das vorliegende Buch entstand über einen Zeitraum von fast dreißig Jahren in Fortentwicklung einer Vorlesung, die ursprünglich auf einen von Friedrich Ludwig Bauer konzipierten Ansatz zurückgeht. Dieser Ansatz, dokumentiert in dem Buch von Bauer und Wössner mit dem Titel „Algorithmische Sprache und Programmentwicklung“ [BW81], macht sich zur Aufgabe, für alle grundlegenden Daten- und Kontrollstrukturen und Vorgehensweisen der Softwareentwicklung Theorien zu behandeln, die zum einen eine wissenschaftliche Grundlage bilden und zum anderen aber konkrete Methoden vorgeben, um die Aufgaben in der Programm- und Systementwicklung systematisch anzugehen und zu bewältigen. Mein Dank gilt somit zuerst Prof. Friedrich L. Bauer, der mit seinen ambitionierten Vorstellungen schon in den siebziger Jahren mit Nachdruck aufgezeigt hat, in welche Richtung sich die Theorie der Programm- und Systementwicklung zu orientieren hat. Weiter gilt mein Dank auch meinen Kollegen aus der damaligen Zeit, die mit großem Enthusiasmus und Ideenreichtum in vielerlei Hinsicht Beiträge erbracht haben, die in das vorliegende Werk eingeflossen sind. Zu Dank verpflichtet bin ich auch mehreren Generationen von Studenten und wissenschaftlichen Mitarbeitern, die mir im Rahmen meiner Vorlesungen immer wieder Hinweise und Anregungen gegeben haben, sei es durch konstruktive Vorschläge oder durch kritische Anmerkungen und Fragen. Nachhaltigen Einfluss hat diese Schrift auch aus der internationalen Forschungsszene durch Arbeiten auf dem Gebiet der Grundlagen und formalen Methoden der Software- und Systementwicklung bekommen. Besondere Bedeutung hatten hierbei für mich immer die IFIP Working Group 2.3 „Programming

viii

Vorwort

Methodology“, die IFIP Working Group 2.2 „Formal Models of Program Construction“ und nicht zuletzt die von mir über viele Jahre organisierte Serie der Sommerschulen in Marktoberdorf, die mit ihren offenen und konzentrierten Diskussionen zu vielen Themen jene Klarheit geschaffen haben, die wir uns insgesamt für die Programm- und Systementwicklung wünschen. Stellvertretend für viele sollen hier nur Edsger Wybe Dijkstra und Sir Charles Antony Richard Hoare dankend erwähnt werden, die mit ihrem Beharren auf Klarheit und Einfachheit unserem Gebiet entscheidende Impulse gegeben haben. Ich wünsche mir, dass diese Abhandlung ein Beitrag zu der wissenschaftlichen Fundierung des Gebietes der Programm- und Systementwicklung ist. München, 2018

Manfred Broy

Voraussetzungen für den Leser Dieses Buch setzt bei seinen Leserinnen und Lesern Vorkenntnisse voraus: • Grundlegende Kenntnisse in Mathematik zum Begriff Menge, Funktion, Relation, Ordnung; • Grundlegende Kenntnisse in mathematischer Logik zu Themen wie Aussagenlogik, Prädikatenlogik, Beweis, Ableitung, Konsistenz. Unverzichtbar sind insbesondere Kenntnisse zu Programmiersprachen und zur Programmierung. Das Werk verzichtet bewusst auf komplexere und weniger geläufige mathematische Theorien wie etwa Kategorientheorie.

Inhaltsverzeichnis

1

2

Einführung in die Grundlagen der Softwareentwicklung . . . . . . . 1.1 Softwareentwicklung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Die Software- und Programmentwicklungsaufgabe . . . . . . . . . . 1.3 Strukturierung und Abstraktion in der Systementwicklung . . . 1.4 Modellierung in der Systementwicklung . . . . . . . . . . . . . . . . . . 1.4.1 Modellierung der Problemstellung und der Anforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.2 Modellierung der Lösungsarchitektur . . . . . . . . . . . . . . 1.4.3 Implementierung und deren Modellierung sowie Verifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5 Aspekte der Modellbildung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5.1 Deskriptive versus operationelle Beschreibungen . . . . . 1.5.2 Nutzungs- und Realisierungssicht . . . . . . . . . . . . . . . . . . 1.5.3 Abstraktionsebenen und Schichtenarchitekturen . . . . . . 1.5.4 Zum Begriff des Modells . . . . . . . . . . . . . . . . . . . . . . . . . 1.6 Besondere Entwicklungsansätze . . . . . . . . . . . . . . . . . . . . . . . . . 1.6.1 Agilität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.6.2 Lernende Systeme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.7 Der Einfluss der Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8 Historische Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.1 Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.2 Programmiermethodik . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.8.3 Strukturierung großer Systeme . . . . . . . . . . . . . . . . . . . . 1.8.4 Methoden zur systematischen Entwicklung von Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.9 Historische Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.10 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1 1 5 10 11 14 15 16 16 17 17 18 19 20 20 21 22 24 25 27 28 29 31 32

Rechen- und Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.1 Rechenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.1.1 Die Bedeutung des Konzepts der Rechenstruktur . . . . . 35

ix

x

Inhaltsverzeichnis

2.1.2 Nutzungs- und Realisierungssicht für Rechenstrukturen 2.1.3 Signaturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.4 Σ-Algebren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.5 Terme über einer Signatur . . . . . . . . . . . . . . . . . . . . . . . . 2.1.6 Erweiterungen von Algebren . . . . . . . . . . . . . . . . . . . . . . 2.1.7 Homomorphismen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1.8 Allgemeine Bemerkungen zu Rechenstrukturen . . . . . . 2.2 Beschreibung von Daten- und Rechenstrukturen . . . . . . . . . . . . 2.2.1 Modellorientierte Beschreibung von Rechenstrukturen 2.2.2 Eigenschaftsorientierte Beschreibung von Rechenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Historische Bemerkungen zu Datenstrukturen . . . . . . . . . . . . . . 2.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

36 40 43 47 51 53 63 64 66 73 75 78

Algebraische Datenmodellierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 3.1 Algebraische Spezifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 3.1.1 Formeln und ihre Bedeutung . . . . . . . . . . . . . . . . . . . . . . 82 3.1.2 Syntax für algebraische Spezifikationen . . . . . . . . . . . . 87 3.1.3 Strukturierung axiomatischer Spezifikationen . . . . . . . . 91 3.1.4 Modelle axiomatischer Spezifikationen . . . . . . . . . . . . . 95 3.1.5 Wesentliche Begriffe aus der Ordnungstheorie . . . . . . . 108 3.1.6 Verbandsstruktur der Modelle einer Spezifikation . . . . 109 3.1.7 Ableitung von Eigenschaften aus axiomatischen Spezifikationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 3.1.8 Eigenschaften algebraischer Spezifikationen . . . . . . . . . 121 3.2 Wichtige Beispiele für Spezifikationen von Rechenstrukturen . 122 3.2.1 Die Wahrheitswerte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 3.2.2 Zahlartige Rechenstrukturen . . . . . . . . . . . . . . . . . . . . . . 123 3.2.3 Sequenzartige Rechenstrukturen . . . . . . . . . . . . . . . . . . . 125 3.2.4 Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 3.2.5 Mengen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 3.2.6 Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 3.2.7 Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 3.3 Implementierung algebraischer Spezifikationen in Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 3.3.1 Datentypdeklarationen als algebraische Spezifikationen137 3.3.2 Namen- und Strukturäquivalenz . . . . . . . . . . . . . . . . . . . 140 3.3.3 Entitäts/Relationen-Beziehungsmodellierung . . . . . . . . 143 3.3.4 Objektorientierte Datenmodellierung . . . . . . . . . . . . . . . 147 3.3.5 Das formale Objektmodell . . . . . . . . . . . . . . . . . . . . . . . 149 3.4 Methodische Aspekte der Datenmodellierung . . . . . . . . . . . . . . 151 3.4.1 Anforderungsspezifikation . . . . . . . . . . . . . . . . . . . . . . . 152 3.4.2 Funktionsanreicherung . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 3.4.3 Normalformen und eindeutige Normalformen . . . . . . . 153 3.5 Partielle Algebren, Ausnahmen und Fehleralgebren . . . . . . . . . 154

Inhaltsverzeichnis

xi

3.5.1 3.5.2 3.5.3

Funktionsaufrufe ohne reguläre Ergebnisse . . . . . . . . . . 155 Die Logik des Undefinierten . . . . . . . . . . . . . . . . . . . . . . 163 Von der Unterspezifikation zur Spezifikation des Undefinierten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 3.5.4 Fehlerelemente als Ausnahmen . . . . . . . . . . . . . . . . . . . . 171 3.5.5 Bewertung der Ansätze . . . . . . . . . . . . . . . . . . . . . . . . . . 173 3.6 Subtyping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 3.7 Historische Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 3.8 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 4

Funktionale Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 4.1 Konzepte funktionaler Programmierung . . . . . . . . . . . . . . . . . . . 190 4.1.1 Funktionsdeklaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 4.1.2 Bedingte Auswahlfunktion . . . . . . . . . . . . . . . . . . . . . . . 194 4.1.3 Nichtsequenzielle Funktionen . . . . . . . . . . . . . . . . . . . . . 195 4.2 Auswertung von Termen: Rekursion und Berechnung . . . . . . . 199 4.2.1 Rekursive Deklaration . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 4.2.2 Explizite Gleichungen und strukturelle Rekursion . . . . 200 4.2.3 Operationelle Semantik funktionaler Programme . . . . . 202 4.2.4 Fixpunktsemantik für rekursive Deklaration . . . . . . . . . 204 4.3 Beweisprinzipien für Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . 213 4.3.1 Berechnungsinduktion . . . . . . . . . . . . . . . . . . . . . . . . . . . 213 4.3.2 Beweis durch Fixpunkteigenschaften . . . . . . . . . . . . . . . 216 4.3.3 Strukturelle Induktion . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 4.3.4 Terminierungsbeweise . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 4.4 Methodik funktionaler Programmierung . . . . . . . . . . . . . . . . . . 229 4.4.1 Strukturierung funktionaler Programme . . . . . . . . . . . . 230 4.4.2 Einbettung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232 4.5 Funktionale Sorten und Funktionen höherer Stufe . . . . . . . . . . . 234 4.5.1 Funktionale Sorten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234 4.5.2 Currying . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237 4.5.3 Die Rolle formaler Parameter . . . . . . . . . . . . . . . . . . . . . 240 4.5.4 Tupel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242 4.5.5 Der Fixpunktoperator . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 4.6 Spezifikation funktionaler Programme . . . . . . . . . . . . . . . . . . . . 244 4.6.1 Algebraische Spezifikation von Funktionen . . . . . . . . . 245 4.6.2 Prädikative Spezifikation von Funktionen . . . . . . . . . . . 247 4.6.3 Zusammenhang zwischen Spezifikationen und funktionalen Programmen . . . . . . . . . . . . . . . . . . . . . . . . 249 4.7 Historische Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 4.8 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253

xii

Inhaltsverzeichnis

5

Anweisungsorientierte, sequenzielle Programme . . . . . . . . . . . . . . 257 5.1 Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258 5.1.1 Grundlegende Sprachformen, Syntax der GCL . . . . . . . 259 5.2 Zustände und Zustandsübergänge . . . . . . . . . . . . . . . . . . . . . . . . 264 5.2.1 Prozeduren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268 5.3 Logik zuweisungsorientierter Programme . . . . . . . . . . . . . . . . . 270 5.3.1 Schwächste Vorbedingungen . . . . . . . . . . . . . . . . . . . . . . 271 5.3.2 Prädikative Semantik von Anweisungen . . . . . . . . . . . . 281 5.3.3 Prädikative Spezifikation der Wirkung von Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 5.3.4 Prädikative Spezifikation von Prozeduren: Kontrakte . 290 5.3.5 Verifikation von Anweisungen durch Zusicherungen . . 291 5.3.6 Terminierungsbeweise . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 5.3.7 Zusammenhang zwischen wp-Kalkül, Zusicherungskalkül und prädikativer Spezifikation . . . 301 5.4 Historische Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304 5.5 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304

6

Referenzen, Zeiger und organisierter Speicher . . . . . . . . . . . . . . . . 311 6.1 Referenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314 6.1.1 Das Konzept der Referenz . . . . . . . . . . . . . . . . . . . . . . . . 314 6.1.2 Zustandsfreie einfache Geflechtstrukturen . . . . . . . . . . . 317 6.2 Zustandsfreie, zyklische Geflechtstrukturen . . . . . . . . . . . . . . . . 322 6.3 Allgemeine Geflechte: der organisierte Speicher . . . . . . . . . . . . 325 6.3.1 Funktionen für organisierte Speicher . . . . . . . . . . . . . . . 326 6.4 Programmvariable und Referenz . . . . . . . . . . . . . . . . . . . . . . . . . 332 6.5 Die Rolle der Referenzen in der Programmierung . . . . . . . . . . . 333 6.6 Ausblick: Separationslogik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335 6.7 Historische Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339 6.8 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340

7

Verfeinerung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343 7.1 Die grundlegende Idee der Verfeinerung . . . . . . . . . . . . . . . . . . 344 7.2 Schrittweise Verfeinerung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348 7.2.1 Verfeinerung von Spezifikationen funktionaler Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348 7.2.2 Verfeinerung für funktionale Spezifikationen . . . . . . . . 352 7.2.3 Verfeinerung algebraischer Spezifikationen . . . . . . . . . 355 7.2.4 Verfeinerung im Zusicherungskalkül . . . . . . . . . . . . . . . 360 7.2.5 Tragweite des Verfeinerungsansatzes . . . . . . . . . . . . . . . 363 7.3 Wechsel der Abstraktionsebene . . . . . . . . . . . . . . . . . . . . . . . . . . 364 7.3.1 Wechsel der Rechenstruktur . . . . . . . . . . . . . . . . . . . . . . 364 7.3.2 Die Implementierungsrelation für Rechenstrukturen . . 367 7.3.3 Implementierung algebraischer Spezifikationen . . . . . . 370 7.3.4 Konstruktion von Implementierungen . . . . . . . . . . . . . . 375

Inhaltsverzeichnis

7.4 7.5

xiii

Historische Bemerkungen zur Verfeinerung . . . . . . . . . . . . . . . . 375 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377

8

Grundlagen der Objektorientierung . . . . . . . . . . . . . . . . . . . . . . . . . 381 8.1 Objektorientierte Programmierung . . . . . . . . . . . . . . . . . . . . . . . 382 8.1.1 Aufbau objektorientierter Programme . . . . . . . . . . . . . . 384 8.1.2 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389 8.1.3 Module und Objektbasierung . . . . . . . . . . . . . . . . . . . . . 391 8.1.4 Persistenz und dynamisches Erzeugen von Objekten . . 392 8.2 Spezifikation objektorientierter Programme: „Design-byContract“ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393 8.3 Objektorientierter Entwurf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396 8.4 Komponenten und Schnittstellen in der Objektorientierung . . . 398 8.4.1 Komponenten in der Objektorientierung . . . . . . . . . . . . 398 8.4.2 Methoden und Aufrufe/Antwortnachrichten . . . . . . . . . 399 8.4.3 Spezifikation durch Vertrag – Design-by-Contract . . . . 401 8.4.4 Objektorientierte Exportschnittstellen . . . . . . . . . . . . . . 404 8.4.5 Spezifikation von Exportschnittstellen durch Kontrakte 408 8.4.6 Offene Systeme: Klassen mit Export und Import . . . . . 408 8.4.7 Spezifikation von Export-/Importschnittstellen mittels Vertrag . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411 8.4.8 Export-/Importschnittstellen: Entwurf mittels Vertrag . 413 8.5 Komponenten und Architektur in der Objektorientierung . . . . 415 8.5.1 Komponenten als Menge von Klassen . . . . . . . . . . . . . . 416 8.5.2 Komposition von Klassen . . . . . . . . . . . . . . . . . . . . . . . . 419 8.5.3 Komposition: Schnittstellen von Komponenten verbinden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421 8.5.4 Spezifikation und Verifikation zusammengesetzter Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 422 8.6 Verfeinerung in der Objektorientierung . . . . . . . . . . . . . . . . . . . 424 8.7 Historische Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 425 8.8 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 426

9

Ausblick: parallel ablaufende, verteilte, kooperierende Systeme 435

Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437 Symbolverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445 Sachverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 447

Kapitel 1

Einführung in die Grundlagen der Softwareentwicklung

Dieses erste Kapitel motiviert die beschriebenen Ansätze zur Programm- und Softwareentwicklung. Es hat die Einordnung der behandelten Themen zum Ziel und stellt Bezüge zu angrenzenden Gebieten und Themen her. Es gibt insbesondere eine Übersicht über wesentliche methodische Gesichtspunkte und die Rolle der logischen und methodischen Grundlagen für das Gebiet des Software Engineering.

1.1 Softwareentwicklung Programme sind in einer formalen Syntax abgefasste Verarbeitungsvorschriften zur Ausführung auf Rechenanlagen, die Datenformate, Algorithmen sowie die Interaktion mit der Umgebung (etwa über das Betriebssystem) und Peripherie (Ein-/Ausgabe und Nutzerschnittstelle) beschreiben. Wir sprechen bei umfangreicheren Systemen von Programmen und Daten auch allgemein von Software und Softwaresystemen. Softwaresysteme bestehen aus der Gesamtheit von Programmen und Daten, die auf einem Rechensystem eine ausführbare Einheit zur Lösung einer Aufgabe bilden. Beim Einsatz von Software in einem Anwendungsgebiet sprechen wir auch von einer Anwendung. Den Prozess der Erstellung von Programmen nennen wir Programmierung oder unter Betonung der über die reine Erstellung des Programmtextes hinausgehenden dazu anfallenden Tätigkeiten Programm- und Softwareentwicklung. Software- und auch Programmentwicklung nennen wir somit die Gesamtheit aller Tätigkeiten, die bei der Erstellung von Software beziehungsweise von Programmen zur Lösung einer Aufgabe im Sinn einer Anwendung anfallen. Software im Einsatz ist in der Regel entweder über eine Nutzerschnittstelle unmittelbar dem Nutzer zugänglich und/oder arbeitet eingebettet in Systemumgebungen und Systemabläufe oft auch in Interaktion mit dem Nutzer. Die Systemabläufe in der Umgebung können organisatorischer Natur („Geschäftsprozesse“) oder technische Systeme („technischer Prozess“) sein. Im © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019 M. Broy, Logische und Methodische Grundlagen der Programm- und Systementwicklung, https://doi.org/10.1007/978-3-658-26302-7_1

1

Programm

Softwaresysteme

Softwareentwicklung Programmentwicklung

2

Informatiksysteme

1 Einführung in die Grundlagen der Softwareentwicklung

letzteren Fall sprechen wir von eingebetteter Software und eingebetteten Systemen. In der Regel erfordert die Entwicklung von Programmen ein umfassendes Verständnis für die Systemumgebung und gegebenenfalls auch den dadurch unterstützten Anwendungsprozess, das übergeordnete System, in dem das Programm ablaufen soll, und das Anwendungsgebiet, in dessen Kontext das Programm Verwendung findet. Wir sprechen von der Programm- oder Softwareumgebung oder auch vom Kontext. Dabei ist die Programmentwicklung häufig eng an eine Anwendungsdomäne und an eine Systementwicklung gekoppelt – insbesondere, wenn Programme eingebettet in ein übergeordnetes System zur Ausführung kommen. Informatiksysteme (oft IT-Systeme genannt) bestehen aus Rechnerhardware, aus Software und weiteren elektronischen oder elektromechanischen Bestandteilen wie Übertragungseinheiten, Sensoren, Aktuatoren, aber auch Peripherie und Nutzerschnittstellen oder den umgebenden organisatorischen Prozessen. Unter dem Oberbegriff Systementwicklung verstehen wir die Gesamtheit aller Tätigkeiten, die bei der Erstellung eines Systems insbesondere im Hinblick auf seinen Softwareanteil zur Lösung einer Aufgabe im Sinn einer Anwendung anfallen. In den Grundlagen der Programm- und Systementwicklung behandeln wir die wissenschaftliche Basis im Hinblick auf • Konzepte, Modelle und Strukturen von Daten, Programmen und von Software; • Beschreibungsmittel wie Notationen, Formalismen, Tabellen, Diagramme, Spezifikationssprachen und algorithmischen formalen Sprachen (Programmiersprachen) für Programme und Software; • Theorien, Regeln, Kalküle, Logiken, Methoden, Entwicklungsprozesse für Datenstrukturen, Programme und Softwaresysteme, die in der Programm- und Softwareentwicklung Verwendung finden. Eine der großen Schwierigkeiten und Herausforderungen bei der Programmentwicklung besteht in der unmissverständlichen Beschreibung und Dokumentation der einem Programm zugrundeliegenden Ideen, Lösungen, Modelle, Theorien, Theoreme, Strukturen und Entscheidungen. Diese sind für das Verständnis und die Analyse von Software von großer Bedeutung. Besonderes Augenmerk legen wir im Folgenden auf formale Präzision und damit auf eine genaue logische und mathematische Fundierung der auftretenden Terminologie, Konzepte, Modelle und Methoden. Wenn auch in der Praxis eher pragmatischere Herangehensweisen als die hier eingeführten Methoden und Formalismen eingesetzt werden, so soll doch deutlich werden, dass die Ansätze der Praxis einer vollständigen wissenschaftlichen, formalen Behandlung zugänglich sind. Eine solche wissenschaftliche Fundierung zeigt nicht zuletzt eine Vielzahl von Möglichkeiten auf, die Verfahren der Praxis zu bewerten, zu begründen, zu verbessern oder letztlich zu automatisieren.

1.1 Softwareentwicklung

3

Neben den Modellen und Beschreibungsmitteln, die in der Programmentwicklung Verwendung finden, ist für die systematische Entwicklung von Programmen ein methodischer Rahmen erforderlich. Dazu gehören Entwicklungsregeln zur Durchführung von Entwicklungsschritten und ein Konzept, das die Schritte im Programmentwicklungsprozess in eine sinnvolle Reihenfolge bringt. Wir sprechen von einem Vorgehensmodell. Die Vor- und Nachteile Vorgehensmodell unterschiedlicher Vorgehensmodelle und ihre Struktur werden im Software Engineering in der Softwaretechnik eingehend behandelt. Wir beschränken uns im Folgenden auf die Methodik. Darunter verstehen wir Regeln und Vorgaben zu einzelnen Entwicklungsschritten sowie die dafür verwendeten Strukturen und Beschreibungsmittel. Schwerpunkte sind die methodischen und beschreibungstechnischen Grundlagen des Software Engineering. Im Weiteren werden zunächst folgende Themen für die Modellbildung, Spezifikation, Verfeinerung und Implementierung von Software behandelt: • die abstrakte Beschreibung von Daten- und Rechenstrukturen als Grundlage der Datenmodellierung, • funktionale Programmstrukturen als Grundlage der Deklaration von Rechenvorschriften und Algorithmen. Damit handelt es sich um Programmierstile und eine Modellbildung auf Basis besonders klarer mathematischer Konzepte. Dadurch werden auch Grundlagen für alle weiter behandelten Programmierstile gelegt. Anschließend wird davon ausgehend eine Reihe verbreiteter Programmierstile behandelt, die oft durch die vorherrschenden Maschinenstrukturen geprägt sind und entsprechend Sprachelemente anbieten, die für heute gebräuchliche Rechner eine besondere Effizienz erlauben, wie • imperative Programme und Anweisungen und • Referenzen, Verweisstrukturen und Geflechtprogrammierung. Der Zusammenhang zwischen den schrittweise fertiggestellten Versionen von Programmen und Daten wird durch das Konzept der Verfeinerung als Mittel der Formalisierung von Entwicklungsschritten erfasst. Abschließend behandeln wir mit der Objektorientierung einen Ansatz, der die methodischen Elemente in ein Konzept integriert. Geplant ist, in einem zweiten Band Systemstrukturen wie verteilte, reaktive und interaktive Systeme und Programme mit Ein- und Ausgabe zu behandeln. Diese Schrift setzt sich zum Ziel, alle wichtigen Begriffe in diesem Gebiet sowie deren logisch-mathematische Grundlagen zu vermitteln. Schwerpunkte sind die Konzepte der verwendeten Sprachen, deren Modellierung und Spezifikation, Theoriebildung, zielgerichtete Verfeinerung, Implementierung und Verifikation im Entwicklungsprozess. Die mathematische Logik stellt ein wesentliches Fundament für die Grundlagen der Programm- und Systementwicklung dar. Die Gründe dafür liegen auf der Hand. Viele Aspekte, Strukturen und Begriffe der Programmierung ähneln denen der mathematischen Logik, die eine lange Tradition besitzt und damit

4

1 Einführung in die Grundlagen der Softwareentwicklung

für viele der Konzepte und Aspekte der Programmierung zumindest in Teilen bereits ein formales Fundament geschaffen hat. Logik bildet die Basis für die Spezifikation und die Verifikation von Programmen. Die Grundlagen der Programm- und Systementwicklung umfassen zwangsläufig weite Teile der theoretischen Informatik. Dazu gehören im Einzelnen folgende Themen: • Syntax und Semantik von Datenstrukturen, Spezifikations- und Programmiersprachen; • Berechenbarkeit, Komplexität, effiziente Algorithmen; • formale Spezifikation von Daten- und Rechenstrukturen; • logische und mathematische Verifikation; • Verfeinerungsrelationen. Darüber hinaus sind folgende weniger theoretische als vielmehr methodische Themen von Interesse, die auch von praktischer Bedeutung im Software Engineering sind: • • • • • • • • •

Datenstrukturen und ihre logischen Eigenschaften; Kapselung von Daten und den dazugehörigen Zugriffsoperationen; Wechsel der Datenstruktur, Schaffung von Abstraktionsebenen; Programmiernotationen und -sprachen und ihre Bedeutung; Programmiermethoden; Schnittstellenbildung, Schnittstellenbeschreibung, Modularität; Softwarearchitektur, Gliederung von Software in Komponenten; Top-Down-Entwicklung; Modellbildung in der Programmentwicklung.

In den Grundlagen der Programmentwicklung werden die theoretischen Ansätze nur soweit wie erforderlich behandelt, um die methodischen Aspekte zu verstehen und präzise erfassen zu können. Viele der theoretischen Aspekte führen in umfangreiche Theorien, deren umfassende theoretische Behandlung den Rahmen der methodischen Grundlagen der Programmentwicklung sprengen würde. Hierzu werden im Weiteren lediglich Literaturhinweise gegeben. Entsprechend schwierig ist die Auswahl und Abgrenzung der Themen für eine Lehrveranstaltung zu den Grundlagen der Programm- und Systementwicklung. Viele der genannten Bereiche erfordern und rechtfertigen eigenständige Spezialvorlesungen für eine sorgfältige Behandlung. Das Ziel dieser Abhandlung ist, die Rolle dieser Themen im Hinblick auf eine Grundlagenbildung für die Software- und Systementwicklung in einem einheitlichen Rahmen darzustellen, ohne den Anspruch zu erheben, Spezialvorlesungen ersetzen zu wollen. Entsprechend sollte eine Veranstaltung, die dieses Buch zur Basis nimmt, sich auf eine Auswahl der wesentlichen Themen konzentrieren und Teile dieses Werks als Hintergrundmaterial betrachten. Wichtig ist dabei, dass die Grundprinzipien der Programm-, Software- und Systementwicklung dabei verständlich vermittelt werden.

1.2 Die Software- und Programmentwicklungsaufgabe

5

Im Weiteren werden Aufgaben und Fragen des Managements und der Organisation von Softwareprojekten vernachlässigt. Allerdings soll ein Bezug zu der Systematik der Softwareentwicklung hergestellt werden. Die wesentlichen Grundprinzipien der Software- und Systementwicklung umfassen folgende Konzepte: • systematisches Vorgehen strukturiert durch Meilensteine; • agiles Vorgehen bei einer iterativen und inkrementellen Erarbeitung der Lösung; • Trennung von Problem und Lösung, von Spezifikation und Realisierung (Abstraktion und Schnittstellenbildung, engl. interface abstraction); • Dekomposition von Problemen in Teilprobleme (engl. divide and conquer); • Verstecken von Realisierungsdetails (engl. information hiding); • schrittweise Verfeinerung (engl. stepwise refinement); • strukturiertes Programmieren (engl. structured programming); • Qualitätssicherung durch Verifikation des Entwicklungsergebnisses (engl. formal verification). Ein wesentliches Ziel dieser Schrift ist, zu zeigen, wie die oftmals schwer zu durchschauenden, komplizierten Konzepte und Methoden der Programmierung und der Softwareentwicklung explizit durch Notationen und Theorien erklärt und durch logische Spezifikations- und Verifikationsansätze unterstützt werden können und damit einer sorgfältigen Analyse, einer wissenschaftlichen Systematik und Überprüfbarkeit zugänglich werden.

1.2 Die Software- und Programmentwicklungsaufgabe Da besonders in komplexen oder umfangreichen Anwendungen die Programmentwicklung schwierig und fehleranfällig ist, ist ein systematisches, strukturiertes, wohlüberlegtes Vorgehen dringend geboten. Fragen der Organisation des Entwicklungsprozesses für umfangreiche Software werden ausführlich in dem Gebiet Software Engineering behandelt. In den Grundlagen der Programm- und Systementwicklung werden die methodischen Konzepte, Theorien, Modelle und Regeln besprochen, die bei diesem Vorgehen Verwendung finden können. Wir unterscheiden bei der Programmentwicklung zwischen Entwickeln und Programmieren im Kleinen und Entwickeln und Programmieren im Großen. Programmieren und Entwickeln im Kleinen umfasst den Aufbau von mittelgroßen Programmen und kleineren Softwarebausteinen (Datenstrukturen, Module und Komponenten) sowie die Entwicklung der dafür eingesetzten Algorithmen und Implementierungen für Datenstrukturen. Im Vordergrund stehen somit die Ablauf- und Kontrollstrukturen und die Struktur der dazugehörigen Daten und charakteristischen Funktionen. Wesentliche Fragen betreffen für beide Aspekte die Korrektheit und Effizienz der Algorithmen und der Datenrepräsentation.

6

Spezifikation

Validierung Verifikation

1 Einführung in die Grundlagen der Softwareentwicklung

Programmieren und Entwickeln im Großen betrifft die Erfassung aller Anforderungen („Requirements Engineering“) und die Struktur großer Softwaresysteme („die Softwarearchitektur“), Fragen des Entwurfs der Datentypen, der Struktur und der Modularisierung („Datenmodellierung“, „Schnittstellen“), der funktionalen Qualitätssicherung („Validierung“, „Verifikation“, „Test“), der Wiederverwendung („Componentware“, „Produktlinie“) oder der Generierung von Systemteilen (Beispiel: Generator für die grafische Benutzeroberfläche, engl. graphical user interface, GUI). Natürlich gibt es enge Bezüge zwischen dem Programmieren und Entwickeln im Großen und dem Programmieren und Entwickeln im Kleinen. Eine Reihe der im Folgenden behandelten Theorien und Ansätze dient letztlich beiden Aufgabenkomplexen. Eine scharfe Trennung der Themen im Hinblick auf das Programmieren im Kleinen oder im Großen ist somit ohnehin nicht möglich und sinnvoll. Trotzdem ist es hilfreich, bei den behandelten Ansätzen zu unterscheiden, welche sich davon betont an den einen oder anderen Aufgabenkomplex richten. Da jedes Programm letztendlich vollständig formal ist, damit es rein mechanisch durch Maschinen verarbeitet werden kann, erfordert der Übergang von einer informellen, oft unscharfen Problemstellung zum konkreten Programm stets eine mehr oder weniger umfangreiche explizite oder implizite formale Modellierung. Abhängig vom Aufgabengebiet kann dabei entweder auf bereits vorhandene formale Modelle zurückgegriffen werden, die lediglich mit Mitteln der Informatik dargestellt zu werden brauchen, oder es muss (gegebenenfalls in enger Zusammenarbeit mit Fachleuten des Anwendungsgebietes) eine spezifische formale Modellierung vorgenommen werden. Eine zentrale Aufgabe der Programm- und Systementwicklung besteht in der Festlegung der wesentlichen funktionalen Eigenschaften eines Programms oder Systems als Teil der Erarbeitung der Anforderungen. Wir sprechen von den funktionalen Anforderungen und bei deren Festlegung auch von Spezifikation. Eine Spezifikation dient also der Beschreibung von Eigenschaften eines vorhandenen oder zu erzeugenden Entwicklungsergebnisses. Die Sicherstellung und Überprüfung der Validität („Gültigkeit“) und der Angemessenheit der formalen Modellierung und Anforderungsspezifikation in Bezug auf die informelle Problemstellung nennen wir Validierung. Den Nachweis, dass das Ergebnis eines Programmentwicklungsschritts der vorgegebenen formalen Spezifikation entspricht, nennen wir Verifikation. Man beachte in Hinsicht auf Verifikation und Validierung: • Beide Tätigkeiten sind Bestandteile der Qualitätssicherung für Software. • Die Validierung entzieht sich einer rein formalen Behandlung, da sie eine Beziehung zwischen einer formalen oder informellen Beschreibung und allgemeinen nichtformalen Zielvorgaben sicherstellen muss. • Die Verifikation ist eine im Kern formale Aufgabe und kann, zumindest im Prinzip, mit rein formalen (mathematischen) Methoden (durch formalen Beweis) durchgeführt werden; dies setzt allerdings eine formale Spezifikation voraus.

1.2 Die Software- und Programmentwicklungsaufgabe

7

Für die Entwicklung von Programmen, die im Sinne eines bestimmten Anwendungsgebietes und einer bestimmten Aufgabenstellung zuverlässig sind, ist sowohl die Durchführung der Validierung als auch die Verifikation der anfallenden weiteren Entwicklungsschritte erforderlich (siehe Abb. 1.1). Alle Schritte (dargestellt durch Pfeile), die in der Abb. 1.1 „nach oben“ zeigen, entsprechen Qualitätssicherungsschritten.

Informelle Aufgabenstellung

Validierung

– Analyse der Aufgabe und der Anforderungen – Modellbildung für die Aufgabenstellung (Abbildung der Aufgabe auf die Strukturen der Informatik) – Beschreibung des Domänenmodells – Formalisierung der Anforderungen

Anforderungsspezifikation

Verifikation

– Strukturierung und Zerlegung der Aufgabenstellung – Einführung informatikspezifischer Konzepte – Wahl einer Softwarearchitektur (Programmstruktur)

Entwurfsspezifikation

Verifikation

– Wahl der Algorithmen – Wahl der grundlegenden Datenstrukturen

Abstrakte Implementierung — Programm auf hoher Abstraktionsebene

Verifikation

– Codierung in konkreter Programmiersprache – Optimierung und Anpassung der Algorithmen und Datenstrukturen an eine spezielle Ausführungsplattform

Konkrete Implementierung — Programm ausgerichtet auf eine spezielle Maschine Abb. 1.1 Schematisches, idealisiertes Vorgehen bei der Programmentwicklung (Top-DownAnsatz)

Abbildung 1.1 ist natürlich eine starke Abstraktion und Vereinfachung des Entwicklungsprozesses. Den vollen Umfang der Prozesse erfassen sogenannte Vorgehensmodelle. Zur Illustration stellen wir hier die Struktur des V-Modells XT grob dar. Wie in Abb. 1.2 erkennbar sieht das V-Modell XT im Wesentlichen die folgenden Entwicklungsschritte vor: • Systemanforderungsanalyse und -entwurf • Anforderungsanalyse zur Informationsverarbeitung und Anforderungsentwurf • Softwareanforderungsanalyse, Modellbildung

8

1 Einführung in die Grundlagen der Softwareentwicklung

• Anforderungsspezifikation, Strukturierung und Festlegung der Aufgabenstellung • Design, Erarbeitung der Architektur – Grobentwurf (Entwurfspezifikation) · Softwarearchitektur · Schnittstellen – Feinentwurf • Implementierung – Abstrakt: Einführung informatikspezifischer Konzepte, Wahl der Algorithmen und Datenstrukturen – Konkret: Codierung in konkreter Programmiersprache, Optimierung der Algorithmen und Datenstrukturen, Anpassung an Maschineneigenschaften • Modulverifikation • Softwareintegration und -verifikation • Systemintegration Diese Schritte werden im V-Modell detailliert festgelegt. Der Name „V-Modell“ ergibt sich auch aus dem Umstand, dass die einzelnen Schritte oft in einem Vförmig angeordneten Schema erklärt werden (siehe Abb. 1.2). nsmodell analy ehe sie g r rt Vo Änderungsplan festgelegt

Projekt beauftragt

ge Vor e od sm hen

iert

System integriert

odel l

ung

Feinentwurf abgeschlossen

realisiert

Lieferung durchgeführt

System entworfen

zip

Projekt abgeschlossen

n sm

Ver b es s e r

System spezifiziert

ll ko n

Abb. 1.2 V-Modell XT [BRSK05]

Abnahme erfolgt

Systemelemente realisiert

eh e

Anforde- Projekt Angebot rungen ausgeabgefestgelegt schrieben geben

o rg

Projekt definiert

ng V

Projekt genehmigt

se be s Ver

ru

1.2 Die Software- und Programmentwicklungsaufgabe

9

In der Programmentwicklung unterscheiden wir zwei grundsätzlich unterschiedliche Vorgehensweisen: • Top-Down: Ausgehend von einer umfassenden, abstrakten Problemstellung Top-Down wird die Aufgabe in Teilaufgaben zergliedert (engl. divide and conquer), bis die Einzelaufgaben so weit untergliedert sind, dass sie für sich gelöst werden können. Daraus wird die Gesamtlösung zusammengesetzt. • Bottom-Up: Aus vorgegebenen oder erarbeiteten Lösungen für kleine Bottom-Up Einzelaufgaben wird die Lösung für die Gesamtaufgabe schrittweise zusammengesetzt. In der Praxis werden die Vorgehensweisen in der Regel kombiniert eingesetzt. Das Top-Down-Vorgehen ist oft ein guter Weg, eine Entwicklung – wenn sie abgeschlossen ist – anschaulich zu erklären. In der Praxis sind in den letzten Jahren verstärkt auch sogenannte „agile“ Methoden im Einsatz, die das Programmieren stark ins Zentrum stellen und die Erfassung von Anforderungen, die Programmierung und Verifikation durch Tests nicht streng sequenziell sondern iterativ und inkrementell durchführen. Das V-Modell 97 propagiert hingegen ein Top-Down-Vorgehen (vgl. [DW99]), das nahezu einem Wasserfallmodell entspricht. Das V-Modell XT (siehe [ABD+ 99, BRSK05]) lässt ein flexibleres Vorgehen zu, das iteratives und inkrementelles Entwickeln explizit einschließt. Wenn auch oft erbittert darüber gestritten wird, welche der Vorgehensweisen die angemessenere ist, so wird in der Praxis in aller Regel pragmatisch eine Mischung beider Ansätze verfolgt. In den zurückliegenden 40 Jahren ist eine Vielzahl von Vorschlägen zum Vorgehen in der Programm- und Softwareentwicklung ausgearbeitet worden. So werden inkrementelle Vorgehensweisen in der agilen Programmierung (siehe SCRUM und das Stichwort Continuous Integration) empfohlen, die Entwicklung von Prototypen, das mehrfache Durchlaufen des Entwicklungsprozesses oder (wie im „Extreme Programming“) die starke Konzentration auf das eigentliche Programmieren und den Test („Codezentrierung“). Fast jeder dieser Vorschläge hat in gewissen Anwendungssituationen seine Berechtigung. Weiter nutzt nahezu jeder dieser Ansätze ähnliche Grundlagen und Theorien, allerdings gegebenenfalls in unterschiedlicher Reihenfolge und Betonung der einzelnen Aufgaben. Gerade im Hinblick auf den geforderten Umfang der Dokumentation finden sich Unterschiede. Dennoch können und wollen wir im Weiteren bei der Behandlung der methodischen Grundlagen weitgehend unabhängig von einem bestimmten Vorgehensmodell bleiben. Im Folgenden konzentrieren wir uns im Wesentlichen auf das Entwickeln im Kleinen. Wir betrachten kleine Programme, die man bestenfalls auf der Ebene einzelner Prozeduren oder Module ansiedeln würde, gegebenenfalls auch einige etwas umfangreichere Datenstrukturen, schneiden aber nur ansatzweise Fragen des Strukturierens von Programmen und Themen an, die stärker dem Entwickeln und Programmieren im Großen zuzurechnen sind. Das Ziel der Darstellung ist es, formal sauber und eindeutig ausgearbeitete Methoden zu präsentieren, die für die Spezifikation, die schrittweise Entwicklung und die

10

1 Einführung in die Grundlagen der Softwareentwicklung

Verifikation von Programmen und damit Datenstrukturen die Grundlagen für die System- und Softwareentwicklung bilden.

1.3 Strukturierung und Abstraktion in der Systementwicklung

Strukturierung

Artefakte Abstraktion

Zur Bewältigung der quantitativen und qualitativen Komplexität einer Aufgabenstellung aus dem Bereich der Programmentwicklung sind die Zerlegung der Problemstellung in einfachere Teilprobleme durch Strukturierung und die Vereinfachung durch Abstraktion wesentliche Hilfsmittel. Die Strukturierung dient der Gliederung der Aufgabenstellung mit dem Ziel der Erarbeitung einer Lösung. Wir unterscheiden zwischen der Strukturierung der Vorgehensweise (dem Vorgehensmodell, siehe Abschnitt 1.1) und der Strukturierung der Ergebnisse, der erarbeiteten Modelle, Spezifikationen und der Programme. Diese Teilergebnisse in der Entwicklung nennen wir Artefakte. Abstraktion zielt auf die Elimination unwesentlicher Details und die Konzentration auf die Kerninhalte. Dabei dient eine Abstraktion in der Regel einem bestimmten Zweck und nimmt eine bestimmte Sicht („Perspektive“, engl. viewpoint) ein. Modelle sind hilfreiche Mittel der Abstraktion. Oftmals ist es günstig, zu Beginn eine Abstraktion zu wählen, die Zusammenhänge überstark vereinfacht, und erst später weitergehende Sonderfälle und Komplikationen zu betrachten. Die Strukturierung dient auch der Verdeutlichung der Grundstrukturen einer Problemstellung und der Zerteilung in unabhängig bearbeitbare Teilprobleme. Sie dient ferner der Gliederung von Programmen mit dem Ziel, diese verständlicher, durchschaubarer und handhabbarer zu machen. Dies erlaubt auch eine Gliederung in wiederverwendbare Bestandteile. Als einfaches Beispiel für Strukturierung können wir ein Buch nehmen: Jedes Buch hat eine Gliederung in Kapitel und Absätze, die es strukturiert. Klassische Strukturierungs- und Abstraktionshilfsmittel in der Programmentwicklung sind: • hierarchisch aufgebaute Daten- und Rechenstrukturen; • hierarchische Gliederung von Softwaresystemen in Komponenten und Schnittstellen: – funktionale Dekomposition (Zerlegung in unabhängige, in sich geschlossene Teilaufgaben und deren Lösungen, Modularität), – Zusammenfassung zusammengehöriger Bestandteile zu eigenständigen Einheiten (Modulbildung, Schnittstellen, Kapselung); • einfache Denkmodelle zum besseren Verständnis der Aufgabenstellung und möglicher Lösungen (Vereinfachung, Reduktion auf das Wesentliche, Gedankenmodelle);

1.4 Modellierung in der Systementwicklung

11

• Abstraktionsebenen (Ebenen unterschiedlichen Detaillierungsgrads) und der Übergang zwischen diesen Ebenen durch schrittweise Verfeinerung; • Verbergen von Implementierungsdetails durch eigenschaftsorientierte Beschreibung, Kapselung und Schnittstellenbildung („Information Hiding“, „Geheimnisprinzip“). Ein wesentliches Konzept zur Abstraktion bei der Betrachtung von Systemen und Programmen sind Schnittstellen. Bei der Zerlegung eines Problems oder eines Systems ist es erforderlich, eine klare Beschreibung der Schnittstelle von Programmeinheiten angeben zu können. Die Schnittstellen erfassen die Wechselwirkungen zwischen Teilsystemen. Definition 1.1 (Schnittstelle, nach DIN 44300). Eine Schnittstelle ist der gedachte oder tatsächliche Übergang an der Grenze zwischen zwei Funktionseinheiten mit den vereinbarten Regeln für die Übergabe von Daten oder Signalen.  Wir verstehen den Begriff der Schnittstelle noch deutlich weiter, auch im Sinn der von einer Programmeinheit, etwa einer Datenstruktur oder einer Menge von Rechenstrukturen, zur Verfügung gestellten Konstrukte. Eine Schnittstelle einer Programm- oder Systemeinheit wird durch die Angabe aller Informationen beschrieben, die nötig sind, um • eine syntaktisch fehlerfreie Benutzung der Elemente der Einheit sicherzustellen (syntaktische Schnittstelle), • das Verhalten der Einheit zu dokumentieren, soweit es für die Schnittstelle bedeutsam ist, um ihren sinnvollen und korrekten Gebrauch zu ermöglichen (semantische Schnittstelle, Verhaltensschnittstelle, Nutzungsschnittstelle), • die Korrektheit der Realisierung bezogen auf das Verhalten an der Schnittstelle nachweisbar zu machen. Die Festlegung und Beschreibung einer Schnittstelle zielt auf die Kapselung und das Verbergen der für die Nutzung nicht relevanten Information (Schnittstellenabstraktion). Insbesondere dient die Schnittstellenbeschreibung als Vertragsund Verständigungsgrundlage zwischen den Benutzern einer zu entwickelnden Software- oder Hardwareeinheit und den Implementierern und Verifizierern dieser Einheit. Gute Schnittstellenkonzepte abstrahieren möglichst weitgehend von den für das Schnittstellenverhalten unerheblichen Implementierungsdetails. Wir sprechen auch von einer Verhaltens- oder Dienstschnittstelle. Ihre Beschreibung erfolgt durch eine Schnittstellenspezifikation.

1.4 Modellierung in der Systementwicklung Für die Entwicklung von Software sind – auf Basis umfangreicher Kenntnisse über die Struktur von Rechnern und verwendete Softwareumgebungen – folgende Tätigkeiten erforderlich:

12

1 Einführung in die Grundlagen der Softwareentwicklung

• Zutreffende Erfassung der Aufgabenstellung und deren unmissverständliche Beschreibung (Anforderungsanalyse, Problemspezifikation); • Modellierung der Aufgabenstellung durch Konzepte der Informatik wie Datenstrukturen, Zustände und Funktionen; • Abbildung der geforderten Funktionalität auf Algorithmen und Daten mit dem Ziel der letztendlichen Umsetzung in Programme.

domain theory

Beides erfordert die Befähigung zur Problemlösung im Anwendungsgebiet sowie Kenntnisse über die Implementierungsplattform und die Strukturen in Informationsverarbeitungsprozessen. Kreativität in der Problemlösung kann man nicht im klassischen Sinn lehren. Jedoch kann man sich die Methodik aneignen, die eine Basis zur Problemlösung bietet. Zudem können Problemlösungsbeispiele wertvolle Übung und Schulung bieten. Das Ziel der Programmentwicklung besteht darin, Programmeinheiten anzufertigen, die bei der Lösung von Problemen der realen Welt helfen. Um solche Probleme der realen Welt, der Anwendung, einer Bearbeitung mit Mitteln der Informatik zugängig zu machen, ist eine Modellierung dieser Probleme und aller relevanten Zusammenhänge des Anwendungsgebietes mit Mitteln der Informatik erforderlich. Im Rahmen der Analyse und Modellierung entsteht eine Anwendungsbereichstheorie oder ein Domänen- oder Bereichsmodell (engl. domain theory, domain model). Dieses erfordert stets eine Abstraktion (Weglassen unwichtiger Teilinformation) und eine Ausschnittsbildung (Konzentration auf wesentliche Teilaspekte). Beispielsweise erfordert ein Programm zur Steuerung einer Heizung über Sensoren eine Bereichsmodellierung des Heizens und der Wärmeausbreitung sowie der Sensorik und Aktuatorik. Ein Programm zur Verwaltung von Konten in einer Bank erfordert eine Bereichsmodellierung für das Bankengeschäft und die Informationen für die Kontenverwaltung. Ein Programm zur Vermittlung von Telefongesprächen erfordert eine Bereichsmodellierung des Telefonierens und der Vermittlungsnetze sowie der Gebührenberechnung. Ein Programm für die Aufgabe des automatisierten Fahrens erfordert die Modellierung der Fahraufgabe und dafür ein Umgebungsmodell. Um für die Programmierung hilfreich zu sein, müssen wesentliche Teile der Bereichstheorie des Anwendungsgebietes mit Modellierungsmitteln erfasst und beschrieben werden. Eine informelle Problemstellung kann in der Regel mit sehr unterschiedlichen Mitteln der Informatik modelliert werden. Die sorgfältige und geschickte Wahl dieser Mittel ist für die erfolgreiche und ökonomische Durchführung der Programmentwicklung von zentraler Bedeutung. Bei der Wahl einer Modellierung ist auf die Adäquatheit1 im Sinne der Aufgabenstellung zu achten, auf die Verständlichkeit, auf die Handhabbarkeit und auf die Umsetzbarkeit in eine Lösung und schließlich in ein Programm. Oft ist eine geschickte Modellbildung im Sinn gut gewählter Denkmodelle der Schlüssel zur Problemlösung. Wir demonstrieren dies an einem einfachen Beispiel.

1 Alle relevanten Aspekte der Aufgabenstellung werden angemessen wiedergegeben.

1.4 Modellierung in der Systementwicklung

13

Beispiel 1.1 (Einfachheit der Modellierung als Schlüssel zur Problemlösung). In einer amerikanischen Quizsendung kann der Kandidat ein Auto gewinnen, wenn er von drei gegebenen, verschlossenen Toren A, B und C dasjenige Tor errät, hinter welchem das Auto verborgen ist. Um den Ablauf spannender zu machen, läuft diese Auswahl wie folgt ab: Zuerst darf sich der Kandidat für eines der drei Tore entscheiden. Der Quizmaster öffnet darauf von den verbleibenden zwei Toren eines, hinter dem sich das Auto nicht befindet. Es verbleiben also noch zwei verschlossene Tore, und hinter einem von ihnen steht das Auto. Nun erhält der Kandidat erneut die Möglichkeit, zwischen den verbleibenden zwei Toren zu wählen. Es ergeben sich dabei offensichtlich folgende mögliche Strategien für den Kandidaten: (1) Er wählt eingangs ein Tor und revidiert diese Wahl später nicht. (2) Er wählt eingangs ein Tor und wählt dann frei unter den verbliebenen zwei Toren. (3) Er wählt eingangs ein Tor, revidiert aber später seine Wahl und wechselt somit auf das andere, verbliebene Tor. Über die Frage, welche der Vorgehensweisen die aussichtsreichste ist, ist es vor einigen Jahrzehnten zu erbitterten Diskussionen in den amerikanischen Massenmedien gekommen. Die Strategie (1) ergibt zweifellos eine 1/3-Chance für den Gewinn des Autos. Die Strategie (2) ergibt eine 1/2-Chance, da beide verbleibenden Tore gleich wahrscheinlich sind und hinter einer das Auto steht. Eine einfache Erklärung dafür, dass die Strategie (3) am vielversprechendsten ist, ist das Resultat der adäquaten Modellierung (oder aber alternativ einer nicht ganz unaufwändigen Rechnung in Wahrscheinlichkeitstheorie). Behauptung: Die Strategie (3) ist gleichwertig damit, dass der Kandidat zwei Tore wählt und das Auto erhält, wenn es sich hinter einem dieser Tore befindet. Somit ergibt sich bei Strategie (3) eine 2/3-Chance für den Gewinn des Autos. Begründung: Der Kandidat kann zwei Tore wählen, und bekommt das Auto, wenn sich dieses hinter einem der Tore befindet. Dazu geht er wie folgt vor: Angenommen der Kandidat entscheidet sich für die beiden Tore A und B. Dazu gibt er im ersten Durchgang vor, Tor C zu wählen. Folgerichtig öffnet der Quizmaster dann Tor A oder B. Nun wechselt der Kandidat das Tor seiner Wahl von C auf das verbleibende Tor. Ist das Auto hinter Tor A oder B, so gewinnt er es bei dieser Taktik sicher.  Das Beispiel zeigt, wie durch eine geschickte Modellierung eine Sichtweise auf das Problem erreicht wird, die Komplexität reduziert, und das Problem gelöst werden kann, ohne dass notwendigerweise in eine komplizierte Argumentation beispielsweise mit Mitteln der Wahrscheinlichkeitstheorie eingetreten werden muss. (Für die wahrscheinlichkeitstheoretische Behandlung verweisen wir auf das mathematisch äquivalente Bertrand’sche Kästchenparadoxon [Ber89a].) Berührt wird durch das Beispiel ein wichtiges Feld der Programmierung, nämlich das Problemlösen und die Reduktion von Komplexität durch geschickte

14

1 Einführung in die Grundlagen der Softwareentwicklung

Modellbildung. Der Programmentwickler ist immer wieder mit den unterschiedlichsten Problemen konfrontiert, deren Lösung oft Scharfsinn und kreatives Denken erfordert. Hier kann eine gut gewählte Modellbildung und Reduzierung auf das Wesentliche helfen. Problemlösung kann man nicht in jeder Hinsicht systematisch erlernen. Hilfreich dafür ist aber ein geeignetes Handwerkszeug in Form von Wissen über Modellierungsmethoden und Erfahrung. In der Programmentwicklung ist nicht nur die Modellierung von Bedeutung, sondern auch die Notation (äußere Form), in der das Modell wiedergegeben wird, und die Theorie, auf die es sich abstützt. Dabei ist klar, dass die Notation selbst nicht die Lösung im Sinne einer guten Idee garantiert, sondern nur Mittel zum Zweck ist. Schon die Wahl der geeigneten Notation und der Modellierung ist jedoch oft Teil der erfolgreichen Suche nach einer guten Lösung. Daneben ist Kreativität und Problemverständnis von ausschlaggebender Bedeutung für das Entwickeln angemessener Lösungen. Dabei können fundierte Kenntnisse über Modelle und deren Darstellung helfen. Oft ist nicht nur die Lösung eines Problems, sondern seine angemessene Erfassung und Formulierung ein schwieriger Schritt und gleichzeitig ein wesentlicher Beitrag zur Lösung.

1.4.1 Modellierung der Problemstellung und der Anforderungen Probleme und Anforderungen sind bei der Entwicklung von Systemen und von Software in der Regel oft unklar und zunächst bestenfalls informell gegeben. In der Praxis ist die Aufgabenstellung oftmals vage, in sich widersprüchlich und oft an gegenläufigen Zielen des Anwendungsgebietes und der Beteiligten orientiert. Manchmal sind die Anforderungen an ein Softwaresystem zunächst völlig unklar oder gar strittig und müssen über aufwändige Analysen, Abstimmungsprozesse und Experimente identifiziert werden. Betrachten wir ein einfaches Beispiel für eine Anforderung an ein System. Beispiel 1.2 (Problemstellung Geschwindigkeitsregelung – engl. speed control, cruise control). In einem Fahrzeug soll die Steuerung für eine Geschwindigkeitsregelung realisiert werden. Durch die Geschwindigkeitsregelung kann der Fahrer eine Geschwindigkeit vorgeben, die dann vom System eingehalten wird. Beim Einschalten der Anlage soll die Beschleunigung sanft aber spürbar einsetzen. In kritischen Situationen soll sich das System abschalten und dies dem Fahrer unübersehbar anzeigen.  Was „sanft aber spürbar“ genau bedeutet, ist dabei nicht weiter festgelegt, ebenso wenig wie „kritische Situation“ oder „unübersehbar“. Für die eindeutige Festlegung muss eine umfangreiche Bereichsmodellierung und Analyse durchgeführt werden, die unter Umständen auch eine Reihe von Experimenten mit den potentiellen Nutzern eines Systems einschließt.

1.4 Modellierung in der Systementwicklung

15

Oftmals ist eine Ermittlung und detaillierte Erarbeitung der informellen Anforderungen wesentlicher Teil einer Programmentwicklung. Wir sprechen von der Analysephase und Spezifikationsphase und auch vom Requirements Requirements EngineeEngineering. Erforderlich sind dabei in der Regel umfangreiche Untersuchungen ring und Interviews, um unter anderem die Nutzererwartungen, Marktchancen und Randbedingungen zu erfassen. Gearbeitet wird in gemischten Teams von Mitarbeitern mit ganz unterschiedlichem fachlichen Hintergrund. Parallel zur Identifizierung und Bewertung der informell gegebenen Anforderungen kann mit einer Formalisierung der Anforderungen begonnen werden. Ziel ist dabei die präzise, unmissverständliche Festlegung der Anforderungen und ihre Darstellung durch Standardbeschreibungsmittel. Dies dient unter anderem der Sicherstellung einer vollständigen Festlegung aller Anforderungen als Vorgabe für die Realisierung. Als Teil der formalen Anforderungsspezifikation sind auch die Strukturen des Anwendungsgebietes formal zu modellieren, soweit sie für die Problemstellung bedeutsam sind. Dies sind die Rechenstrukturen (Datenmengen und Funktionen), die an der Schnittstelle des Systems (Nutzungssicht) auftreten oder die Teile der fachlichen Aspekte erfassen. Die Anforderungsspezifikation bestimmt die Funktionalität eines Systems Anforderungsspezifikation und damit seine Nutzbarkeit, aber auch die Entwicklungskosten. Hauptziel ist die Beschreibung von fachlichen Anforderungen an das System. Die Anforderungsbeschreibung dient im weiteren Verlauf einer Entwicklung auch der funktionalen Qualitätssicherung (durch Verifikation und Test) der Realisierung und ist schließlich Bestandteil der Vereinbarung zwischen Auftraggeber und Auftragnehmer in einer Softwareentwicklung.

1.4.2 Modellierung der Lösungsarchitektur Liegen die Anforderungen fest, so ist im ersten Schritt in Richtung Realisierung eine Struktur der Daten und Komponenten für das zu erstellende Softwaresystem festzulegen. Wir sprechen vom umfassenden Datenmodell und von der Softwarearchitektur oder auch vom Grobdesign. Oft wird auch zwischen Fachentwurf (Strukturierung und Spezifikation im Sinn der Anwendung) und dem softwaretechnischen Entwurf (Gliederung in implementierungstechnische Einheiten, in der Objektorientierung Gliederung in Klassen) unterschieden. Die Qualität des Datenmodells und der Softwarearchitektur bestimmt in weiten Teilen die Stabilität, Leistungsfähigkeit und den Entwicklungsaufwand für ein Softwaresystem. Auch das Datenmodell und die Softwarearchitektur werden in der Regel zunächst skizzenhaft informell angegeben, als Gliederung des Systems in Teileinheiten (genannt Komponenten), und deren Vernetzung und Interaktionen. In einem nachfolgenden Schritt wird dann detailliert festgelegt, in welcher Form die Komponenten miteinander kooperieren, um die in der Anforderungsspezifi-

Grobdesign Fachentwurf softwaretechnischer Entwurf

16

1 Einführung in die Grundlagen der Softwareentwicklung

kation geforderte Funktionalität zu garantieren. Dies dient als Richtschnur für die Festlegung der Schnittstellen für die Gliederung der Komponenten. Bei umfangreichen Systemen kann es erforderlich sein, auch für die Komponenten selbst wieder Architekturen festzulegen. Diese hierarchische Zerlegung endet, wenn das System in Einheiten zerlegt ist, deren Funktionalität durch Programme überschaubarer Größe (grob eine Seite Programmtext) realisiert werden kann. Der Stil und die Form der Beschreibung der Softwarearchitektur hängt vom verwendeten Komponentenbegriff ab. Wir werden im Weiteren eine Reihe unterschiedlicher Komponentenbegriffe kennenlernen (wie etwa Funktion, Prozedur, Klasse, Objekt, Modul, Methode, Komponente, interaktives Teilsystem). Für die umfassende Beschreibung einer Softwarearchitektur sind Schnittstellenbeschreibungen für die verwendeten Komponenten erforderlich. Diese können wieder informell oder formal erfolgen.

1.4.3 Implementierung und deren Modellierung sowie Verifikation Die Implementierung erfolgt durch die Realisierung der in der Softwarearchitektur festgelegten Komponenten durch Programme („Algorithmen“) und durch die Realisierung der im Datenmodell festgelegten Strukturen durch Datentypen in Programmiersprachen oder durch Datenbanken. Programme bestehen aus Algorithmen, die über Datenstrukturen arbeiten. Zur Spezifikation, Verifikation und Dokumentation der Programme ist deshalb eine Modellierung der Eigenschaften sowohl der Datenstrukturen als auch der Algorithmen erforderlich. Dabei ist es sinnvoll, die Realisierung der Funktionen durch Algorithmen und die Realisierung der Rechenstrukturen durch geschickt gewählte Datenstrukturen in gesonderte Programmeinheiten nach dem Prinzip des „Information Hiding“ zu kapseln. Die Korrektheit der Implementierung wird durch Verifikation nachgewiesen.

1.5 Aspekte der Modellbildung Die Methoden der Modellbildung und die Modelle der Informatik sind vielfältig und haben eine Vielzahl von Facetten. Schon die Beschreibungsformen der Modelle, die Strukturen der Modelle und die Eigenschaften der Modelle haben sehr unterschiedliche Ausprägungen und werfen viele Fragen auf. Im Folgenden diskutieren wir einige der zentralen Gesichtspunkte zu diesem Thema.

1.5 Aspekte der Modellbildung

17

1.5.1 Deskriptive versus operationelle Beschreibungen Eine wesentliche Aufgabe der Informatik ist die Beschreibung von Eigenschaften von Programmen und Aspekten der Programmentwicklung. Beispiele sind die Beschreibung einer Problemstellung („Spezifikation“) oder der strukturellen Eigenschaften von Software. In der Informatik unterscheiden wir zwischen operationellen und deskriptiven Beschreibungen. Eine operationelle Beschreibung entspricht einem Programm (einer Auswertungsvorschrift) und kann durch einen Interpreter (genauer durch einen Algorithmus) mechanisch ausgewertet werden. Die Angabe dieses Auswertungsalgorithmus nennen wir operationelle Semantik der Beschreibungstechnik. Manchmal wird der operationelle Semantik Auswertungsalgorithmus lediglich durch eine Menge von Auswertungsregeln beschrieben. Eine rein deskriptive Beschreibung einer Problemstellung oder eines Programms erfolgt mit Mitteln der Logik und Mathematik ohne ein konstruktives Ausführungskonzept. Sie kann – selbst wenn sie formal ist – nicht notwendigerweise durch einen Algorithmus ausgewertet werden. Unter Umständen stoßen wir an die Grenze der Berechenbarkeit. Beschrieben wird die Wirkung, der Effekt, nicht der Mechanismus. Die semantische Bedeutung deskriptiver Beschreibungen wird auf mathematische Strukturen (Mengen, Relationen, Funktionen) oder auf Logik zurückgeführt. Wir sprechen von funktionaler funktionale Semantik oder axiomatischer Semantik. Für die Modellierung abstrakter Sichten auf ein axiomatische Semantik Softwaresystem und die Beschreibung von Zusammenhängen und Schnittstellen sind deskriptive Beschreibungen wertvoll. Ihr Wert liegt in ihrer Abstraktion und Präzision (im Vergleich zu informellen Beschreibungen), Verständlichkeit und einheitlicher Strukturierung. Sie dienen auch als Grundlagen in der Verifikation. Deskriptive Beschreibungstechniken haben den Vorteil, dass der Entwickler nicht durch Beschränkungen wie Ausführbarkeit oder Effizienz eingeengt wird. Gerade bei der Durchführung einer Bereichsmodellierung oder der Erarbeitung einer Problemspezifikation oder einer Lösungsstruktur ist Verständlichkeit und Ausdruckskraft oft wichtiger als Ausführbarkeit.

1.5.2 Nutzungs- und Realisierungssicht Die Unterscheidung zwischen der Implementierungssicht (auch Realisierungssicht, engl. glass-box view), die den inneren Aufbau eines Programms beschreibt, und der Zugriffssicht (auch Nutzungssicht, engl. black-box view) ist fundamental für die Programm- und Systementwicklung. Dies schafft wesentliche Abstraktionsmöglichkeiten und die Grundlagen für das Prinzip des Information Hiding. • Die Black-Box-View (Schnittstellensicht, Nutzungssicht, Zugriffssicht)

Zugriffssicht, Black-BoxView

18

Implementierungssicht, Glass-Box-View, White-Box-View

1 Einführung in die Grundlagen der Softwareentwicklung

zeigt das Programm oder System als eine funktionale Einheit in seiner Wirkung nach außen ohne Berücksichtigung seines strukturellen „inneren“ Aufbaus. • Die Glass-Box-View (auch White-Box-View, Realisierungssicht, Implementierungssicht) berücksichtigt zusätzlich die innere Gliederung und den Aufbau eines Programms oder Systems, etwa seine Gliederung in Anweisungen oder seine Unterteilung in Komponenten und deren Vernetzung. Zentral für dieses Prinzip der Trennung von Zugriffs- und Implementierungssicht ist der Schnittstellenbegriff (engl. interface). In der Schnittstellensicht wird nur die Außenwirkung eines Programmelements beschrieben. Wir benötigen dazu eine in sich schlüssige, präzise und praktikable Technik zur Beschreibung der Schnittstelle. Eine Schnittstellenbeschreibung gibt die Zugriffsmöglichkeiten auf ein Programmelement wieder. Durch die Spezifikation der Schnittstelle werden der Gebrauch und die Funktionalität des Programmelements festgeschrieben. Die konsequente Trennung von Zugriffs- und Implementierungssicht kennzeichnet gutes Softwaredesign, besonders bei der Entwicklung umfangreicher Programmsysteme. Dies dient der Entkopplung von Nutzung und Realisierung und somit der Komplexitätsreduktion durch Verbergen von Implementierungsdetails und von Einzelheiten der Optimierung der Implementierung unter Beibehaltung der Schnittstelle. Das Prinzip lässt sich nicht nur auf Rechenund Datenstrukturen, sondern auch auf Programmeinheiten wie funktionale Rechenvorschriften, Prozeduren, Module und Klassen (in der objektorientierten Programmierung, vgl. Kap. 8) und letztlich auf ganze Systeme übertragen. Wir werden im Folgenden dem Schnittstellenthema verstärkt Aufmerksamkeit schenken.

1.5.3 Abstraktionsebenen und Schichtenarchitekturen Es ist besonders typisch für die Methoden der Informatik, dass die Sichten auf ein System in eine Hierarchie von Abstraktionen organisiert sind. Wir sprechen von Abstraktionsebenen. So lässt sich über einer Implementierungsplattform mithilfe geeigneter Funktionen eine Sicht auf eine andere „abstraktere“ Rechenstruktur aufbauen. Abstraktion ist eines der wichtigsten Prinzipien in der Softwareentwicklung zur Komplexitätsreduktion. Abstraktion bedeutet das Weglassen unwesentlicher Details und die Konzentration auf das für die jeweilige Aufgabe Wesentliche. In der Programmentwicklung wird oft eine Folge von Abstraktionen eingesetzt. Zu Beginn ist die Vereinfachung durch Abstraktion am stärksten. Dann werden Abstraktionsstufen („Ebenen“) betrachtet, in denen mehr und mehr Details hinzugefügt werden. Wir sprechen auch von schrittweiser Verfeinerung. Ein anderes Prinzip ist die Strukturierung von Systemen in Schichten, die aufeinander aufbauen.

1.5 Aspekte der Modellbildung

19

Beispiel 1.3 (Schichtenarchitektur). Software ist oft in Schichten aufgebaut. Jede Schicht bietet eine bestimmte Funktionalität an und bedient sich zur Realisierung dieser Funktionalität der darunter liegenden Schicht. Dies ist das Prinzip einer Schichtenarchitektur.  Schichtenbildung findet sich in unterschiedlichen Ausprägungen. Ein Beispiel ist das ISO/OSI-Schichtenmodell (vgl. [Ker92] und [Int84]). Ein anderes Beispiel ist die Schichtenarchitektur betrieblicher Informationssysteme (vgl. [Den91]).

1.5.4 Zum Begriff des Modells Neben den Begriffen System, Software und Programm haben wir bisher immer wieder den Begriff des Modells gebraucht. Wie viele zentrale Begriffe des Software Engineering wird der Begriff des Modells in einer Reihe unterschiedlicher Bedeutungen verwendet. Allgemein verstehen wir unter einem Modell Modell ein vereinfachtes Abbild – also eine Abstraktion – eines Ausschnitts der realen oder gedachten Welt. Ein Modell kann sehr gegenständlich sein, wie bei einem Gebäudemodell für die Begutachtung des Entwurfs oder einem Flugzeugmodell für den Windkanal. Da die Informatik Systeme betrachtet, die in der Regel nicht unmittelbar anschaulich sind, sind die Modelle der Informatik auch entsprechend abstrakt und auf den ersten Blick oft wenig anschaulich. Eine verbreitete Tendenz ist deshalb heute die Verwendung grafischer Modellierungstechniken mit dem Ziel der Verbesserung der Anschaulichkeit (siehe die „Unified Modeling Language“, UML [BW99] und [Bur97]). In der Softwareentwicklung ist eine Reihe unterschiedlicher Aufgaben der Modellbildung zu lösen und entsprechend werden darauf zugeschnittene Modelle verwendet. Die unterschiedlichen Aufgaben der Modellbildung und ihrer Zielsetzung in der Informatik führen auf zwei Klassen von Modellen: • Domänenmodell (engl. domain model): Abbild eines Ausschnitts der Domänenmodell realen oder gedachten Welt des Anwendungsgebietes als Grundlage für die domain model Datenstrukturen, die Problemspezifikation oder die Problemlösung; • System-, Software-, Programmmodell: Modellierung des Softwaresystems, Systemmodell seiner Struktur und seines Verhaltens als Grundlage für die Strukturierung und Formulierung der Lösung einer Entwicklungsaufgabe in Form eines Softwaresystems oder dessen Analyse. Beide Aufgaben der Modellierung sind eng verwandt, da Teile des Domänenmodells in das Systemmodell eingehen. Streng genommen ist natürlich auch ein Softwaresystem Teil der realen oder gedachten Welt. Bei der Aufgabe der Modellierung als Ausschnittsbildung, Vereinfachung und Abstraktion ist es erforderlich, das entstehende Modell syntaktisch zu repräsentieren. Dazu werden bestimmte Notationen (Sprachen und Grafiken)

20

1 Einführung in die Grundlagen der Softwareentwicklung

verwendet. Wir sprechen von Modellierungssprachen und von Beschreibungstechniken. Eine Programmiersprache ist syntaktisch eine formale Sprache. Eine formale Sprache besteht aus einer Menge von Sequenzen von Zeichen („Wörter“). Die Syntax der Programmiersprache legt fest, welche Zeichenfolgen Elemente der Sprache und somit syntaktisch korrekte Programme sind. Durch die Semantik der Programmiersprache wird festgelegt, welche Bedeutung Programme haben. Die operationelle Semantik legt fest, welche Folgen von Abarbeitungsschritten durch ein Programm beschrieben werden. Die denotationelle oder deskriptive Semantik legt fest, welche Funktion, welche Abbildung von Eingabedaten auf Ausgabedaten ein Programm definiert. Wie bei Programmiersprachen können wir für Modellierungssprachen zwischen Syntax und Semantik unterscheiden. Die Syntax, also die äußere Struktur der Aufschreibung, kann durch sogenannte Metamodelle – durch Modelle, die eine Menge von Modellen beschreiben – bestimmt werden. Die Semantik, also die Bedeutung, einer Modellierungssprache kann informell oder durch Abbildung auf eine mathematische Struktur, eine Algebra, angegeben werden. Die mathematische oder logische Struktur zur Angabe der Semantik ist auch wieder ein Modell. Auch Programmiersprachen können als Modellierungssprachen Verwendung finden. Wird ein Modell durch eine Modellierungssprache dargestellt, deren Semantik durch die Abbildung auf mathematische Strukturen erklärt ist, so können wir das Modell auch als mathematische Struktur darstellen und verstehen. Wichtig ist es, beim Begriff des Modells zwischen der syntaktischen Darstellung des Modells, der konkreten Notation, der konzeptuellen oder mathematischen Idee und dem durch das Modell dargestellten Ausschnitt der realen oder gedachten Welt zu unterscheiden.

1.6 Besondere Entwicklungsansätze Neben Themen der Modellierung, die in den folgenden Kapiteln im Zentrum stehen, existieren Ansätze zur Entwicklung von Programmen, die weniger explizit auf Modellbildung ausgerichtet sind.

1.6.1 Agilität Seit etlichen Jahren ist agiles Vorgehen eine der weitverbreiteten Vorgehensweisen in der Praxis. Die Grundidee dabei ist, dass man sehr früh Code-zentriert arbeitet, dass man sehr früh mit dem Codieren beginnt, und, zumindest in einer Reihe von Fällen, auf eine explizite Modellierung außerhalb der Programmierung verzichtet, teilweise auch auf eine Spezifikation. Das Programm wird

1.6 Besondere Entwicklungsansätze

21

iterativ Schritt für Schritt vervollständigt. Die Vorgehensweise in der Agilität besteht insbesondere darin, dass es einen sogenannten „Product Owner“ gibt, der für die jeweiligen sogenannten „Sprints“, also für die meist recht kurz gehaltenen Phasen, die die Umsetzung bis zu einem nächsten Zwischenstand der Entwicklung kennzeichnen, die Anforderungen vorgibt. Zu jedem Zeitpunkt existiert ein ausführbares Programm (Stichwort: „Continuous Integration“). Agiles Vorgehen ist sehr erfolgreich bei Projekten überschaubarer Größe, insbesondere wenn in Teams bis zu maximal zehn Personen gearbeitet werden kann. Für große Systeme, insbesondere für sicherheitskritische Systeme, stoßen agile Methoden jedoch an Grenzen und erfordern zumindest eine Erweiterung des Ansatzes um Themen der Dokumentation und der Projektsteuerung. Das vorliegende Buch beschreibt eine ganze Reihe von Techniken, die durchaus auch für agiles Vorgehen eingesetzt werden können. So ist die Technik des Design-by-Contract, aber auch ein nachhaltiges Verständnis von Datenstrukturen oder der Art und Weise, wie man die Terminierung eines Algorithmus nachweist, auch im Bereich des agilen Vorgehens durchaus von Interesse. Insgesamt erhebt dieses Buch den Anspruch, dass selbst für einen ausschließlich praxisorientierten Programmierer, wie beispielsweise einen Web-Designer, die grundlegenden Techniken • der strukturierten Beschreibung von Datenstrukturen und von logischen Eigenschaften von Algorithmen und • des Nachweises der Terminierung und der Methoden, Schnittstellen zu beschreiben, nützlich sind. Gerade in den unterschiedlichen Anwendungsbereichen der Programmierung tauchen im Grunde genommen immer wieder die gleichen Konzepte auf, die in diesem Buch grundlegend beschrieben werden.

1.6.2 Lernende Systeme Ein thematischer Bereich der Programmentwicklung, der gerade in den letzten Jahren viel an Bedeutung gewonnen hat, wird im vorliegenden Werk vollständig ausgeklammert. Es handelt sich dabei um sogenanntes „maschinelles Lernen“ (engl. machine learning), in der Regel abgestützt auf neuronale Netze und die Verwendung großer Datenmengen von Trainingsbeispielen, die dann eine entsprechende Programmierung der Systeme erlauben. Durch die Leistungszuwächse im Bereich der Hardware und Fortschritte in der Methodik, unterschiedliche Versionen von sogenanntem „maschinellen Lernen“ aufzusetzen, ist das Gebiet, das zunächst keine sehr starke praktische Bedeutung hatte, in den letzten Jahren in einer Reihe von Anwendungsbereichen sehr erfolgreich. Wichtige Beispiele sind die Interpretation von Bildern und Videos oder das Erkennen von Sprache, und zwar sowohl das Umwandeln von gesprochener

22

1 Einführung in die Grundlagen der Softwareentwicklung

Sprache in Text als auch eine Interpretation des Textes im Hinblick auf seine Bedeutung. Ganz allgemein von Interesse sind Anwendungen in der Analyse großer Datenmengen. Eine der großen Herausforderungen in diesem Bereich ist, dass auch durch eine hohe Anzahl von Trainingsbeispielen, die eben auch durch die Fortschritte im Bereich der Erfassung großer Datenmengen durch Sensorik und auch durch die Fülle der Nutzereingaben im Internet inzwischen verfügbar sind, ganz erstaunliche Programme auf diese Weise generiert werden. Allerdings ist das Ergebnis eine „Black Box“, ein Algorithmus in Form eines neuronalen Netzes, das durch die Trainingsdaten mit entsprechenden Koeffizienten versehen worden ist und auf jede Eingabe ein Ergebnis liefert, von dem aber keine Spezifikation existiert und auch keine wirkliche Garantie für sein Verhalten außer eingeschränkte statistische Aussagen. Insbesondere ist es im Augenblick eine große Herausforderung für die Forschung, wie man entsprechende Ergebnisse des Trainingsprozesses verifiziert. Die bisherigen Ansätze bedienen sich allesamt eher Erprobungsszenarien unter Einbezug von Simulation. Vor diesem Hintergrund verzichten wir nachstehend völlig darauf, entsprechende Verfahren für „maschinelles Lernen“ zu behandeln.

1.7 Der Einfluss der Notation Grundsätzlich müssen die im Rahmen einer Programmentwicklung auftretenden Zwischen- und Endversionen von Modellen, Spezifikationen, Programmen oder Daten notationell („syntaktisch“) repräsentiert werden. Dies kann durch Schrift in natürlicher Sprache, durch eine formale Sprache oder grafisch erfolgen. Natürlich wird sinnvollerweise eine gute Kombination dieser Mittel eingesetzt. Natürliche Sprache hat den Vorteil, dass sie (in der Regel) von allen Beteiligten beherrscht oder zumindest verstanden wird. Sollen jedoch – wie in der Programmentwicklung häufig nötig – komplizierte technische Zusammenhänge ausgedrückt werden, so erweist sich natürliche Sprache oft als nicht ausreichend präzise oder nicht strukturiert genug. Natürlichsprachliche Beschreibungen tragen – stärker noch als formale Beschreibungen – das Risiko • verdeckter Widersprüche, • unbeabsichtigter Unvollständigkeit, • unbeabsichtigter Mehrdeutigkeit (mögliche Fehlinterpretationen). Insbesondere das zuletzt genannte Problem kann durch geschickt gewählte formale Beschreibungen vermieden werden. Ein weiterer Nachteil natürlichsprachlicher Beschreibungen besteht oft in ihrem Umfang, ihrer mangelnden Strukturierung und ihrer Unübersichtlichkeit. Zudem ist der Grad der Automatisierung bei Werkzeugunterstützung eingeschränkt. Formale Beschreibungen erfordern in der Regel entsprechende Expertise und zeigen oft Schwächen in der Lesbarkeit und Verständlichkeit für den ungeübten Leser.

1.7 Der Einfluss der Notation

23

Eine Beschreibungstechnik und eine darin abgefasste Beschreibung werden formal genannt, wenn (1) für sie eine formale Sprache (Syntax) festgelegt ist, oder im Fall grafischer Beschreibungen eine äußere „syntaktische“ Form, (2) die Bedeutung (Semantik) der Syntax mathematisch genau beschrieben ist, (3) zur Semantik konsistente Analyse-, Umformungs- und Beweisregeln vorliegen (auf Basis einer geeigneten „Theorie“). Ist nur der Punkt (1) gegeben, so sprechen wir von einem syntaktischen Rahmen und von halbformalen Beschreibungsmitteln. Bezüglich Punkt (2) unterscheiden wir operationelle und deskriptive Beschreibungsmethoden. Operationelle Beschreibungsmittel beschreiben Zusammenhänge durch Algorithmen und Auswertungsverfahren und erlauben somit eine mechanische Auswertung. Deskriptive Beschreibungen unterstützen eine stärkere Abstraktion und eine Konzentration auf Eigenschaften eines Systems. Formale Beschreibungstechniken bieten in der Regel folgende Vorteile gegenüber natürlichsprachlichen Beschreibungen: • vergleichsweise geringer Umfang (Kompaktheit); • Präzision; • festgelegte Struktur, genaue Umformungsregeln (Maschinenunterstützung, maschinelle Verarbeit- und Auswertbarkeit); • einheitlicher Modellrahmen (Verständnisunterstützung); • Katalogisierung erfassbarer Eigenschaften; • Einsetzbarkeit formaler Verifikation. Allerdings können in der Regel Menschen ohne angemessene Schulung formale Beschreibungen kaum verstehen und erst recht nicht selbst erarbeiten. Dies ist ein gravierender Nachteil für die Akzeptanz formaler Techniken. Umfangreiche, ungenügend strukturierte formalisierte Beschreibungen wirken oft abschreckend. Deshalb kommt es wesentlich darauf an, formale Techniken wohldosiert und strukturiert einzusetzen. Dabei darf jedoch von einem versierten Softwareentwickler erwartet werden, dass er einen Werkzeugkasten formaler Techniken der Modellierung beherrscht und zugleich versteht, wann und wie sie nutzbringend einsetzbar sind. Formalisierung ist eine wesentliche, ja unumgängliche Aufgabe in der Softwareentwicklung. Die Formalisierung einer Problemstellung dient dazu, • die Bearbeitung eines Problems durch Algorithmen (Maschinen) zu ermöglichen, • durch die mathematische Modellierung das präzise Verständnis für die Aufgabenstellung und deren Teilaspekte zu unterstützen. In der Praxis sind grafische Beschreibungstechniken aufgrund ihrer scheinbar intuitiven, anschaulichen Verständlichkeit verbreitet (zum Beispiel die „Unified Modeling Language“, UML). Grafische Beschreibungstechniken können natürlich nur formal genannt werden, wenn die vorher unter Punkten (1)

24

1 Einführung in die Grundlagen der Softwareentwicklung

und (3) aufgeführten Forderungen erfüllt sind, wobei zu Punkt (1) entsprechend statt einer formalen Sprache ein System von grafischen Repräsentationen unmissverständlich festgelegt sein muss. Werden grafische Methoden als Beschreibungstechniken ohne genaue Festlegung ihrer Bedeutung „halbformal“ verwendet, so besteht ähnlich wie bei natürlicher Sprache oder textuellen halbformalen Ansätzen die Gefahr unterschiedlichen Verstehens und der Fehlinterpretation. Leicht erzeugen gerade grafische Darstellungen die trügerische Sicherheit einfachen „intuitiven“ Verständnisses trotz der Gefahr unterschiedlicher Interpretationen bei unterschiedlichen Betrachtern. Wie andere Ingenieurdisziplinen braucht das Software Engineering einen allgemein akzeptierten Satz von Modellierungs- und Beschreibungsmitteln sowie Vorgehensweisen, Lösungsverfahren, Methoden und Konzepten. Allerdings wird es, bedingt durch die Dynamik des Gebietes und die noch anhaltenden Lernprozesse, noch einige Zeit dauern, bis die unterschiedlichen Ansätze vereinheitlicht und wissenschaftlich umfassend fundiert sind.

1.8 Historische Bemerkungen Die Wurzeln der Grundlagen der Softwareentwicklung reichen weit in die Logik und frühe Theorie der Berechenbarkeit zurück. Wir beginnen unsere historische Betrachtung jedoch erst zu dem Zeitpunkt ab 1940, als erste programmgesteuerte, frei programmierbare Maschinen entstanden (vgl. [Zus86] und [Bau98]). Erste Anwendungen programmgesteuerter, maschineller Informationsverarbeitung fanden sich im Bereich rechenintensiver arithmetischer, „numerischer“ oder kombinatorischer (Beispiel: Entschlüsselung von codierten Nachrichten im Zweiten Weltkrieg) Berechnungen sowie einfacher datenintensiver Anwendungen (Beispiel: Verwaltung, Buchhaltung). Als ein Beispiel für numerische Berechnungen können wir die Berechnungen von Schießtafeln für die Artillerie nennen. Der erste elektronische Rechner für diesen Einsatzzweck in den USA, genannt ENIAC (ursprünglich engl. Electronic Numerical Integrator, Analyzer, and Calculator, später und offiziell engl. Electronic Numerical Integrator and Computer), wurde an der Moore School of Electrical Engineering der University of Pennsylvania hergestellt. Dieser sollte der Artillerie bei der Berechnung der Flugbahnen helfen. In einem Bericht 1947 über ENIAC heißt es: „Dieser Computer kann die Flugbahn eines Geschosses, die in zweieinhalb Sekunden durchlaufen wird, in eineinhalb Sekunden berechnen. Die Programmierung dauert eineinhalb Tage.“ Die geringen verfügbaren Rechen- und Speicherleistungen erforderten hohen Aufwand bei der Anpassung der Algorithmen an die technischen Besonderheiten der Rechenmaschinen. Die beschränkten Hauptspeicher ließen nur vergleichsweise kleine Programme zu. Dementsprechend standen bei

1.8 Historische Bemerkungen

25

den Anfängen der Programmentwicklung die geeignete Wahl der Struktur der Rechenmaschinen und Techniken zur Effizienzsteigerung der Hardware im Vordergrund. Der Entwurf der Programme war nur eine untergeordnete Tätigkeit. Erst allmählich wurde deutlich, dass nicht nur der Entwurf von Rechnern, sondern auch deren Programmierung große Herausforderungen mit sich bringen.

1.8.1 Programmiersprachen Anfänglich waren die Rechenmaschinen vergleichsweise klein in Speicherumfang und gering in der Leistung. Entsprechend überschaubar im Umfang waren die Programme. Im Zentrum des Interesses war die Effizienz der Programme zur optimalen Nutzung der ohnehin zu geringen Leistung der Rechner. Allmählich wurde aber der Aufwand bei der Programmerstellung zum Problem. Um den Programmieraufwand in Grenzen zu halten, entstanden Schritt für Schritt allgemeinere Programmnotationen und daraus schließlich Programmiersprachen. Die allerersten Programmiersprachen im heutigen Sinn waren folgerichtig Assembler-Sprachen: Ein Assembler-Programm ist nichts anderes als ein Programm, welches für den Befehlssatz eines Computers, dessen Befehlswörter jeweils einen bestimmten Code haben, Schlüsselwörter vorsieht, die ein Mensch besser verstehen und sich leichter merken kann. Der Assembler ist für den jeweiligen Prozessor – bedingt durch unterschiedliche Befehlssätze – spezifisch. In Assembler geschriebene Programme sind deshalb in der Regel nicht unmittelbar auf anderen Prozessoren verwendbar. Die Fehleranfälligkeit maschineller Programmierung, der Wunsch nach der Nutzung eines Programms auf unterschiedlichen Maschinen – wir sprechen von Portierbarkeit – und der schließlich nicht mehr tragbare Arbeitsaufwand der maschinennahen Programmentwicklung führten zu den ersten nicht maschinennahen (sogenannten „höheren“) Programmiersprachen: • COBOL (Common Business-Oriented Language, vgl. [MPM00]) erlaubt COBOL es, Daten und Dateien zu verwalten, sortieren, selektieren und formatiert auszugeben. Man kann damit nur beschränkt rechnen. Die Syntax ähnelt eher gesprochenem Englisch denn Formeln. Mit COBOL wurden und werden etwa die Verwaltung von Bankkonten oder die Gehaltsabrechnung programmiert. • Fortran (FORmula TRANslation, vgl. [Cha04]) ist für Berechnungen gedacht Fortran (aber nicht für die Datenverwaltung), wie sie in naturwissenschaftlichen Fragestellungen vorkommen. Fortran wurde (und wird) daher auf Computern eingesetzt, die in der Forschung und technischen Entwicklung genutzt wurden. Aber die auf numerische Berechnungen ausgerichteten Strukturen waren kein Hindernis, Fortran nicht doch für ganz allgemeine Aufgaben einzusetzen. Mit Fortran wurden typischerweise umfangreiche Berechnungen wie Simulationen des Wetters, von Atomwaffen oder Raketenflugbahnen programmiert.

26 ALGOL

LISP

Prolog

1 Einführung in die Grundlagen der Softwareentwicklung

• ALGOL (ALGOrithmic Language, vgl. [BBG+ 60], [vK74] und [Pag84]) war eine der ersten durch ein internationales Komitee entwickelten Sprachen. ALGOL entstand stark aus den Bedürfnissen der Numerik, für ihre Algorithmen eine einheitliche, auf unterschiedliche Rechenanlagen verwendbare und ausführbare Notation zur Verfügung zu haben. • LISP (LISt Processing language, vgl. [MAE+ 85, Wag87, Bro87, McC78]) ist eine sehr mathematische Programmiersprache. Sie verwendet eine einheitliche Datenstruktur, Listen oder genauer eine bestimmte Art von Binärbäumen. Programme sind Funktionen über dieser Datenstruktur. Sie werden wie mathematische Gleichungen formuliert und syntaktisch selbst wieder durch Bäume dargestellt. • Prolog (PROgramming in LOGic, vgl. [CM94]) arbeitet regelbasiert: Der Programmierer gibt die Fakten in Form logischer Aussagen und Verknüpfungen in Form bedingter Aussagen ein und das Programm ermittelt durch systematisches Kombinieren und Auswerten selbstständig die Lösung des Problems (vgl. Programmierung von Deduktionssystemen in der künstlichen Intelligenz). Bei diesen Sprachen standen die Wahl der Programmiersprachkonzepte, syntaktische Fragen und Implementierungsprobleme und -techniken im Vordergrund. Lisp ähnelt dem λ-Kalkül (vgl. [BB00]), Prolog hingegen klassischen Logikkalkülen. Der Vorläufer von Prolog war Q-Systems (vgl. [CR93]). METEO, ein Programm zur automatischen Übersetzung von Wetterberichten, das noch heute in Gebrauch ist, wurde in den 70ern auf der Basis von Q-Systems programmiert. Q-Systems besteht aus einer Menge von Regeln und ermöglicht die Darstellung komplexer Baumstrukturen. Es unterstützt bereits eine einfache Unifikation als Teil der Auswertung. Die Regeln können hintereinander angewandt werden, sodass eine modulare, hierarchische Aufteilung möglich ist. Die ersten Programmiersprachen waren auf engere Anwendungsgebiete wie Numerik (ALGOL, Fortran) oder die Verarbeitung betriebswirtschaftlicher Daten (COBOL) ausgerichtet. Sie kannten nur sehr einfache, starr vorgegebene Daten- und Rechenstrukturen wie Zahlen und Felder oder Dateien. Mitte der 60er-Jahre wurde dann gezielt eine universelle Programmiersprache gesucht. Der erste Entwurf kam von IBM – die Sprache PL/1, konzeptuell aber zu umfassend und zu „barock“ – enthielt zu viele Schnörkel und war für damalige Rechner und auch für die Programmierung einfach zu groß und zu umfangreich. In den in der zweiten Hälfte der 60er-Jahre entstehenden Programmiersprachen schlug sich die Notwendigkeit strukturierter Datentypen nieder. Es entstanden Sprachen wie etwa ALGOL W, Simula 67, ALGOL 68, PL/1 und wenig später, bereits stark beeinflusst durch das gewachsene Verständnis für das Gebiet der strukturierten Programmierung, die Sprachen C und Pascal. Mit PL/1 und ALGOL 68 kam das Streben nach immer mächtigeren, allumfassenden Programmiersprachen an seine Grenzen. Das Bestreben, eine allumfassende, für alle Anwendungsgebiete gleichermaßen geeignete Sprache zu schaffen, erzeugte schwer handhabbare Sprachmonster, schwierig zu erlernen, zu nutzen und zu implementieren. Schlankheit, Einfachheit und Handhabbarkeit

1.8 Historische Bemerkungen

27

wurden durch diese Erfahrung als wesentliche Ziele für Programmiersprachen entdeckt. Sehr dominant wurden in den vergangenen 20 Jahren objektorientierte Sprachen. Ausgehend von Simula 67 entstand eine eigene Familie von Sprachen. Ursache für den Erfolg objektorientierter Sprachen ist das Bedürfnis nach geeigneten Strukturierungsmitteln und wiederverwendbaren Bestandteilen. Wichtige Meilensteine bei der Entwicklung objektorientierter Sprachen sind Smalltalk (vgl. [GR83]) und C++ (vgl. [Str91, Eck99, Wei99]). Bei jüngeren Entwicklungen dominieren Java (vgl. [Eck02]) und C# (vgl. [Lib03]). Die Sprache Ada (vgl. [Nag99]) stellte einen späten Versuch dar, eine Standardsprache insbesondere für militärische Anwendungen etwa in Luftund Raumfahrt zu etablieren. Trotz großen Aufwands bei der Entwicklung von Ada konnte sich die Sprache nicht in der Breite durchsetzen. Heute dominieren schlankere Sprachen, vornehmlich objektorientierter Natur wie C++, Java und C# oder einfache, stärker maschinennahe Sprachen wie C. Objektorientierte Sprachen wie Java schöpfen ihre Attraktivität intensiv aus den Möglichkeiten, vorgefertigte Programmteile („Frameworks“) anzubieten und in der Entwicklung in zukünftigen Softwaresystemen wiederzuverwenden. Zunehmendes Interesse findet auch Python.

1.8.2 Programmiermethodik Aufgrund der mit den immer komplizierteren und umfangreicheren Programmieraufgaben und den in der Systemprogrammierung auftretenden Problemen setzte sich Ende der sechziger Jahre allmählich die Erkenntnis durch, dass die Entwicklung von Software doch anspruchsvoller ist als zunächst angenommen und dass gute Programmiersprachen allein das Problem nicht lösen. Forderungen nach einer methodischen Grundlage und wissenschaftlichen Disziplin wurden laut. Jedoch erst Anfang bis Mitte der 60er-Jahre ergab sich – bedingt durch negative Erfahrungen mit Programmierfehlern, mit gravierenden Fehlschlägen bei der Entwicklung größerer Programmsysteme, mangelhafter Portabilität und Wiederverwendbarkeit von Programmen – ein starkes Interesse an Fragen der Semantik und Verifikation. Es entstanden als erste Meilensteine einer Fundierung der Programmierung die „denotationelle Semantik“ (vgl. [GS90]) und die Methode des Programmbeweises durch Zusicherungen (vgl. [Flo67, Hoa72]). Als Beitrag zur Methodik formierte sich das Gebiet der strukturierten Programmierung und Programmverifikation (durch Arbeiten von Floyd, Hoare und Dijkstra, vgl. [DDH72]). Erste richtungsweisende Arbeiten wurden publiziert. Die 1968 auf breiter Front konstatierte Softwarekrise führte auf Begriffe wie • Software Engineering (nach Bauer, vgl. [Bau75]),

28

1 Einführung in die Grundlagen der Softwareentwicklung

• Strukturierte Programmierung (nach Hoare, Dijkstra und Dahl, vgl. [DDH72]), • Schrittweise Verfeinerung, engl. stepwise refinement (nach Wirth, vgl. [Wir72, Wir83]). Die Programmierung wurde nicht länger im Wesentlichen nur als das Problem der Aufschreibung (Codierung) eines Algorithmus in einer Programmiersprache begriffen. Techniken und Notationen zur Erfassung der fundamentalen „charakteristischen“ Eigenschaften der in der Programmentwicklung auftretenden Strukturen wurden entwickelt. Im Zusammenhang mit der Entwicklung größerer Programmsysteme gewann die Modularisierung an Bedeutung (vgl. Parnas [Par72b]). Mitte der siebziger Jahre entstand ausgehend vom Begriff des abstrakten Datentyps (nach Horning, Guttag, Liskov und Zilles, vgl. [GH93] und [LZ74]) das Gebiet der algebraischen Spezifikation von Datenstrukturen.

1.8.3 Strukturierung großer Systeme Aufgrund der zwiespältigen Erfahrungen mit zu umfangreichen Sprachen wie ALGOL 68 und PL/1 wurden schlankere, an die Erfordernisse besser angepasste Sprachen entwickelt. Die Programmiersprache Pascal war zur Zeit ihrer Entstehung der nahezu perfekte Kompromiss zwischen dem Bedarf nach Strukturierung und Abstraktion und dem Ziel, in den Programmen die vorgegebene Hardware über leistungsfähige Übersetzer möglichst effizient nutzen zu können. Mit der Weiterentwicklung der Hardwarestrukturen, ihrer umfassenden Vernetzung und neuen großen Softwareanwendungen wurden aber gewisse Grenzen dieser Programmiersprachen sichtbar. Im Bemühen um immer abstraktere Beschreibungsformen wurden Ansätze wie funktionale Programmierung und Logikprogrammierung entwickelt. Neuere Anwendungsgebiete wie interaktive Mehrbenutzersysteme, Rechnernetze und -verbunde bewirkten ein steigendes Interesse am Gebiet der parallelen Rechnung, an Fragen der Kommunikation wie Kommunikationsprotokollen und an verteilten Systemen (siehe dazu [Bro14]). Die beschriebenen neuen Anforderungen und neuartigen Ansätze schlugen sich in Programmiersprachen wie Modula 2, Ada (strukturierte Programmiersprache mit statischer Typbindung), ML, Miranda, Gofer nieder. Modula entstand aus Pascal und setzte die Strukturierung von Daten und Code weiter fort. Die Basis war das Modul, das eine gekapselte Einheit aus Daten und Code ist, die man als Programmierer im Sinne einer Schnittstelle als Black-Box nutzen konnte, ohne sich um die Implementierung im Detail kümmern zu müssen. Die Sprache Ada setzt ebenfalls auf das Modulkonzept. Besondere Aufmerksamkeit fanden gerade in der Praxis objektorientierte Sprachen, da mit ihnen die Hoffnung auf eine höhere Produktivität durch Kapselung und Wiederverwendung genährt wurde. Typische Vertreter sind

1.8 Historische Bemerkungen

• • • • • • •

29

Simula 67 als Weiterentwicklung von ALGOL 60, Smalltalk, C++, Eiffel, Modula 3, Java, C#.

Der Vorläufer aller objektorientierten Sprachen ist Simula 67 (vgl. [DN65]). Eiffel (vgl. [Mey92]) ist eine universelle, rein objektorientierte Programmiersprache, entworfen von Bertrand Meyer als Alternative zu C++, wobei zunächst nur an den Gebrauch durch die eigene Firma gedacht war. Die Syntax ist beeinflusst von Ada und der ALGOL-Sprachfamilie. Die grafische Darstellung der Entwicklung der wichtigsten Programmiersprachen ist in Abb. 1.3 im Überblick dargestellt. Eine detailliertere, laufend fortgeführte Version dieser Darstellung findet man in [EL17]. Mittlerweile steht die Entwicklung neuer Programmiersprachen nicht mehr so stark im Zentrum der Forschungsaktivitäten. Dies ist einerseits bedauerlich, da nahezu alle genannten und heute eingesetzten Sprachen noch eher von der sequenziellen Verarbeitung auf Zentralrechnern geprägt sind und das Konzept der Verteilung, Nebenläufigkeit und Interaktion sowie das Rechnen in Netzen nicht ausreichend berücksichtigen. Eine der wenigen Ausnahmen in dieser Entwicklung ist die auf verteilte kommunizierende Systeme ausgerichtete funktionale Programmiersprache Erlang [AVWW95]. Derzeit finden in der Forschung zwar – vielleicht auch aufgrund der Defizite der Programmiersprachen – Spezifikations- und Modellierungssprachen mehr Interesse. Die aktuell immer stärker eingesetzte Multi-Core-Hardware verstärkt allerdings das Interesse an Konzepten der Nebenläufigkeit auch in Programmiersprachen wieder.

1.8.4 Methoden zur systematischen Entwicklung von Software In den 80er- und 90er-Jahren traten Themen der Methodik und des systematischen Vorgehens in den Vordergrund. Ein Ziel war die Komplexitätsbegrenzung, ein weiteres die Erhöhung der Qualität der Software. Erreicht werden sollte das auch durch spezifische Modellierungs- und Spezifikationssprachen. Beispiele aus dem industriellen Einsatz sind UML (vgl. [BW99] und [Bur97]) oder SDL (vgl. [BHS91, Bro91]), die abstrakte (d. h. maschinenunabhängige) Techniken zur Beschreibung komplexer Systeme zur Verfügung stellen. Inzwischen konzentriert sich das Interesse der Forschungsarbeiten insbesondere auf Kernfragen der Programmentwicklung wie • Erarbeitung und Repräsentation von Anforderungen, • modulare, modifizierbare, verifizierbare, wiederverwendbare Softwarearchitekturen,

30

1 Einführung in die Grundlagen der Softwareentwicklung

 





 





  

 





"#$ 

 !

%







  





&

%





'

4(

&'! &'  (



  , )) 

 , *! +

-.. 



6 (   

  8

0



$%

5 ! 

*! + /$

7!$ 

! / ,%

6 01

82 1  



3





, 3 3



8 

4.



# , 1 ,%

#' (1 ,%

#.!( ,%

#2 (   ,%

Abb. 1.3 Entwicklungslinien der Programmiersprachen

• verteilte Systeme, • allgemein modellbasierte Entwicklung sowie Werkzeuge (Computer-Aided Software Engineering, CASE), die in vielfältiger Weise den Programmentwurf unterstützen und teilweise automatisieren. Die wissenschaftliche und methodische Konsolidierung der Software- und Programmentwicklung ist jedoch noch längst nicht abgeschlossen. Die schnelle technologische Entwicklung stellt immer neue Herausforderungen und wirft immer wieder neue Fragen auf, deren Beantwortung intensive Forschung erfordert. Nur eine tragfähige Grundlage der Programmentwicklung kann Informatikern das tiefe Verständnis für Strukturen der Programmierung geben,

1.9 Historische Bemerkungen

31

das sie in die Lage versetzt, sich schnell in neue Technologien, Methoden und Konzepte einzuarbeiten und ihre Arbeit mit Systematik und Verständnis zu erledigen.

1.9 Historische Bemerkungen zu methodischen Grundlagen Die Aufgabe, Programme für elektronische Rechenanlagen zu schreiben, wurde anfangs in ihrer Komplexität stark unterschätzt. Zu dominierend standen zu Beginn Fragen der Maschinenrealisierung, des Hardwareentwurfs und der Maschinenarchitektur im Vordergrund. Bald aber zeigte sich, dass viel Rechenzeit unnütz durch inkorrekte Programme vergeudet wurde, dass Fehler in Programmen im Betrieb großen Schaden verursachen können und dass die Anfertigung umfangreicher Programmsysteme sehr schnell die Fähigkeiten der Programmierer zu übersteigen droht und damit sehr fehleranfällig wird. Folgerichtig gab es bald erste Überlegungen, wie man durch methodisch geschicktes Vorgehen die genannten Probleme vermeiden könnte. Schon in den 60er-Jahren wurde von einer Reihe von Informatikern, allen voran Hoare, Dijkstra, Dahl und Wirth empfohlen, diszipliniert und strukturiert bei der Entwicklung von Programmen vorzugehen. Es entstand die Idee der strukturierten Programmierung (vgl. [DDH72]) und schrittweisen Verfeinerung (vgl. [Wir71]). Die Bedeutung der Abstraktion und von Abstraktionsebenen bei dieser Vorgehensweise wurde unter anderem von David Parnas (vgl. [Par72a] und [Par02]) hervorgehoben. Die Erkenntnis, wie eng Fehler in Programmiersprachen mit der Fehleranfälligkeit von bestimmten Konstruktionen in Programmiersprachen in Verbindung stehen, wurde besonders durch Edsger Wybe Dijkstras richtungsweisenden Artikel „Go-to statement considered harmful“ (vgl. [Dij68]) publik gemacht, der darauf hinweist, dass die Zahl der Fehler mit der Zahl der fehleranfälligen Konstrukte in Programmen proportional wächst. Unter dem Stichwort der strukturierten Programmierung entstand ein erster methodisch und wissenschaftlich abgesicherter Ansatz zur systematischen Programmierung auf wissenschaftlicher Basis. Das Ziel, die Entwicklung von Programmen, deren Spezifikation und Verifikation auf saubere wissenschaftliche Grundlage zu stellen, hat in den letzten 50 Jahren eine Fülle von Arbeiten in der Informatik hervorgebracht. Darunter fallen viele letztendlich in der Praxis nicht umsetzbare Ansätze, die aber dennoch Beiträge zu einem tieferen Verständnis für die Systematik, Methodik und die Grundlage der Programmentwicklung erbringen. Letztlich ist, verglichen mit der Situation der 60er-Jahre, viel erreicht worden. Inzwischen gibt es ein solides Fundament für Programmiersprachen und Programmierkonzepte, für die semantische Formalisierung des Verhaltens von Programmen, für deren Spezifikation und Verifikation. Wie viel von diesen wissenschaftlichen Grundlagen sich direkt in die Praxis umsetzen lässt, kann zum augenblicklichen

32

1 Einführung in die Grundlagen der Softwareentwicklung

Zeitpunkt kaum abgeschätzt werden. Es liegt aber klar auf der Hand, dass der anspruchsvolle, auf Systematik ausgerichtete Programmierer und Softwareingenieur die Grundlagen der Programm- und Systementwicklung beherrschen sollte, um ein tieferes Verständnis für die Konzepte, Modelle und Strukturen zu besitzen und so auf die Durchführung seiner Entwicklungsaufgaben vorbereitet zu sein. Viel Aufmerksamkeit haben Modellierungsansätze gefunden, oft in Gestalt von Modellierungssprachen wie der UML. Die Grundlagen der Programmund Systementwicklung sind auch die Grundlagen der Modellierung in der Programmentwicklung. Formale Grundlagen und Modelle sowie ihre Beschreibung – ob durch Diagramme, Tabellen oder Formeln (textuelle Syntax) – werden zu einer soliden, umfassenden Disziplin der Programm- und Systementwicklung zusammenwachsen. Es kommt zusammen, was zusammen gehört.

1.10 Übungsaufgaben Übung 1.1. Diskutieren Sie am Beispiel eines Geldautomaten den Unterschied zwischen Black-Box- und Glass-Box-View. Übung 1.2. Klassifizieren Sie für eine Ihnen bekannte Programmiersprache deren Sprachelemente nach ihrer Einsetzbarkeit für das Programmieren im Großen und das Programmieren im Kleinen. Übung 1.3. Geben Sie eine möglichst abstrakte Sicht auf die Kontodaten einer Bank an und erarbeiten Sie Verfeinerungen. Übung 1.4. Ermitteln Sie möglichst alle Anforderungen an einen Geschwindigkeitsregler im Fahrzeug und überlegen Sie, wie stark dazu Domänenmodelle erforderlich sind. Übung 1.5. Beschreiben Sie das Sortierproblem für eine endliche Menge von Datenelementen über einer Menge mit linearer Ordnung. Übung 1.6. Beschreiben Sie in eigenen Worten die Gemeinsamkeiten und Unterschiede von • Syntax und Semantik, • operationeller Semantik und axiomatischer Semantik, • Verifikation und Validierung, • Nutzungssicht (Black-Box-View) und Realisierungssicht (White-BoxView).

Kapitel 2

Rechen- und Datenstrukturen

Eine der zentralen Aufgaben der Programm- und Softwareentwicklung besteht in der Festlegung und Beschreibung der auftretenden Datenelemente und der darauf zur Verfügung stehenden Operationen. Im Software Engineering Datenmodellierung sprechen wir von Datenmodellierung. Die Zusammenfassung von Familien von Datenmengen (Mengen gleichartiger Daten, „Trägermengen“) und elementaren Operationen darauf nennen wir allgemein Rechenstruktur. Rechenstrukturen beschreiben die Erzeugungs- und Zugriffsstrukturen für die Datenelemente und ihre Logik. Dies legt fest, welche Operationen existieren, um Datenelemente aufzubauen, und welche, um auf ihre Bestandteile zuzugreifen oder Eigenschaften abzufragen. Rechenstrukturen dienen einerseits zur Modellierung der relevanten fachlichen Strukturen eines Anwendungsgebietes, also zur Domänenmodellierung, und andererseits auch zur Beschreibung der Daten und Operationen, die in einem Programm und einem Softwaresystem Verwendung finden. Welche Datenelemente und Operationen in Rechenstrukturen auftreten, bestimmt sich anhand der der Anwendung und der in Programmiersprachen und Rechnern typischerweise auftretenden Strukturen. Es ist eine Aufgabe der Datenmodellierung, diese Rechenstrukturen festzulegen und in Struktur und Wirkung zu beschreiben. Typischerweise erfolgt im Rahmen der Entwicklung der Übergang von einer logischen Modellierung der Datenstrukturen, insbesondere des Anwendungsgebietes, zu Daten- und Rechenstrukturen wie sie in der angepeilten Programmiersprache auf Implementierungsebene zur Verfügung stehen.

2.1 Rechenstrukturen In der Programm- und Softwareentwicklung treten typischerweise anwendungsbedingt umfangreiche Mengen der unterschiedlichsten Datenelemente auf. In Programmen müssen diese Datenstrukturen implementiert und die © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019 M. Broy, Logische und Methodische Grundlagen der Programm- und Systementwicklung, https://doi.org/10.1007/978-3-658-26302-7_2

33

34

Sorte Typ

2 Rechen- und Datenstrukturen

darauf benötigten Operationen realisiert werden. In den frühen Phasen einer Programmentwicklung sind wir jedoch weniger an spezifischen Fragen der Implementierung als vielmehr an den funktionalen Eigenschaften der auftretenden Datenmengen und Funktionen interessiert. Dies führt zu der Erkenntnis, dass zunächst weniger der innere Aufbau und die Darstellung der Datenelemente, als vielmehr die für sie verfügbaren Möglichkeiten des Zugriffs und der Nutzung anhand der vorgesehenen elementaren Operationen von Bedeutung sind. Entsprechend hat es sich durchgesetzt, nicht isoliert Mengen von Daten und ihre Struktur zu betrachten, sondern zunächst betont die dazugehörigen charakteristischen Operationen und Funktionen sowie deren Eigenschaften und Wirkung. Wir sprechen auch von der Logik der Daten- und Rechenstrukturen. In der Mathematik werden Mengen und die Operationen darauf in der Algebra untersucht. Die Informatik verwendet ähnliche Begriffe und Methoden wie die Algebra der Mathematik, zielt jedoch dabei in eine andere Richtung. In der mathematischen Algebra werden typischerweise Strukturen wie Gruppen, Ringe, Körper, Vektorräume mit nur einer vergleichsweise geringen Anzahl von Mengen, Operationen und Axiomen in großer Detailtiefe untersucht. Die Informatik hingegen betrachtet typischerweise umfangreiche Familien von Mengen und Operationen, ist jedoch dabei meist weniger an tiefgründigen Aussagen und Theoremen interessiert. Da in der Entwicklung umfangreicher Programmsysteme sehr unterschiedliche Daten auftreten, hat es sich weitgehend durchgesetzt, gleichartige Daten in Sorten oder Typen1 zusammenzufassen und mit einer Familie solcher Datentypen zu arbeiten. Jeder Datentyp kennzeichnet eine Datenmenge. Diese umfasst die Datenelemente einer bestimmten Sorte. Wir verwenden im Folgenden vorwiegend den Begriff Datensorte. Das Konzept der Typisierung durch Datensorten bietet für Programmiersprachen den Vorteil weitgehender syntaktischer Überprüfungen der syntaktisch korrekten Zusammensetzung programmiersprachlicher Ausdrücke im Hinblick auf die Sorten/Typen (Typüberprüfung, engl. type checking), durch die eine Reihe einfacher Schreib- und Denkfehler in Programmen entdeckt werden kann. Ferner werden Lesbarkeit und Verständlichkeit durch Sorteninformation in Programmen deutlich verbessert. Für die Begriffsbildung ähnlich den Taxonomien in der Logik dienen Sorten zur Strukturierung der Begriffe eines Anwendungsgebietes.

1 Wir verwenden hier die Bezeichnungen Sorte („Datensorte“) und Typ („Datentyp“) synonym (bedeutungsgleich). Allerdings gibt es in der Literatur vielerlei unterschiedliche Verwendungen des Begriffs Typ. Wir sprechen deshalb im Weiteren bevorzugt von (Daten-) Sorten statt von (Daten-)Typen.

2.1 Rechenstrukturen

35

2.1.1 Die Bedeutung des Konzepts der Rechenstruktur In diesem Abschnitt führen wir die wichtigsten Begriffe für die Datenmodellierung ein. Eine wesentliche Aufgabe bei der Erstellung eines Datenmodells liegt in der Festlegung der für die Anwendung relevanten Datensorten und deren Beziehungen untereinander. Beispiel 2.1 (Datenmodell für Bankanwendung). Für eine Softwareanwendung für eine Bank ist es erforderlich, die in einem Domänenmodell relevanten Begriffe und Daten wie • Konto, • Kunde, • Buchung, • Überweisung • ... festzulegen und zu spezifizieren. Aus der Sicht des Softwareentwicklers sind alle diese Begriffe Namen für Mengen von Daten, die unter einem einheitlichen Gesichtspunkt zusammengefasst werden und gewisse Informationen gleicher Sorte repräsentieren. Deshalb sprechen wir bei diesen Namen von Sorten.  Wie das Beispiel illustriert, ist eine Sorte ein Name für eine Menge von Datenelementen gleicher Art. Im Beispiel ist es sinnvoll, zwischen Konto als Begriff (Sorte der Datenelemente, die ein Konto repräsentieren) und den einzelnen Exemplaren dieser Sorte (bestimmten Elementen, wir sprechen von „Instanzen“, der Sorte Konto) zu unterscheiden. Neben den Sorten betrachten wir Funktionen (wir sprechen auch von Operationen) auf den Datenelementen. Durch diese lassen sich auch Beziehungen („Relationen“) zwischen Datenelementen und das Ergebnis von Berechnungen ausdrücken. Beispielsweise können wir in einer Bank jedes Konto einem Kunden zuordnen. Dies können wir durch eine Funktion modellieren, die wir in unserem Fall inhaber nennen. Mathematisch ist inhaber eine Funktion, die jedem Datenelement der Sorte Konto ein Datenelement der Sorte Kunde zuordnet, das den Inhaber des Kontos repräsentiert. Neben der Funktion inhaber gibt es noch weitere Funktionen, die für das Datenmodell einer Bank von Interesse sind. Fassen wir die Datenmengen zu den Sorten und die Funktionen zusammen, so erhalten wir mathematisch gesehen eine Algebra. Wir sprechen auch unter Betonung der Verwendung dieser Algebra als Basis für Programme zur Ausführung in einer Rechenanlage von einer Rechenstruktur. Definition 2.1 (Rechenstruktur, informell). Eine Familie von Grundmengen Rechenstruktur (Trägermengen) von (Daten-)Elementen und eine Familie darauf operierender Funktionen nennen wir eine mathematische Struktur, Algebra oder auch Re- Algebra chenstruktur. 

36

Signatur

2 Rechen- und Datenstrukturen

Die Namen der Trägermengen einer Rechenstruktur nennen wir Sorten, die Namen für die Datenelemente und Funktionen nennen wir Konstantenbezeichnungen (oder kurz Konstante) und auch Funktionssymbole. Zusammen mit den Angaben der Sorten der Konstantenbezeichnungen und Funktionalitäten der Funktionssymbole bildet diese Familie von Namen eine Signatur. Wir sprechen von der Signatur der Rechenstruktur. Das Konzept der Rechenstruktur bietet, wie bereits in Kap. 1 motiviert, folgende Vorteile für die Programmentwicklung: (1) Zusammenfassung und Beschreibung von Daten und Operationen in begrifflich zusammenhängende Einheiten (vgl. Module, Abstrakte Datentypen, Abschnitte 1.3 und 1.5.3); (2) in sich geschlossene, eigenständig beschreibbare, entwickelbare, austauschbare und wiederverwendbare Spezifikations- und Programmeinheiten (vgl. Abschnitt 1.3, Def. 1.1); (3) Unabhängigkeit von Anwendung und Realisierung durch klar festgelegte syntaktische und semantische „Schnittstellen“, die die Zugriffslogik der Rechenstrukturen beschreiben (vgl. Abschnitte 1.5.1 und 1.5.2). Die Idee der Rechenstruktur findet sich mehr oder weniger deutlich in allen modernen Programmier- und Modellierungssprachen. So ist das Klassenkonzept in objektorientierten Sprachen eine spezifische Ausprägung dieser Idee. Allerdings ist die Idee der Rechenstruktur dort mit einer Reihe weiterer Konzepte verknüpft, worauf wir insbesondere in Kap. 8 ausführlich zurückkommen.

2.1.2 Nutzungs- und Realisierungssicht für Rechenstrukturen Rechenstrukturen bilden die Grundlage für die Formulierung von Algorithmen und Rechenvorschriften in der Programmierung. Programme und Algorithmen arbeiten über gegebenen Rechenstrukturen. Wir unterscheiden zwei Sichten auf eine Rechenstruktur: • die Nutzungssicht (auch Black-Box-View, Zugriffssicht, Schnittstellensicht), • die Realisierungssicht (auch Glass-Box-View, Implementierungssicht). Bei der Nutzungssicht interessieren wir uns nicht für den konkreten inneren Aufbau der Datenelemente in den Trägermengen der Sorten einer Rechenstruktur und ihre innere Struktur und konkrete Implementierung, sondern für das Zusammenspiel der Funktionen und für die Wirkung einer Folge von Funktionsaufrufen. Diese Sicht wird durch den Begriff der Rechenstrukturen unterstrichen. Die Klasse aller Rechenstrukturen einer festgelegten Zugriffssicht nennen wir auch eine abstrakte Rechenstruktur. In der Realisierungssicht interessieren wir uns zusätzlich für den inneren Aufbau der Elemente der Trägermengen, für ihre Struktur. Dies betrifft insbesondere auch deren Darstellung in der Maschine.

2.1 Rechenstrukturen

37

Unter der Realisierungssicht eines Datenelements verstehen wir seinen inneren Aufbau und somit seine Datenstruktur. Die Datenstruktur bestimmt die kon- Datenstruktur krete Struktur zur Darstellung der Daten beispielsweise in einer Programmiersprache und auch den Platzbedarf eines Datenelements im Speicher und den Berechnungsaufwand beim Zugriff auf seine Bestandteile. Zu Beginn einer Programmentwicklung sind naturgemäß weniger die konkrete Realisierung und der innere Aufbau der Datenstrukturen als die Zugriffs- und Rechenstrukturen und damit die Logik der Zugriffsoperationen von Interesse. In manchen Fällen verwenden wir eine Rechenstruktur mit einer konkreten Trägermenge bestehend aus Elementen einer bestimmten Datenstruktur (oder gar eine Menge von Rechenstrukturen) stellvertretend zur Festlegung und Beschreibung einer Zugriffssicht. Beispiel 2.2 (Nutzungs- und Realisierungssicht auf einen Fahrkartenautomaten). Als ein einführendes Beispiel können wir etwa einen Fahrkartenautomaten betrachten. Für den Fahrgast (Nutzer) ist völlig unerheblich, welche innere Struktur der Automat hat. Er interessiert sich aber dafür, wie der Automat generell auf seine Eingaben reagiert und was er als Nutzer tun muss, um korrekte Informationen und ein gewünschtes Ticket zu bekommen. Diese Sichtweise führt auf die Nutzungssicht auf den Automaten. Wenn ein Wartungstechniker hingegen die Aufgabe hat, die Automaten in Stand zu setzen oder mit Wechselgeld, Papier und Druckerfarbe zu versorgen, muss er in Teilen die innere Struktur und die Realisierungsaspekte kennen. In diesem Fall benötigt er eine Realisierungssicht.  Diese Idee der Sichten auf ein System lässt sich einfach auf Rechenstrukturen übertragen. Beispiel 2.3 (Zugriffs- und Realisierungssicht für den indexgesteuerten Zugriff). Wir betrachten informell eine Rechenstruktur mit den Sorten Data Index Array

Datenelemente Schlüssel für Zugriff Ansammlung von Daten, auf die über Schlüssel zugegriffen wird

der Konstante earray : Array

die ein leeres Feld darstellt, und den folgenden Funktionen für den Zugriff und Aufbau: get : Array, Index → Data put : Array, Index, Data → Array

In der Zugriffssicht auf die Elemente der Sorte Array interessieren wir uns ausschließlich für die beobachtbaren Ergebnisse von Funktionsaufrufen oder Folgen von Funktionsaufrufen. Beim indexgesteuerten Zugriff erwarten wir in der Zugriffssicht, dass ein Aufruf get(put(g, i, d), i) mit einem beliebigen

38

2 Rechen- und Datenstrukturen

Datenelement d, einem Index i und Datenbestand g als Ergebnis wieder das Datenelement d liefert: get(put(g, i, d), i) = d

Diese Gleichung beschreibt beispielhaft das Zusammenspiel der Funktionen get und put. Wie das Datenelement g genau dargestellt und aufgebaut ist, ist dabei ohne Belang. Ein weiteres interessantes Gesetz für den indexgesteuerten Zugriff ist durch folgende bedingte Gleichung gegeben: get(put(g, i, d), j) = get(g, j) ⇐ i  j

Die Gleichung drückt aus, dass beim Zugriff auf ein Datenelement mit Schlüssel j ein davor stattgefundenes Überschreiben mit einem anderen Schlüssel i keinen Einfluss hat. Die beiden angegebenen Gleichungen beschreiben bereits wesentliche Eigenschaften der Rechenstruktur. Es gibt eine Vielzahl unterschiedlicher Datenstrukturen (Hash-Tabellen, AVL-Bäume, falls die Menge Index linear geordnet werden kann, B-Bäume, Felder, verkettete Listen etc.), die den indexgesteuerten Zugriff in unterschiedlicher Implementierung realisieren, wobei in den verschiedenen Implementierungen ganz unterschiedliche Berechnungsaufwände für die Funktionen get und put auftreten können.  In der Programmierung hat es sich als fruchtbar erwiesen, bewusst zwischen abstrakten Rechenstrukturen (auch abstrakte Datentypen genannt) und konkreten Datenstrukturen zu unterscheiden. Eine abstrakte Rechenstruktur bezeichnet die Klasse aller konkreten Rechenstrukturen, die eine gegebene Zugriffssicht realisieren. Dazu werden die logischen Eigenschaften beschrieben, die diese Rechenstrukturen gemeinsam haben. Bei einer abstrakten Rechenstruktur handelt es sich also genau genommen um eine Klasse (im Sinne der allgemeinen Mengenlehre, siehe [Obe94]) von Rechenstrukturen und um eine Abstraktion der Sicht auf die Trägermengen und auf die für sie verfügbaren Funktionen. Abstrakte Rechenstrukturen stellen somit die Zugriffssicht (auch BlackBox-Sicht oder Nutzungssicht) für eine Sorte oder eine Familie von Sorten dar. Hierfür ist entscheidend, welche grundlegenden Operationen für die Datenelemente der Sorten verfügbar sind. Dazu werden in einer Signatur die verfügbaren Funktionen und ihre Funktionalität angegeben. Sprechen wir im Zusammenhang mit den Elementen einer Sorte M von Datenstrukturen, so zielen wir damit auf den inneren Aufbau der Datenelemente der Sorte M. Die Datenstruktur kennzeichnet, auf welche Weise die Datenelemente aufgebaut und beispielsweise konkret im Speicher einer Rechenanlage implementiert sind. Der Begriff der Datenstruktur bezieht sich somit in der Regel auf die Implementierungssicht. Die Zugriffssicht legt die Schnittstelle für eine Sorte oder Rechenstruktur fest. Diese Sicht ist ausreichend für die funktional korrekte Verwendung der

2.1 Rechenstrukturen

39

Sorten, ihrer Elemente und der für sie verfügbaren Operationen (Nutzungssicht), sie dient aber auch als Vorgabe (Spezifikation) für die Implementierung. Die Spezifikation der Zugriffssicht bildet den „Vertrag“ zwischen Nutzer und Implementierer. Zusätzlich sind für Effizienzüberlegungen noch Angaben über die Zeit- und Speicherkomplexität der auftretenden Operationen erforderlich. Die Realisierungssicht ist die Sicht des Implementierers einer Sorte oder eines Datenmodells und richtet sich auf Implementierungsdetails. Diese umfassen den inneren Aufbau von Datenstrukturen und ihre konkrete Darstellung mit den implementierungstechnisch gegebenen Mitteln (wie etwa in einer Rechenanlage). Die Realisierungssicht schließt – zumindest implizit – eine Zugriffssicht ein. Die Zugriffssicht stellt eine Abstraktion der Realisierungssicht dar. Abstraktion heißt hier, dass für die Nutzung irrelevante Implementierungsdetails nicht betrachtet werden. Es existieren in der Regel viele Datenstrukturen für die Implementierung der gleichen Zugriffssicht, ebenso wie es viele Algorithmen für die Lösung einer Problemstellung gibt. Bei der Verwendung einer Rechenstruktur braucht der Nutzer nur die Zugriffssicht zu kennen, die eigentliche Realisierung durch eine Datenstruktur kann ihm verborgen bleiben (engl. information hiding information hiding). Dies hat entscheidende Vorteile: • Der Nutzer einer Rechenstruktur braucht die oftmals komplexen, schwer zu durchschauenden Implementierungsdetails der realisierenden Datenstruktur nicht zu kennen, sondern nur die abstrakte Zugriffssicht und die damit verbundenen Eigenschaften und gegebenenfalls Angaben über den benötigten Berechnungsaufwand. Die Zugriffssicht dient auch zur Dokumentation für die Nutzung im Sinne einer Gebrauchsanweisung. • Die Implementierung kann parallel zur Programmierung der Nutzung der Rechenstruktur vorgenommen werden. Als Verständigungsbasis („Schnittstelle“) und Verifikationsgrundlage dient die Zugriffssicht. • Die Implementierung einer Rechenstruktur kann geändert werden, ohne dass der Nutzer dies in der Verifikation (abgesehen von Fragen der Effizienz) zur Kenntnis nehmen muss, solange nur die Zugriffssicht erhalten bleibt. Um wie angedeutet vorgehen zu können, ist eine handhabbare Beschreibung („Spezifikation“) der Nutzungssicht unverzichtbar. Wir sprechen von einer Schnittstellenspezifikation. Über einer vorgegebenen Schnittstelle, die gewisse Sorten und Operationen als Rechenstruktur (nennen wir sie P) bereit stellt, können weitere Sorten und Operationen formuliert werden, die dann selbst wieder zu einer Schnittstelle (nennen wir sie A) mit einer bestimmten Nutzungssicht zusammengefasst werden können. Wir sprechen dann von der Realisierung der Rechenstruktur A über der Rechenstruktur P. Somit entsteht die Voraussetzung für eine arbeitsteilige, modulare Arbeitsweise. Rechenstrukturen können dann ähnlich einer Schichtenarchitektur übereinander geschichtet aufgebaut werden und unabhängig voneinander („modular“), nur unter Einhaltung der Schnittstellenvorgaben, realisiert werden.

40

2 Rechen- und Datenstrukturen

Wir beschäftigen uns im Weiteren vorwiegend mit der Nutzungssicht auf Datenstrukturen und der damit verbundenen Zugriffslogik. Die Realisierungssicht hingegen ist das Hauptthema des Gebietes der Datenstrukturen und effizienter Algorithmen (vgl. [Wir83] und [GT02]). Hier steht die effiziente Realisierung einer gegebenen Nutzungssicht im Rahmen einer bestimmten Programmiersprache und Hardware im Vordergrund.

2.1.3 Signaturen Sorten sind Bezeichnungen („Namen“) für Datenmengen, die allgemein Trägermengen genannt werden. Es treten in der Regel sehr unterschiedliche Sorten in einer Programmentwicklung auf. Am einfachsten und elementarsten sind die Sorten vorgegebener wohlvertrauter Trägermengen wie etwa Zahlen, Wahrheitswerte und Zeichen (engl. character) eines Alphabetes, aber auch anwendungsspezifischer Trägermengen wie Kontonummern, Farben oder Indexe. Wir sprechen von Grundsorten. Welche Sorten für eine Entwicklung als Grundsorten gewählt werden, liegt natürlich im Ermessen des Entwicklers eines Datenmodells. Grundsorten sind wie alle Sorten Bezeichnungen für Datenmengen. Sie können für beliebige Trägermengen stehen. Wichtige Beispiele für elementare Grundsorten sind Bool, die Sorte der Wahrheitswerte, oder Nat, die Sorte der natürlichen Zahlen. Mithilfe gegebener Sorten können wir verschiedene Formen zusammengesetzter Sorten bilden. So betrachten wir später beispielsweise Tupelsorten (engl. records) und Vereinigungssorten (engl. unions). Von besonderer Bedeutung sind Funktionssorten. Über der gegebenen Menge S von Grundsorten definieren wir Funktionssorten wie folgt: Seien M1, . . . , Mn+1 beliebige Sorten, beispielsweise Grundsorten aus der Menge S. Dann bezeichnet der Ausdruck M1, . . . , Mn → Mn+1 eine Funktionssorte. Diese Funktionssorte heißt auch Sorte erster Stufe, wenn alle Sorten Mi Grundsorten, also selbst keine Funktionssorten, sind. Die Menge der Grundsorten heißen entsprechend Sorten nullter Stufe. Die Menge der Funktionssorten erster Stufe über der Menge der Grundsorten S bezeichnen wir mit S → . Lassen wir für die Bildung von Funktionssorten selbst wieder Funktionssorten zu, so erhalten wir Funktionssorten höherer Stufe. Darauf kommen wir später zurück. Signatur

Definition 2.2 (Signatur, erster Stufe). Eine Signatur (erster Stufe) ist ein Paar (S, F)

2.1 Rechenstrukturen

41

bestehend aus • einer Menge S von Sorten, • einer Menge F von Konstanten2, darunter Bezeichnungen für Elemente von Grundsorten und Bezeichnungen für Funktionen (Elemente von Sorten erster Stufe), • einer (zwecks Übersichtlichkeit im obigen Paar nicht aufgezeigten, aber streng genommen dazugehörigen) Abbildung sort : F → S ∪ S → , die den Bezeichnungen in F Sorten zuordnet. Für jede Bezeichnung f ∈ F bezeichnet sort( f ) die Sorte der Bezeichnung f . Wir schreiben auch f: M für die Aussage, dass die Bezeichnung f die Sorte M hat. Ist M Funktionssorte, so heißt M auch die Funktionalität von f .  Signaturen definieren die Bezeichnungen, die in einer Rechenstruktur auftreten, und deren syntaktische Natur (bezeichnen sie eine Sorte oder eine Konstante zu einer Sorte). Signaturen repräsentieren die syntaktische Schnittstelle von Rechenstrukturen. Eine Signatur beschreibt formal lediglich eine Menge von Sorten, Konstanten und Funktionsbezeichnungen sowie ihre Sorten. Beispiel 2.4 (Boolesche Signatur). Die boolesche Signatur ist gegeben durch die einzige Sorte Bool sowie folgende Konstanten und Funktionssymbole false, true : Bool and, or : Bool, Bool → Bool not : Bool → Bool



Beispiel 2.5 (Signatur des indexgesteuerten Zugriffs). Die formale Signatur der Nutzungssicht des indexgesteuerten Zugriffs aus Bsp. 2.3 ist   {Data, Index, Array }, {earray, get, put} mit Sorten Data, Index, Array und den Bezeichnungen earray : Array get : Array, Index → Data put : Array, Index, Data → Array



Beispiel 2.6 (Signatur der Prioritätsschlangen). Gegeben sei eine Sorte Item von Elementen, die in Warteschlangen mit Prioritäten abgelegt werden, und ein zweistelliges Operationssymbol ≤, das wir zum Vergleichen von Prioritäten dieser Elemente nutzen. Der Vergleich der Prioritäten von Elementen x, y : Item geschieht mit (x ≤ y) = true bzw. (x ≤ y)  true, wobei true ein nullstelliges 2 Eine Konstante ist eine Bezeichnung für ein Element einer Sorte.

42

2 Rechen- und Datenstrukturen

Symbol der Sorte Bool ist, das den logischen Wert „wahr“ bezeichnet. Dabei nutzen wir für den Bezeichner ≤, um die Lesbarkeit zu verbessern, die sogenannte Infix-Schreibweise: statt ≤(x, y) schreiben wir x ≤ y. Prioritätsschlangen über Item werden durch eine Rechenstruktur mit folgender Signatur dargestellt. Die Signatur enthält die Sorte Item der in der Prioritätsschlange abgelegten Werte, die Sorte Pq der Prioritätsschlangen und die Sorte Bool der Wahrheitswerte sowie folgende Konstanten und Funktionsbezeichnungen: true :

logischer Wert „wahr“ Item,Item → Bool Vergleichsoperation ≤: emptyq : Pq leere Prioritätsschlange enq : Pq, Item → Pq Einfügen eines Elements in eine Prioritätsschlange next : Pq → Item nächstes Element höchster Priorität in einer Prioritätsschlange deq : Pq → Pq Prioritätsschlange, die entsteht, wenn das Element höchster Priorität gelöscht wird Bool

Selbstverständlich haben wir die Freiheit, in die Signatur andere übliche boolesche Bezeichner aus Bsp. 2.4 aufzunehmen, worauf jedoch aus Gründen der Übersichtlichkeit in diesem Beispiel verzichtet wird.  Beispiel 2.7 (Allgemeine Bäume und Wälder). Die Signatur der Rechenstruktur der allgemeinen Bäume und Wälder ist gegeben durch folgende Grundsorten (die Grundsorte Item sei gegeben): Tree, Forest

und die Konstanten und Funktionssymbole: emptyforest : emptytree : buildtree : makeforest : concforest : root : lefttree, righttree : firsttree : restforest :

Forest Tree Item, Tree, Tree → Tree Tree → Forest Forest, Forest → Forest Tree → Item Tree → Tree Forest → Tree Forest → Forest



Zwar gilt in der Regel, dass formal durch die Bezeichnungen in keiner Weise die Eigenschaften der Funktionen festgelegt sind; allerdings ist die Wahl einer Signatur mit ihren Sorten und Funktionsbezeichnungen ein wichtiger Schritt auf dem Weg zu einem Programm oder einer Spezifikation. Werden die Bezeichnungen geschickt gewählt (Stichwort „sprechende Bezeichnungen“), so wird oft schon dadurch weitgehend intuitiv klar, welche Funktionalität

2.1 Rechenstrukturen

43

gemeint ist. Eine Signatur oder die Wahl der Bezeichner legt jedoch formal in keiner Weise die Bedeutung, die Wirkung, den Wertverlauf der mit den Funktionssymbolen in einer Rechenstruktur verbundenen Funktionen oder Einzelheiten zu den Elementen der Sorten fest. Will man also Missverständnisse vermeiden, so muss man Techniken einsetzen, die auch das Verhalten der Funktionen eindeutig beschreiben. Wir kommen darauf ausführlich zurück. Anmerkung 2.1 (Zusammenhang Signatur/Ontologie). Eine Ontologie ist ein Begriffssystem. Sie besteht aus einer Menge von Begriffen und benannten Beziehungen zwischen den Begriffen. Ontologien sind von zentraler Bedeutung für die Formalisierung natürlicher Sprachen. Signaturen ähneln Ontologien und lassen sich wie diese zur Begriffsbildung in der Softwareentwicklung einsetzen. Dabei entspricht jede Sorte einem Begriff und die Funktionen den Beziehungen zwischen den Begriffen.  Allein die Sammlung aller relevanter Begriffe unter der Wahl geeigneter Bezeichnungen und ihrer Beziehungen ist ein wesentlicher Schritt in der Datenmodellierung in der Softwareentwicklung.

2.1.4 Σ-Algebren Gegeben sei eine Signatur. Durch die Zuordnung von Trägermengen zu Sorten sowie von Werten und Funktionen zu den Bezeichnungen entstehen Algebren einer Signatur. Jede Algebra ergibt eine konkrete Festlegung für die Sorten und Symbole ihrer Signatur. Im Folgenden ist eine partielle Funktion („partielle Abbildung“) von einer Menge X in eine Menge Y ein Element der Menge X  Y , wobei X  Y

=

partielle Funktion

{ f ⊆ X × Y | ∀x, y1, y2 : (x, y1 ), (x, y2 ) ∈ f ⇒ y1 = y2 } .

Eine partielle Funktion heißt total, wenn für jedes x ∈ X ein y ∈ Y existiert mit (x, y) ∈ f . Gilt (x, y) ∈ f und f ∈ X  Y , dann bezeichnet f (x) das Element y. Man beachte, dass f (x) nicht definiert ist („keinen definierten Wert besitzt“), falls für kein y ∈ Y die Aussage (x, y) ∈ f zutrifft. Wie wir später sehen werden, führt das zu Komplikationen beim Zusammensetzen von Funktionsanwendungen zu Termen. Nach diesen Vorbereitungen präzisieren wir Def. 2.1:

totale Funktion

Definition 2.3 (Σ-Algebra3, Σ-Rechenstruktur). Sei Σ = (S, F) eine Si- Σ-Algebra gnatur. Eine partielle Σ-Algebra A (auch partielle Σ-Rechenstruktur) ist ein Paar   A (M ) M ∈S , ( f A) f ∈F 3 Man beachte, dass der Begriff der σ-Algebra in der Maßtheorie Verwendung findet und nicht mit der hier eingeführten Σ-Algebra zu verwechseln ist.

44

2 Rechen- und Datenstrukturen

bestehend aus • einer Familie von Trägermengen M A für Sorten M ∈ S und • einer Familie von Elementen und partiellen Funktionen f A für Bezeichner f ∈ F, wobei für jede Bezeichnung f ∈ F Folgendes gilt:  A • wenn sort( f ) ∈ S, dann f A ∈ sort( f ) ; • wenn sort( f ) = M1, . . . , Mn → Mn+1 , dann f A : M1A × · · · × MnA  A . Mn+1  Besitzt also f ∈ F eine Grundsorte M, so ist f A ein Wert aus der Trägermenge M A. Besitzt f ∈ F eine funktionale Sorte M1, . . . , Mn → Mn+1 , so ist f A eine A . partielle n-stellige Funktion von M1A × · · · × MnA nach Mn+1 Ist die Menge S der Grundsorten einelementig, so heißt A homogen, sonst heterogen. Sind alle Funktionen f A für f ∈ F total, so heißt A totale Σ-Algebra. Beispiel 2.8 (Zweiwertige boolesche Algebra). Zur Signatur Σ aus Bsp. 2.4 konstruieren wir die folgende totale Σ-Algebra SBA (Abkürzung für engl. simplest Boolean algebra): BoolSBA trueSBA falseSBA andSBA orSBA notSBA

= B  {O, L} =L =O =∧ =∨ =¬

zwei unterschiedliche Elemente („Literale“) 4 logisches Ja logisches Nein logische Konjunktion logische Disjunktion logische Negation

Die Wahl der Trägermenge B = {O, L} ist in gewisser Weise willkürlich. Grundsätzlich können wir eine beliebige zweiwertige Menge als Trägermenge wählen oder, noch allgemeiner, jede Menge, die mindestens zwei unterschiedliche Elemente enthält. Wir wählen die konkrete Trägermenge {O, L}, da sich dann die typischen Operationen der Aussagenlogik einfach in Tabellen erfassen lassen. Wir erhalten damit die üblichen Definitionen der logischen Funktionen: x O O L L

y x ∧ y x ∨ y ¬x O L O O L L O O O L O L L L



Beispiel 2.9 (Indexgesteuerter Zugriff – die Rechenstruktur G). Gegeben sei eine Zahl n ∈ N. Wir definieren eine partielle Rechenstruktur G zur Signatur aus Bsp. 2.5: 4 L und O sind im Hardwareumfeld anzutreffen. Grundsätzlich können wir natürlich eine beliebige zweielementige Menge als Trägermenge zu Bool wählen, vorausgesetzt, dass mögliche Konflikte mit eventuell vorhandenen anderen Bedeutungen der zwei Elemente vermieden werden. In der Literatur finden sich auch {F, T}, {N, Y}, {F, W}, {⊥, }.

2.1 Rechenstrukturen

45

IndexG = {k ∈ N : 1 ≤ k ≤ n} DataG = {a, . . . , z}∗



ArrayG = IndexG × DataG

∗

Hierbei bezeichnen wir für eine Menge Z mit Z ∗ die Menge der endlichen Sequenzen über dem Alphabet Z (für eine umfassende Erklärung zu Sequenzen und der verwendeten Notation siehe beispielsweise [Bro98]). Als Interpretation des Bezeichners für das leere Feld wählen wir die leere Sequenz (die wir mit ε bezeichnen): earrayG = ε

Wir spezifizieren die partielle Funktion getG und die totale Funktion putG wie folgt:   getG ε, i ist nicht definiert   getG (i, d) ◦ w, i = d     falls i  j getG ( j, d) ◦ w, i = getG w, i  G ◦ put w, i, d = (i, d) w Hierbei bezeichnet x die Sequenz bestehend aus einem einzigen Element x  und ◦ die Konkatenation von Sequenzen. Im obigen Beispiel wird den Elementen der Signatur eine Interpretation („Bedeutung“) in Form einer Rechenstruktur gegeben. In Kürze geben wir eine Rechenstruktur der Prioritätsschlangen an. Zu diesem Zweck führen wir einige grundlegende Begriffe aus der Ordnungstheorie ein: Definition 2.4 (Quasiordnung). Eine Quasiordnung (engl. preorder) auf Quasiordnung einer Menge X ist eine binäre Relation auf X, die reflexiv auf X und transitiv preorder ist. Eine Quasiordnung auf X heißt total, wenn alle Elemente vergleichbar sind, d. h., wenn für alle x, y ∈ X mindestens eines der Paare (x, y) und (y, x) zur Quasiordnung gehört.  Partielle Ordnungen sind antisymmetrische Quasiordnungen (vgl. Abschnitt 3.1.5). Beispiel 2.10 (Eine Σ-Algebra der Prioritätsschlangen). Wir definieren nun eine Rechenstruktur P von Prioritätsschlangen zur Signatur aus Bsp. 2.6. Als Trägermenge zu Bool wählen wir B wie in Bsp. 2.8. Als Trägermenge zu Item wählen wir die Menge der Konten einer Bank zuzüglich eines „frischen“ Extraelements ⊥, das für kein Konto steht: K = Menge der Konten einer Bank Item P = K ∪ {⊥} ,

wobei ⊥  K

46

2 Rechen- und Datenstrukturen

Statt Konten einer Bank, die wir nach Kontostand vergleichen werden, hätten wir andere Objekte der realen Welt wie Äpfel, Birnen, Studenten einer Vorlesung etc. wählen können und als Vergleichsoperation würden wir dann etwa Vergleich nach Gewicht wählen können; diese Einzelheiten sind für die folgende Darstellung irrelevant.5 Zugunsten einer angemessenen Abstraktion ignorieren wir bewusst die innere Struktur der Konten. Für x, y ∈ Item P erlauben wir uns jedoch mit x = y danach zu fragen, ob x und y ein und dasselbe Element von Item P ist. Die Operation ≤ P vergleiche die Konten nach ihrem Stand: Es gelte (x ≤ P y) = true entweder wenn x, y ∈ K und x einen niedrigeren Kontostand als das Konto y aufweist (wir betrachten diese Relation als vom Nutzer vorgegeben) oder wenn x = ⊥ = y gilt. Formal setzen wir jedoch lediglich voraus, dass die Relation {(x, y) ∈ K 2 | (x ≤ P y) = true} eine totale Quasiordnung (siehe Def. 2.4) ist. Wir schreiben < P als Abkürzung für die irreflexive Version des def

Vergleichs, also (x < P y) = true ⇔ ((x ≤ P y) = true ∧ x  y) für x, y ∈ Item P . Das Symbol ⊥ steht für „undefiniert“; es ist mit keinem Konto über die Ordnung ≤ (oder 0 und m > 0: Dann gilt ϕ(n) = L = ϕ(m), n ∗ m > 0 und daher ϕ(n ∗ m) = L = (L ∧ L) = (ϕ(n) ∧ ϕ(m)).  Das Beispiel zeigt, dass ein Homomorphismus im Allgemeinen den Übergang auf eine Abstraktion definiert. Unterschiedliche Elemente können durch den Homomorphismus auf ein und dasselbe Element abgebildet werden. Homomorphismen können durch Komposition verkettet werden: Lemma 2.1. Sei Σ eine Signatur, A, B und C partielle Σ-Rechenstrukturen, ϕ ein Homomorphismus von A nach B und ψ ein Homomorphismus von B nach C. Dann ist (ϕ M ◦ ψ M ) M ∈S , wobei „◦“ die relationale Komposition der Abbildungen ist (vgl. Seite 239), ein Homomorphismus von A nach C. Sei  c ∈ F einer Grundsorte M ∈ S. Dann ϕ M ◦ ψ M (c A) = Beweis.  ψ M ϕ M (c A) = [da ϕ ein Homomorphismus ist] ψ M (c B ) = [da ψ ein Homomorphismus ist] cC . 12 Man beachte, dass ϕ in der Tat eine Abstraktion darstellt, da Zahlen auf Wahrheitswerte abgebildet werden, die lediglich aussagen, ob die Zahl positiv ist.

56

2 Rechen- und Datenstrukturen

Sei nun f ∈ F ein Funktionsbezeichner der Sorte M1, . . . , Mn → Mn+1 für ein n ≥ 1. Sei (a1, . . . , an ) ∈ dom f A beliebig. Da ϕ ein Homomorphismus ist, gilt:   (2.1) ϕ M1 (a1 ), . . . , ϕ Mn (an ) ∈ dom f B  A    und ϕ Mn+1 f (a1, . . . , an ) = f B ϕ M1 (a1 ), . . . , ϕ Mn (an ) . Daher gilt     ψ Mn+1 ϕ Mn+1 f A(a1, . . . , an ) = ψ Mn+1 f B ϕ M1 (a1 ), . . . , ϕ Mn (an ) . (2.2) Aus (2.1) und der Homomorphismus-Eigenschaft von ψ folgt     ψ M1 ϕ M1 (a1 ) , . . . , ψ Mn ϕ Mn (an ) ∈ dom f C und

      ψMn+1 f B ϕM1 (a1 ), . . . , ϕMn (an ) = f C ψM1 ϕM1 (a1 ) , . . . , ψMn ϕMn (an ) .

    Mit (2.2) ergibt sich ψ Mn+1 ϕ Mn+1 f A(a1, . . . , an ) = f C ψ M1 ϕ M1 (a1 ) , . . . ,      ψ Mn ϕ Mn (an ) . Daher ϕMn+1 ◦ψMn+1 f A(a1, . . . , an ) = f C ϕM1 ◦ψM1(a1 ), . . . ,  ϕMn ◦ψMn(an ) . QED Definition 2.10 (Injektiv, surjektiv, bijektiv). Eine Abbildung f : X → Y heißt • injektiv, wenn es zu jedem y ∈ Y höchstens ein x ∈ X mit f (x) = y gibt; • surjektiv, wenn es zu jedem y ∈ Y mindestens ein x ∈ X mit f (x) = y gibt; • bijektiv, wenn es zu jedem y ∈ Y genau ein x ∈ X mit f (x) = y gibt.  Definition 2.11 (Endomorphismus, Monomorphismus, Epimorphismus, Automorphismus, Isomorphismus, Isomorphie). Sei (S, F) eine Signatur. Ein Homomorphismus ϕ von einer partiellen (S, F)-Algebra A in eine partielle (S, F)-Algebra B heißt Endo-, Mono-, Epi-, Isomorphismus

isomorph Automorphismus

• Endomorphismus, wenn A = B ist; • Monomorphismus, wenn alle ϕ M injektiv sind (M ∈ S); • Epimorphismus, wenn alle ϕ M surjektiv sind (M ∈ S);   • Isomorphismus, wenn alle ϕ M bijektiv sind (M ∈ S) und ϕ−1 M M ∈S ein Homomorphismus von B nach A ist. Wenn es einen Isomorphismus von A nach B gibt, heißen die Algebren A und B isomorph. Die Umkehrabbildungen bilden dann ebenfalls einen Isomorphismus. Stimmen für einen Isomorphismus die beiden Algebren A und B überein, so nennen wir ihn einen Automorphismus. Ein triviales Beispiel hierfür ist die Familie aus Identitäten der Trägermengen.  Ist A in der obigen Definition sogar total, so ist ein Homomorphismus von A nach B aus lauter bijektiven Abbildungen bereits ein Isomorphismus:

2.1 Rechenstrukturen

57

Lemma 2.2. Besteht ein Homomorphismus von einer totalen Algebra in eine partielle Algebra derselben Signatur aus lauter bijektiven Abbildungen, so ist er ein Isomorphismus. Beweis. Sei Σ = (S, F) eine Signatur. Sei A eine totale Σ-Algebra und B eine partielle Σ-Algebra. −1 (c B ) = Ist c∈F eine nullstellige Konstante der Sorte M, so gilt ϕM −1 A A ϕM (ϕM (c )) = c . Sei f ∈F ein Bezeichner der Sorte M1, . . . , Mn → Mn+1 mit n∈N+ . Sei (b1, −1 (b ), . . . , ϕ−1 (b )) definiert. Nun . . . , bn) ∈ dom f B . Da A total ist, ist f A(ϕM 1 Mn n 1 −1 B −1 B −1 (b )), . . . , ϕ −1 gilt ϕMn+1 ( f (b1, . . . , bn )) = ϕMn+1 ( f (ϕM1 (ϕM 1 Mn (ϕMn (bn )))) = 1 −1 (ϕ A −1 −1 [da ϕ ein Homomorphismus ist] ϕM Mn+1 ( f (ϕM1 (b1 ), . . . , ϕMn (bn )))) = n+1 A −1 −1 QED f (ϕM1 (b1 ), . . . , ϕMn (bn )). Es gibt Homomorphismen von partiellen in totale Algebren, die aus lauter bijektiven Abbildungen bestehen, jedoch keinen Isomorphismus darstellen: Beispiel 2.16. Sei Σ = {{M }, { f : M→M }} eine Signatur. Sei A eine partielle Σ-Algebra mit M A = {0} und undefiniertem f A(0). Sei B eine totale Σ-Algebra auf der gleichen Trägermenge M B = {0} mit f B (0) = 0. Sei ϕ M : M A → M B , 0 → 0 Dann ist die Familie ϕ = (ϕ N ) N ∈ {M } ein Homomorphismus bestehend aus einer bijektiven Abbildung. Die Familie (ϕ−1 N ) N ∈ {M } ist hingegen kein Homomorphismus im Sinne der Def. 2.9, da f A(0) nicht definiert ist.  Eine Verkettung von Isomorphismen ergibt wieder einen Isomorphismus: Lemma 2.3. Sei Σ eine Signatur, A, B und C partielle Σ-Rechenstrukturen, ϕ ein Isomorphismus von A nach B und ψ ein Isomorphismus von B nach C. Dann ist (ϕ M ◦ ψ M ) M ∈S ein Isomorphismus von A nach C. Beweis. Sei (S, F) = Σ. Nach Def. 2.11 sind alle ϕ M und ψ M bijektiv (M ∈ S), −1 (ϕ−1 M ) M ∈S ist ein Homomorphismus von B nach A und (ψ M ) M ∈S ist ein Homomorphismus von C nach B. Nach Lemma 2.1 ist die Familie θ = (θ M ) M ∈S = (ϕ M ◦ ψ M ) M ∈S ein Homomorphismus von A nach C. Da ϕ M und ψ M bijektiv sind, ist ϕ M ◦ ψ M ebenso bijektiv mit inverser Abbildung θ −1 M = −1 ◦ ϕ−1 (M ∈ S). Nach Lemma 2.1 ist die komponentenweise Verkettung ψM M −1 −1 von Homomorphismen (θ −1 M ) M ∈S = (ψ M ◦ ϕ M ) M ∈S ein Homomorphismus von C nach A. Nach Def. 2.11 ist θ ein Isomorphismus von A nach C. QED Wenn wir in einer Programmiersprache keine Wahrheitswerte zur Verfügung haben, aber Zahlen zur Verfügung stehen, können wir die Wahrheitswerte auch durch natürliche Zahlen darstellen. Eine von mehreren möglichen Darstellungen ist wie folgt beschrieben: • 0 repräsentiert O.

58

2 Rechen- und Datenstrukturen

not

7 Bool

0

A

Bool A

3 ϕ

ϕ

Bool B

L

O

not

Bool B

Abb. 2.2 Veranschaulichung des Homomorphismus zwischen (fast booleschen) Algebren aus Bsp. 2.17.

• Für alle x ∈ N mit x > 0 gilt: x steht für L (der Wert L hat damit mehrere Darstellungen in den Zahlen, jede positive Zahl stellt L dar). Diese Idee wird im folgenden Beispiel erläutert. Beispiel 2.17 (Fortsetzung von Bsp. 2.15 für Bool). Wählen wir Σ = ({Bool}, {and, or : Bool, Bool → Bool, not : Bool → Bool}) und zwei Interpretationen dieser Signatur, totale Algebren A und B: Bool A = N

Bool B = B

and = ∗

and B = ∧

A

or A = + not A(n) =



or B = ∨

0 42

falls n > 0 sonst

not B = ¬

Die Zahl 42 ist hier völlig willkürlich gewählt. Wir könnten auch jede andere positive Zahl für die Repräsentation von true wählen. Ein Homomorphismus von A nach B besteht aus der Abbildung

O falls n = 0 , ϕ : N → B, n → L sonst. Es ist leicht zu sehen, dass dieser Homomorphismus kein Isomorphismus ist (und dass in diesem Fall kein Isomorphismus existieren kann). Wir können die Abbildung nicht umkehren, weil ϕ nicht injektiv ist, da auf den Wahrheitswert L unterschiedliche natürliche Zahlen (alle positiven Zahlen) abgebildet werden (vgl. Abb. 2.2).  Definition 2.12 (Terminterpretation). Sei Σ = (S, F) eine Signatur und A eine partielle Σ-Algebra. Die Terminterpretation – genauer die Interpretation

2.1 Rechenstrukturen

59

von Grundtermen über der Signatur Σ in der Σ-Algebra A – ist eine Familie A) ϕ A = (ϕ M M ∈S von partiellen Abbildungen A : M WΣ  M A ϕM Terme der Elemente der Sorte M Sorte M in A

(für M ∈ S)

die induktiv über den Termaufbau wie folgt definiert sind: A ϕM (c) = c A

für nullstellige Konstantensymbole c ∈ F einer Grundsorte M ∈ S und   A   A A „ f (t1, . . . , tn )“ = f A ϕ M ϕM (t ), . . . , ϕ M (t ) n n n+1 1 1 für die Funktionssymbole f : M1, . . . , Mn → Mn+1 mit n ≥ 1 in F, wobei die letzte Gleichung wie folgt zu interpretieren ist: Wenn die rechte Seite A (t ) undefiniert ist oder wenn alle ϕ A (t ) undefiniert ist (also wenn eins der ϕ M i Mi i i A A definiert sind und (ϕ M1 (t1 ), . . . , ϕ Mn (tn ))  dom f A), so ist auch die linke Seite undefiniert. Bei der Schreibweise „ f (t1, . . . , tn )“ weisen die Anführungszeichen darauf hin, dass wir über den Term und nicht über seine Interpretation sprechen.  Anschaulich gesprochen ordnet die Terminterpretation in der obigen Definition jedem Grundterm seinen Wert zu. Beispiel 2.18 (Terminterpretation). Kommen wir wieder auf das vorangegangene Bsp. 2.17 der Darstellung der Wahrheitswerte durch Zahlen zurück. Da die Signatur aus jenem Beispiel keine nullstelligen Symbole enthält, gibt es keine Grundterme, und die Terminterpretationen in A sowie in B sind trivial: sie bestehen aus lauter leeren Abbildungen. Um ein interessanteres Beispiel zu erhalten, erweitern wir nun A zur totalen Algebra C um nullstellige Konstantensymbole true und false, sodass trueC = 42 und falseC = 0. Dann gilt (Terme sind in Anführungszeichen „· · ·“ geschrieben): ϕC Bool („and(or(true, false), not(true))“) C = andC (ϕC Bool („or(true, false)“), ϕBool („not(true)“)) C = ϕC Bool („or(true, false)“) ∗ ϕBool („not(true)“) C C C = orC (ϕC Bool („true“), ϕBool („false“)) ∗ not (ϕBool („true“)) C C C = (ϕC Bool („true“) + ϕBool („false“)) ∗ not (true )

= (trueC + falseC ) ∗ notC (42) = (42 + 0) ∗ 0 = 42 ∗ 0 = 0



Für totale Algebren ist die Terminterpretation ein Homomorphismus, der Terme („Syntax“) in Werte („Semantik“) abbildet. Da für partielle Algebren die Terminterpretation aus partiellen, also im Allgemeinen nicht totalen Abbildungen

60

2 Rechen- und Datenstrukturen

besteht, ist sie im Allgemeinen kein Homomorphismus im Sinne der Def. 2.9. Im Falle partieller Algebren ist es also naheliegend, von „totalen“ zu „partiellen“ Homomorphismen überzugehen (siehe [BW82]). „Partielle“ Homomorphismen erfordern aber zusätzliche Überlegungen, die etwa ausschließen, dass die überall undefinierte Abbildung nicht stets als Homomorphismus aufgefasst wird. Später werden wir ohnehin zeigen, wie man partielle Algebren zu totalen Algebren vervollständigen kann, für die dann unser Begriff des „totalen“ Homomorphismus ausreicht. A Im Kontext von Def. 2.12 ist c A gleich der Terminterpretation ϕsort (c) für (c) jedes nullstellige Konstantensymbol c aus Σ. Wir erweitern diese Schreibweise def A auf beliebige Grundterme t und schreiben t A = ϕ M (t) für die TerminterpretaW Σ tion eines beliebigen t ∈ M der Sorte M in der Algebra A. Lemma 2.4. Sei Σ = (S, F) eine Signatur, A, B seien partielle Σ-Algebren, ψ ein Homomorphismus von A nach B. Ist t ∈ WΣ ein Grundterm der Sorte M ∈ S, sodass t A definiert ist, so ist auch t B definiert und gleich ψ M (t A). Beweis. Beweis per Induktion über die Teiltermrelation. Sei also t ∈ WΣ ein beliebiger Grundterm der Sorte M ∈ S und die Aussage für alle echten Teilterme von t gezeigt. Fall t nullstellig: Dann ist t B = ψ M (t A) nach Definition eines Homomorphismus. Fall t = „ f (t1, . . . , tn )“ , wobei jedes ti die Sorte Mi ∈ S hat (1 ≤ i ≤ n): Seien ϕ A, ϕ B Terminterpretationen in A bzw. B und t A definiert. Dann sind alle tiA für 1 ≤ i ≤ n definiert und es gilt:   A „ f (t , . . . , t )“ ψ M (t A) = ψ M ϕ M 1 n [nach Definition der Terminterpretation]  A  A (t ) = ψM f A ϕ M (t ), . . . , ϕ 1 n Mn 1 [nach der Definition eines Homomorphismus]  A  A   = f B ψ M1 ϕ M ϕ (t ) , . . . , ψ (t ) 1 M n n Mn 1 [nachInduktionsannahme]  B = f B ϕB M1 (t1 ), . . . , ϕ Mn (tn ) [nach Definition einer Terminterpretation] B QED = ϕB M „ f (t1, . . . , tn )“ = t . Dies zeigt, dass die Terminterpretation mit Homomorphismen verträglich ist. Existiert ein Homomorphismus von A nach B, so können wir Terme zunächst in A interpretieren. Der Homomorphismus liefert dann daraus die Interpretation in B. Für eine Algebra ist der Begriff ihrer Abgeschlossenheit im Hinblick auf ihre Operationen fundamental. Durch die Anwendung der Funktionen auf die Elemente der Trägermengen entstehen stets nur Elemente der Trägermengen. Interessant ist jedoch auch die Umkehrung dieser Betrachtungsweise. Können alle Elemente einer Trägermenge der Algebra ausgehend von den Bestandteilen

2.1 Rechenstrukturen

61

der Signatur durch mehrfache Anwendung der Funktionen auf die Konstanten „induktiv“ analog der Termbildung aufgebaut (erzeugt) werden, so nennen wir die entsprechende Sorte termerzeugt in dieser Algebra. Wir nennen eine Trägermenge einer Algebra termerzeugt, wenn, kurz gefasst, jedes Element der Trägermenge der Sorte die Terminterpretation eines Grundterms dieser Sorte ist. Fordern wir für eine Sorte die Termerzeugtheit, so heißt das, dass wir nur Modelle zulassen, bei denen die zur Sorte gehörende Trägermenge termerzeugt ist. Definition 2.13 (Termerzeugtheit). Eine Sorte M der Signatur Σ heißt in A . Sind alle einer partiellen Σ-Algebra A (Σ-)termerzeugt, falls M A ⊆ img ϕ M Sorten von Σ in A termerzeugt, so heißt A termerzeugt.  Anschaulich gesprochen entspricht die Termerzeugtheit der induktiven Charakterisierung einer Menge (vgl. Def. 2.6). Beispiel 2.19 (Termerzeugtheit). Für die Signatur Σ mit Sorten Seq und Nat und Bezeichnern zero, succ, emptyseq, makeseq und conc betrachten wir die totale Σ-Algebra A mit den Trägermengen Seq A = N∗

(Endliche Sequenzen natürlicher Zahlen)

Nat = N A

und die Funktionen conc A(x, y) = x ◦ y emptyseq A = ε makeseq A(n) = n zero A =0 succ A(n) = n + 1

⎫ ⎪ ⎬ ⎪ ⎪ ⎪ ⎭

erzeugen

Seq A

erzeugen

Nat A (jedoch keine ganzen Zahlen)

Die Algebra A ist termerzeugt. Lässt man in der Algebra nur eine der aufgeführten Funktionen weg, so ist das Ergebnis nicht länger termerzeugt. Ersetzt man N durch die Menge der ganzen Zahlen Z, ist die entstehende Rechenstruktur ebenfalls nicht länger termerzeugt.  Anmerkung 2.3. Ob eine Sorte der partiellen Σ-Algebra oder gar die ganze Algebra termerzeugt ist, hängt wesentlich davon ab, welche Konstanten und Funktionssymbole in Σ verfügbar sind, und nicht nur davon, welche Elemente in den Trägermengen auftreten. Für die Termerzeugtheit einer Sorte in einer partiellen Algebra ist die Signatur, insbesondere der Satz der dadurch verfügbaren Funktionen, ausschlaggebend.  Bemerkenswerterweise existieren für termerzeugte partiellen Algebren keine echten Unteralgebren; die Trägermengen dieser Algebren sind bezogen auf die betrachtete Signatur minimal:

termerzeugt

62

2 Rechen- und Datenstrukturen

Satz 2.1. Sei A eine partielle Unteralgebra einer termerzeugten partiellen Algebra A. Dann gilt A = A. Beweis. Sei nun (S, F) = Σ die gemeinsame Signatur beider Algebren. Wir zeigen erstens induktiv, dass wenn ein Σ-Grundterm zu einem Wert in A interpretiert wird, so liegt dieser Wert bereits in der Trägermenge entsprechender Sorte der Algebra A. A einer Sorte M ∈ S beliebig und (Induktionsannahme) Sei dazu t ∈ dom ϕ M A A für jeden echten Teilterm s von t gelte s ∈ dom ϕsort ⇒ ϕsort (s) ∈ sort(s) A. (s) (s) !



A (t) ∈ M A. Wir zeigen ϕ M 



A (t) = t A = [nach Def. 2.8] t A ∈ M A. Fall t ∈ F. Dann ϕ M Fall t = „ f (t1, . . . , tn )“ für ein f ∈ F der Sorte M1, . . . , Mn → M mit n ≥ 1.   A (t) = f A ϕ A (t ), . . . , ϕ A (t ) , und der letzte Nach Def. 2.12 ist ϕ M M1 1 Mn n A (1 ≤ i ≤ Ausdruck vollständig definiert. Insbesondere gilt ti ∈ dom ϕM i  A    A (t ) ∈ dom f A . Nach Induktionsannahme gilt n) und ϕM1 (t1 ), . . . , ϕM n n   A (t ) ∈ M A (1 ≤ i ≤ n). Also ϕ A (t ), . . . , ϕ A (t ) ∈ M A × · · · × M A. ϕM i n M1 1 Mn n i 1 i  A    A (t ) ∈ dom f A A Daher ϕM1 (t1 ), . . . , ϕM . Aus f A A A | | n M1 ×···×Mn M1 ×···×MnA n  A    A  A A A = [nach Def. 2.8] f erhalten wir f ϕM1 (t1 ), . . . , ϕMn (tn ) = f A ϕM (t1 ), 1  A (t ) . Insbesondere ist der letzte Ausdruck definiert und sein Wert . . . , ϕM n n    A A (t ) definiert liegt daher in img f A ⊆ M A. Also ist f A ϕM (t1 ), . . . , ϕM n n 1   A „ f (t , . . . , t )“ mit Wert in M A. Nach Def. 2.12 ist der Ausdruck ϕ M 1 n definiert mit Wert in M A.

Per Induktion zeigten wir somit 



A A t ∈ dom ϕ M ⇒ ϕM (t) ∈ M A

(M ∈ S, t ∈ M WΣ ) .

(2.3)



Ist nun x ∈ M A beliebig, so gilt wegen der Termerzeugtheit von A nach A (M ∈ S). Also gibt es ein t ∈ dom ϕ A mit x = ϕ A (t). Def. 2.13 x ∈ img ϕ M M M  Nach (2.3) gilt x ∈ M A. Da x beliebig war, gilt M A ⊆ M A. Die umgekehrte Inklusion gilt nach Def. 2.8. Insgesamt ergibt sich also 

MA = MA

(M ∈ S) .

(2.4)

Die Interpretationen nullstelliger Konstantenbezeichner sind in A und A per Definition gleich. Für einen nichtnullstelligen Funktionsbezeichner f ∈ F   der Sorte M1, . . . , Mn → Mn+1 gilt f A = f A | M A ×···×MnA = [nach (2.4)] 

1

f A | M A ×···×MnA = [nach Def. 2.8] f A. 1       Zusammengefasst, (M A) M ∈S , ( f A) f ∈F = (M A ) M ∈S , ( f A ) f ∈F .

QED

Lemma 2.5. Für eine termerzeugte partielle Algebra existiert nur ein Endomorphismus. Dieser besteht aus den Identitätsfunktionen auf den Trägermengen.

2.1 Rechenstrukturen

63

Beweis. Sei Σ = (S, F) eine Signatur, A eine termerzeugte Σ-Algebra und θ ein Endomorphismus von A. Sei M ∈ S. Sei x ∈ M A beliebig. Da A termerzeugt ist, gibt es einen Grundterm t ∈ M WΣ mit x = t A. Dann gilt θ M (x) = θ M (t A) = [nach Lemma 2.4] t A = x. Da x beliebig war, gilt θ M = id M A (M ∈ S). QED

2.1.8 Allgemeine Bemerkungen zu Rechenstrukturen Rechenstrukturen oder (in der Mathematik gebräuchlicher) Algebren sind sehr allgemeine mathematische Strukturen. Sie bestehen aus Mengen und Funktionen, die wesentliche Grundlagen der Mathematik sind. Praktisch alle Strukturen der Informatik können sinnvoll als Rechenstrukturen beschrieben werden. Klassische Beispiele für Gebilde, die sich als Rechenstrukturen modellieren lassen, sind: • Elementare Datenstrukturen in ihrer Zugriffssicht (wie Bool, Stack, Queue, Tree, Array, Record, Hash-Table etc. – siehe Abschnitt 3.1.2 ff.), • Domänenmodelle (siehe Bsp. 2.20), • Programmiersprachen, • Sprachen zur Beschreibung verteilter Systeme (Prozessalgebren), • Zustandsmaschinen, • Hardware. So können wir selbst Programmiersprachen oder Hardware als Rechenstrukturen auffassen und beschreiben. Auch spezielle Systemstrukturen lassen sich als Rechenstrukturen auffassen. So können Rechner, aber auch Prozesse in Betriebssystemen mit algebraischen Mitteln als Rechenstrukturen beschrieben werden. Dies zeigt, dass der Begriff der Rechenstruktur zentral, jedoch gleichzeitig so allgemein ist, dass für dieses Konzept allein keine sehr tiefliegenden Aussagen und Resultate zu erwarten sind. Der Begriff liefert ein sehr umfassendes, für die Modellbildung und Programmierung zentrales Konzept. Wir werden dieses Konzept der Algebra im Folgenden hauptsächlich für Modellierung von Datenstrukturen nutzen. Oft ist bei der Erarbeitung der Rechenstrukturauffassung für diese Gebilde die Entscheidung zu treffen, welche Zugriffssicht eingenommen werden soll. Wir werden darauf zurückkommen. Wir beschränken uns allerdings zunächst auf die Behandlung und Darstellung der logischen Eigenschaften von Datenstrukturen durch entsprechende Rechenstrukturen.

64

2 Rechen- und Datenstrukturen

2.2 Beschreibung von Daten- und Rechenstrukturen Bei der Modellbildung der Zusammenhänge des Anwendungsgebietes, beim Erstellen eines Domänenmodells oder der Erfassung der Strukturen und des Verhaltens eines Programms ist die Festlegung und Beschreibung des Datenmodells eine wesentliche Aufgabe der Programmentwicklung. Dabei sind drei Teilaufgaben zu bewältigen: • Konzeptuelle Abgrenzung (engl. scoping): Es ist zu entscheiden, welche Konzepte und Informationen relevant sind und durch welche Zugriffsstrukturen (Signatur) sie dargestellt und beschrieben werden sollen. • Modellwahl: Es ist zu entscheiden, welches Modell (welche Rechenstruktur) gewählt wird, um die Informationen darzustellen. • Modellbeschreibung: Es ist zu entscheiden, mit welchen Konzepten und Beschreibungsmitteln das gewählte Modell (die Rechenstruktur) dargestellt werden soll (umgangssprachlich, halbformal, formal). Wir behandeln im Weiteren primär den zweiten und dritten Aspekt. Der erste ist kaum befriedigend von einem konkreten Anwendungskontext losgelöst zu betrachten, wobei aber gerade hier wichtige Entscheidungen getroffen werden. In Programmiersprachen existieren durch die vorgegebenen Datentypen und Datentypkonstruktionen bereits Mittel zur Datenmodellierung. Objektorientierte Datenmodellierung (siehe auch Kap. 8) ist ein gutes Beispiel dafür, wie die Konzepte von Programmiersprachen zur Datenmodellierung eingesetzt werden. In Programmiersprachen sind in der Regel gewisse Sorten (Typen) und charakteristische Funktionen darauf fest vorgegeben. Beispiele sind Zahlen oder die Wahrheitswerte. Daneben sind in der Regel Möglichkeiten für die Deklaration weiterer Sorten, etwa durch Records oder in der Objektorientierung durch Klassen, vorgesehen. Genau genommen wird also eine Deklarationssprache für Sorten und bestimmte Zugriffsfunktionen zur Verfügung gestellt. Die Bedeutung dieser Deklarationen kann festgelegt werden, indem wir angeben, welche Rechenstrukturen durch die Sortenvereinbarungen eingeführt werden. Wir kommen darauf zurück. Zunächst wenden wir uns der Frage zu, mit welchen Mitteln wir Rechenstrukturen beschreiben können. Beim Entwurf von Programmsystemen ist die Beschreibung der vorgegebenen Rechenstrukturen eine zentrale Aufgabe. Dazu ist zunächst die Signatur festzulegen. Die Signatur gibt in gewisser Weise die syntaktische Sicht auf eine Rechenstruktur wieder. Sie gibt an, welche Funktionssymbole und Sorten von Bedeutung und verfügbar sind. Dies genügt als Information, um syntaktisch korrekte Programme über einer Rechenstruktur schreiben zu können. Um jedoch die Wirkungsweise der Funktionen über einer Rechenstruktur erfassen zu können, benötigen wir weitere Angaben über ihr Verhalten, ihre Semantik. Bei der Beschreibung des Verhaltens von Rechenstrukturen unterscheiden wir grundsätzlich zwischen zwei Methoden: modellorientiert

(1) Modellorientiert: In diesem Fall wird ein konkretes Modell13, in der Regel

2.2 Beschreibung von Daten- und Rechenstrukturen

65

ein mathematisches Modell oder ein Modell der Informatik, etwa mithilfe einer Programmiersprache, angegeben, abgestützt auf vorgegebene Konzepte wie Mengen, Funktionen und Relationen oder auf programmiersprachliche oder grafische Mittel. (2) Eigenschaftsorientiert: In diesem Fall werden neben der Signatur lediglich eigenschaftsorientiert die prägnanten charakteristischen Eigenschaften der zu beschreibenden Rechenstruktur angegeben, ohne jedoch eine bestimmte Datenstruktur zu beschreiben; dies geschieht etwa durch Angabe der Signatur und, darauf abgestützt, der Angabe von Eigenschaften etwa durch Formeln der mathematischen Logik. Im Allgemeinen wird dadurch nicht genau eine Rechenstruktur, sondern eine Klasse von Rechenstrukturen beschrieben, die die durch die Formeln spezifizierten Eigenschaften besitzen. In der Praxis werden häufig auch standardisierte Notationen zur Beschreibung des Datenmodells verwendet, oft ohne ein bestimmtes Modell (sondern eine Menge von Modellen) damit explizit anzugeben. Wir sprechen von einer halbformalen Beschreibungstechnik, wenn zumindest die Syntax der Beschreibungsmittel genau festgelegt ist. Die Modellierung durch Sortendeklarationen entspricht in der Regel schon einer Implementierungssicht, die Charakterisierung durch Gesetze eher der Zugriffssicht (vgl. Abschnitte 1.5.2 und 2.1.2). Wir betrachten im Folgenden kurz beide Varianten und diskutieren ihre Vor- und Nachteile. Beispiel 2.20 (Spezifikation des Datenmodells einer Bank). Um ein stark vereinfachtes Datenmodell einer Bank zu beschreiben, geben wir wie beispielsweise in Spez. 2.1 zunächst nur die entsprechende Signatur an. SPEC BANK = { sort Kunde, Sachbearbeiter , Konto, Name, Anschrift, KontoNr betreuer : nummer : inhaber : name : adresse : kontoanlegen :

}

Kunde → Sachbearbeiter Konto → KontoNr Konto → Kunde Kunde → Name Kunde → Anschrift KontoNr , Kunde → Konto

Spezifikation 2.1 Signatur zu Bsp. 2.20

Genau genommen beschreibt dies natürlich nur einen stark vereinfachten Ausschnitt der Sorten und Funktionen für die Domäne BANK. Insbesondere sind dadurch nur „syntaktische“ Beziehungen festgelegt. Dabei wird zunächst völlig offengelassen, ob wir die Rechenstruktur eigenschafts- oder modellorientiert beschreiben. Die Signatur können wir auch gut grafisch durch ein Signaturdiagramm beschreiben, wie in Abb. 2.3 dargestellt. 13 Eine bestimmte Rechenstruktur, eine Datenstruktur und damit eine konkrete Algebra.

66

2 Rechen- und Datenstrukturen

Name

KontoNr

nummer

Konto

kontoanlegen name

inhaber Kunde

adresse betreuer Anschrift

Sachbearbeiter

Abb. 2.3 Grafische Darstellung der Signatur der Spezifikation BANK

Man beachte den engen Zusammenhang zur Bildung einer Ontologie für die Anwendungsdomäne BANK. 

2.2.1 Modellorientierte Beschreibung von Rechenstrukturen In einer modellorientierten Beschreibung wird eine Rechenstruktur beschrieben, indem wir ihre Trägermengen mit deren Elementen und Funktionen explizit angeben. Dazu formen wir die Trägermengen aus einem Satz vorgegebener Mengen und Operationen zur Konstruktion von Mengen. Genau genommen setzen wir vorgegebene Rechenstrukturen und deren Trägermengen voraus und formen darüber weitere Strukturen durch Konstruktionen wie Mengenvereinigung und Mengenprodukt. Um ein Modell für eine Rechenstruktur anzugeben, greifen wir wieder auf klassische Konzepte der Mathematik zurück, wie Mengen, Relationen und Funktionen. Daneben lassen sich auch bekannte Strukturen der Informatik wie Datentypdeklarationen, Dateien oder Geflechtstrukturen verwenden. Betrachten wir eine konkrete Rechenstruktur (Algebra), so ist es möglich, nach dem inneren Aufbau, der Struktur, der in ihren Trägermengen auftretenden Datenelemente zu fragen. Wir sprechen auch von der Datenstruktur. Durch die Betrachtung der Datenstrukturen tritt häufig die Bedeutung der charakteristischen Funktionen, die Zugriffsstruktur, in den Hintergrund. Sie wird überlagert durch die implementierungsgeprägte Struktur der Datenelemente, die allerdings in der Regel auch gewisse Zerlegungs- und Konstruktionsoperationen nahelegt (vgl. Abschnitte 1.5.2 und 2.1.2). Eine Schwierigkeit der modellorientierten Beschreibung besteht in dem Umstand, dass man Datenstrukturen in der Regel nicht ansieht, welche Aspekte der Rechenstruktur und welche Zugriffsstruktur für die betrachtete Anwendung wesentlich und welche unwesentlich sind. Beispiel 2.21 (Konkrete Modelle für die Rechenstruktur der natürlichen Zahlen). Es gibt eine Vielzahl unterschiedlicher konkreter Darstellungen (Trägermengen und Datenstrukturen zur Darstellung) der natürlichen Zahlen:

2.2 Beschreibung von Daten- und Rechenstrukturen

67

      • Von-Neumann-Darstellung14: ∅, {∅}, ∅, {∅} , ∅, {∅}, ∅, {∅} , ∅, {∅},       ∅, {∅} , ∅, {∅}, ∅, {∅} ,...; • • • •

Strichzahlen: , |, ||, |||, ||||, . . . ; Binärzahlen: O, L, LO, LL, LOO, . . . ; Dezimalzahlen: 0, 1, 2, 3, 4, . . . ; Römische Zahldarstellung: , I, I I, I I I, IV, . . .

Alle diese Trägermengen modellieren natürliche Zahlen. Die Elemente der Trägermenge weisen unterschiedliche Strukturen auf. Alle diese Modelle bilden bei zusätzlicher Einführung der klassischen arithmetischen Funktionen zueinander isomorphe Algebren zur Darstellung der natürlichen Zahlen.  Anschaulich sind isomorphe Rechenstrukturen zueinander äquivalent. Präziser ausgedrückt: Lemma 2.6. Sei Σ eine Signatur. Sei T eine Menge partieller Σ-Rechenstrukturen. Dann ist die durch def

A ∼ B ⇐⇒ es gibt einen Isomorphismus von A nach B

(A, B ∈ T )

definierte binäre Relation ∼ eine Äquivalenzrelation auf T . Beweis. Sei (S, F) = Σ. Wir weisen nun die definierenden Eigenschaften einer Äquivalenzrelation direkt nach. Reflexivität auf T : Für jedes A ∈ T wähle man die Familie aus Identitäten (id M A ) M ∈S als selbstinversen Isomorphismus von A nach A. Symmetrie: Seien A, B ∈ T und mit einem Isomorphismus ϕ von A nach B gegeben. Dann sind alle ϕ M bijektiv (M ∈ S) und (ϕ−1 M ) M ∈S bildet einen Homomorphismus von B nach A. Insbesondere sind alle ϕ−1 M bijektiv −1

(M ∈ S) und die Familie ihrer Inversen (ϕ−1 M ) M ∈S ist gerade der Homomorphismus ϕ von A nach B. Somit ist (ϕ−1 M ) M ∈S ein Isomorphismus von B nach A. Transitivität: Gilt nach Lemma 2.3. QED

Man beachte im Kontext von Bsp. 2.21, dass wenn man das Inkrementieren (also, die Addition der Eins) und das Dekrementieren (also, die Subtraktion von Eins) in den angegebenen Darstellungen natürlicher Zahlen formal definieren wollen würde, so würde man das Inkrementieren als totale Funktion definieren wollen und man würde das Dekrementieren nur für die positiven Zahlen definieren wollen, und zwar unabhängig davon, mit welcher der fünf angegeben 14 John/Johann von Neumann, * 28. Dezember 1903 in Budapest, Österreich-Ungarn als János (Yonah) Lajos Neumann; † 8. Februar 1957 in Washington, D.C. Bei der Von-NeumannDarstellung wird die Null durch die leere Menge und der Nachfolger der Zahl x durch x ∪ {x } dargestellt.

68

2 Rechen- und Datenstrukturen

Algebren man zu tun hat. Diese Unabhängigkeit von der Wahl der isomorphen Algebra hat weniger mit natürlichen Zahlen zu tun, vielmehr sie ist allein dem Isomorphiebegriff inne: wenn die Interpretation eines gegebenen Grundterms in einer partiellen Algebra definiert ist, so ist sie auch in jeder dazu isomorphen partiellen Algebra definiert. Formal: Lemma 2.7. Ist Σ = (S, F) eine Signatur und sind A und B zueinander A = dom ϕ B (M ∈ S). isomorphe partielle Σ-Algebren, so dom ϕ M M Beweis. Ist ein Isomorphismus ψ von A nach B gegeben, so sind ψ und seine −1 ) komponentenweise Umkehrung (ψ M M ∈S insbesondere Homomorphismen. W A (t) definiert, so auch Ist also für einen Grundterm t ∈ M Σ der Ausdruck ϕ M B ϕ M (t) laut Lemma 2.4 und umgekehrt (M ∈ S). QED Für eine modellorientierte Beschreibung einer Rechenstruktur zu einer gegebenen Signatur geben wir für jede Sorte eine Trägermenge an und für jedes Funktionssymbol eine Funktion. Wir geben damit eine konkrete Rechenstruktur zu der Signatur an. Beispiel 2.22 (Eine totale Rechenstruktur I für den indexgesteuerten Zugriff). Ein informelles Beispiel einer Signatur für den indexgesteuerten Zugriff gaben wir bereits in Bsp. 2.5 an; Rechenstrukturen dieser Signatur stehen in Beispielen 2.9 und 2.14. Nun führen wir eine weitere, totale Rechenstruktur für diese Signatur an: Data I = {a, ..., z}∗ Index I = N Array I = (Index I → Data I ) get (earray , i) = ε I

I

Menge der totalen Funktionen

leere Zeichenkette als Standardelement

get (g, i) = g(i) I



put (g, i, d)( j) = I

g( j) d

falls falls

j i j =i



Wir verwenden im obigen bzw. im nachfolgenden Beispiel die als bekannt vorausgesetzte mathematische Notation für Mengen, Sequenzen und Funktionen. Beispiel 2.23 (Ein totales Modell Q für Prioritätsschlangen). Zur Signatur aus Bsp. 2.6 geben wir, ähnlich zur Rechenstruktur aus Bsp. 2.10, eine weitere totale Rechenstruktur Q an. Dabei verwenden wir wieder die dort deklarierte Menge K der Elemente, die in einer Prioritätsschlange abgelegt werden können, das Symbol ⊥ für „undefiniert“ und die Vergleichsoperation ≤ P . Seien BoolQ = B Item

Q

= K ∪ {⊥}

boolesche Werte speicherbare Elemente inkl. „undefiniert“

2.2 Beschreibung von Daten- und Rechenstrukturen

 {⊥} PqQ = K ∗ ∪

69

die Trägermenge zur Sorte Pq

trueQ = L

Wert für „Wahrheit“

≤ =≤

Vergleich wie in Bsp. 2.10

Q

emptyq

Q

P



enqQ (p,m) =

m ◦ p ⊥

⎧ ⎪ ⎨m ⎪

falls p  ⊥ ∧ m  ⊥ falls p = ⊥ ∨ m = ⊥ falls ∃p : p = m ◦ p ∧ (p=ε ∨ ¬(m< Q nextQ (p)))

nextQ (p) = nextQ (p  ) falls p = m ◦ p  ∧ (m < Q nextQ (p  ))

⎪ ⎪⊥ ⎩ ⎧ p ⎪ ⎪ ⎪ ⎪ ⎨ ⎪

falls p = ε ∨ p = ⊥

falls ∃ m : p = m ◦ p ∧ (p = ε ∨ ¬(m 0 Beweisen Sie folgende Aussage mittels noetherscher Ordnung auf Zahlenpaaren (für alle x, y ∈ Nat): sum(x, y) = sum(y, x) Übung 3.11. Definieren Sie eine algebraische Spezifikation BIN für binäre Zahlen. Übung 3.12. Definieren Sie einen Homomorphismus zwischen der Rechenstruktur binärer Zahlen und der Rechenstruktur natürlicher Zahlen. Übung 3.13. Sei Σ = ({Nat}, {zero, succ}) mit zero : Nat und succ : Nat → Nat eine vereinfachte Signatur für natürliche Zahlen. Definieren Sie die ΣRechenstruktur natürlicher Zahlen in • binärer, • unärer (als Striche) Darstellung. Geben Sie einen Isomorphismus zwischen den beiden Rechenstrukturen mit Beweis an. Übung 3.14. Zu modellieren ist ein Auskunftssystem eines Flughafens, in dem nationale und internationale Flüge repräsentiert werden können. Das System soll Auskunft über Flugnummer, Start- und Zielflughafen sowie Ankunft- und Abflugzeiten geben können. Außerdem sollen aus dem System die durch den Flug verbundenen Flughäfen ersichtlich sein.

182

3 Algebraische Datenmodellierung

1. Geben Sie ein E/R-Modell des Auskunftssystems an. Dabei sind folgende Eigenschaften umzusetzen: • Jeder Flughafen liegt in einer Stadt. Eine Stadt kann aber mehrere Flughäfen haben. • Jeder Flug hat einen bestimmten Start- und Zielflughafen. • Auf seiner Route (Verbindung Start-/Zielflughafen) kann ein Flug mehrere Zwischenlandungen haben. Auch die Funktionalitäten der Beziehungstypen sind anzugeben. Erstellen Sie dafür ein E/R-Diagramm und die entsprechende Beschreibung der Bestandteile des Diagramms. 2. In dem Abschnitt zu E/R-Diagrammen werden diese in Beziehung mit algebraischen Spezifikationen gesetzt. Dort wird gesagt, dass E/R-Diagramme als eine kompakte Schreibweise für eine Schar einfacher algebraischer Spezifikationen aufgefasst werden können und dass mittels Formeln algebraischer Spezifikation weitere Eigenschaften beschrieben werden können, die in E/R-Diagrammen nicht direkt ausdrückbar sind. Veranschaulichen Sie sich diese Aussagen anhand kurzer Beispiele in Bezug auf das modellierte Flugauskunftssystem. 3. Überlegen Sie sich eine entsprechende Erweiterung des E/R-Modells. Übung 3.15. Erweitern Sie die algebraische Spezifikation QUEUE so, dass • first(equeue) = ⊥ und rest(equeue) = underflowq • first(equeue) = underflow_error und rest(equeue) = ⊥ Wie sollen sich die Datentypen α und Queue α in den beiden Fällen unterscheiden? Übung 3.16. Erweitern Sie die Spezifikationen STACK, TREE und SEQ durch Einführung von ⊥ zu monomorphen Spezifikationen. Zeigen Sie die Monomorphie durch Angabe eindeutiger Normalformen unter Nutzung der Striktheitsgesetze. Übung 3.17. Übersetzen Sie die rekursive Deklaration für Bäume sort Tree α = cons(left : Tree α, root : α, right : Tree α) | etree

schematisch in eine algebraische Spezifikation und vergleichen Sie diese mit der Spezifikation TREE in Spez. 3.12. Übung 3.18. Betrachten Sie die Spezifikation TREE aus Abschnitt 3.2.4. 1. Identifizieren Sie die Fehlerfälle. 2. Passen Sie die Spezifikation so an, dass auch die Fehlerfälle berücksichtigt werden.

3.8 Übungsaufgaben

183

Übung 3.19. Verallgemeinern Sie Lemma 2.6 und Satz 3.2 auf abstrakte Rechenstrukturen (die eigentliche Klassen im Sinne von [Obe94] sind). Das heißt, untersuchen Sie, ob die Relation „es existiert ein Homomorphismus von einer Algebra der einen Isomorphieklasse in eine Algebra der anderen Isomorphieklasse“ zwischen den Isomorphieklassen partieller termerzeugter Σ-Algebren eine partielle Ordnung (die hier ausnahmsweise keine Menge sein muss) bildet. Übung 3.20. Untersuchen Sie, ob die Voraussetzung der Termerzeugtheit in Satz 3.2 notwendig ist oder ob man auf diese Voraussetzung verzichten und somit die Aussage verstärken kann. Übung 3.21. Untersuchen Sie im Kontext von Thm. und Def. 3.1, ob es neben der Kongruenz  weitere Kongruenzen ∼ auf WΣ geben kann, sodass A zu einer Teilalgebra von WΣ /∼ isomorph ist. Falls weitere derartige Kongruenzen existieren können, geben Sie ein Beispiel an. Ferner untersuchen Sie, ob es mehr als eine zu A isomorphe Teilalgebra von WΣ / (bzw. von WΣ /∼) geben kann. Übung 3.22. Untersuchen Sie im Kontext von Korollar 3.1, ob es neben der Kongruenz  weitere Kongruenzen ∼ auf WΣ geben kann, sodass A zu WΣ /∼ isomorph ist. Falls weitere derartige Kongruenzen existieren können, geben Sie ein Beispiel an, ansonsten einen Beweis der Nichtexistenz. Übung 3.23. Sei Σ eine Signatur. Zeigen Sie, dass die Menge der von allen Kongruenzen erzeugten Quotientenalgebren von WΣ mit der Halbordnung „es existiert ein Homomorphismus von einer Quotientenalgebra in die andere Quotientenalgebra“ ein vollständiger Verband ist. Übung 3.24 (Folgeaufgabe zur Übg. 2.1). Beschreiben Sie für Ihre Lösung der Übg. 2.1 die Zugriffssicht auf Prioritätskeller, indem Sie die typischen Eigenschaften eines Prioritätskellers durch allgemeingültige Formeln ausdrücken. Übung 3.25 (Folgeaufgabe zur Übg. 2.14). Geben Sie für Ihre Lösung der Übg. 2.14 Axiome an, die für Ihre Signatur der natürlichen Zahlen sinnvoll wären. Übung 3.26 (Folgeaufgabe zur Übg. 2.10). Definieren Sie die Anzahl der Knoten und die Höhe eines fast ausgeglichenen Binärbaumes für die Darstellung aus Ihrer Lösung der Übg. 2.10. Zeigen Sie, dass für genug hohe fast ausgeglichene Binärbäume die Höhe höchstens logarithmisch in der Anzahl der Knoten ist, d. h., dass die Funktion, die jedem genug hohen fast ausgeglichenen Binärbaum seine Höhe zuordnet, in O(ln(Anzahl der Knoten des Baumes)) liegt.20 20 Beim O handelt es sich um das Landau-Symbol mit folgender üblichen Bedeutung: sind f , g : M → N für eine Menge M, so gilt f ∈ O(g) genau dann, wenn es ein c ∈ N gibt, sodass für fast alle (d. h. alle bis auf endlich viele) x ∈ M gilt: f (x) ≤ cg(x).

184

3 Algebraische Datenmodellierung

Übung 3.27. Wie viele Homomorphismen von A nach B gibt es im Bsp. 3.7, wie viele davon sind Mono-, Epi- und Isomorphismen? Wie viele Homomorphismen von B nach A existieren, wie viele davon sind Mono-, Epi- und Isomorphismen? Geben Sie die entsprechenden Kardinalitäten mit Beweis an. Übung 3.28. Untersuchen Sie, ob das Gesetz (x ≤ y) = (succ(x) ≤ succ(y)) der Spezifikation INT aus der generated_by-Eigenschaft und anderen Axiomen über ≤ folgt und somit entbehrlich ist. Übung 3.29. Betrachten Sie die algebraischen Spezifikationen NAT und INT aus den Spezn. 3.3 und 3.6. Zeigen Sie: 1. NAT ist konsistent. 2. NAT ist nicht monomorph. 3. NAT ist nicht vollständig. 4. INT ist konsistent. 5. INT ist monomorph. 6. INT ist vollständig. Anmerkung 3.6. Um Konsistenz zu zeigen, konstruieren Sie ein Modell von NAT aus dem Unendlichkeitsaxiom des üblichen Zermelo-Fraenkelschen Axiomensystems (vgl. [Obe94]). Dieses Unendlichkeitsaxiom besagt, dass es eine Menge N gibt, die die leere Menge ∅ und mit jedem Element x ∈ N auch die Menge x ∪ {x} enthält, formal: ∃ N : (∅ ∈ N ∧ ∀ x ∈ N : x ∪ {x} ∈ N) Um die ganzen Zahlen zu konstruieren, benutzen Sie die natürlichen Zahlen.  Übung 3.30. Sei Σ = (S, F) eine Signatur. Wir konstruieren eine partielle ΣAlgebra A wie folgt. Für jede Sorte M ∈ S: wenn es mindestens eine nullstellige Konstante in F der Sorte M gibt, legen wir M A auf eine beliebige einelementige Menge fest, ansonsten lassen wir M A leer. Wir legen die Interpretationen aller ein- und mehrstelligen Funktionssymbole in F auf nirgendwo definierte Funktionen passender Sorten. Zeigen Sie: A ist initial in der Klasse aller partiellen Σ-Algebren. Übung 3.31 (Wiederholung des Theorems 3.3). Sei Σ = (S, F) eine Signatur und sei für die totale Σ-Algebra A zu jeder Sorte M ∈ S ein Wert a M gegeben, sodass M A = {a M }. Beweisen Sie, dass die Σ-Algebra A terminal ist in der Klasse 1. aller partiellen Σ-Algebren, 2. aller totalen Σ-Algebren. Übung 3.32. Zeigen Sie oder widerlegen Sie: für jede konsistente algebraische Spezifikation besitzt die Klasse der Modelle eine initiale Algebra. Hinweis: Betrachten Sie Spez. 3.16.

3.8 Übungsaufgaben

185

Übung 3.33. Zeigen Sie, dass für die algebraische Spezifikation aus Spez. 3.18 kein terminales Modell existiert und dass man deshalb die Isomorphieklassen zu der Spezifikation, dargestellt als Quotientenalgebren (siehe Übg. 3.23), nicht als einen Verband ansehen kann. Übung 3.34. Entwerfen Sie eine algebraische Spezifikation für fast ausgeglichene Binärbäume. Übung 3.35. Betrachten Sie die Klasse der Modelle der algebraischen Spezifikation FINSET. 1. Geben Sie ein terminales Modell der Klasse an oder zeigen Sie, dass es keins gibt. 2. Geben Sie ein nichtterminales Modell der Klasse an oder zeigen Sie, dass es keins gibt. Beantworten Sie die obigen zwei Fragen für die Klasse der Modelle der algebraischen Spezifikation FINSET , die aus FINSET durch die Löschung der Axiome (∗) und (∗∗) entsteht.

Kapitel 4

Funktionale Programmierung

In den zwei vorangegangenen Kapiteln haben wir uns auf Rechen- und Datenstrukturen konzentriert. Die in Rechenstrukturen betrachteten Funktionen sind in der Regel so gewählt, dass sie vornehmlich dem Aufbau und der Zerlegung der Datenelemente dienen. Nun wenden wir uns der Frage zu, wie wir über gegebenen Rechenstrukturen weitere Funktionen in der Form von Programmen durch Rechenvorschriften formulieren. Rechenvorschriften entsprechen Algorithmen. Das bedeutet, dass wir uns nun mit dem Thema der Algorithmen und Berechnung auseinandersetzen. Dabei konzentrieren wir uns in diesem Kapitel auf sogenannte funktionale Programme. Die funktionale Programmierung besteht in der Beschreibung von Funktionen durch Funktionsdeklarationen („Funktionsvereinbarung“). Sie arbeitet mit Programmen, die Funktionen beschreiben und aus Ausdrücken (engl. expressions) und Termen aufgebaut sind. Beherrschendes Sprachelement ist die Termbildung durch Funktionsapplikation. Neben den durch Rechenstrukturen vorgegebenen Funktionen werden weitere Funktionen in der Form von – in der Regel rekursiven – Funktionsdeklarationen eingeführt. Da die Funktionsdeklaration gleichzeitig eine Vorschrift für die Berechnung der Funktionswerte ergibt, sprechen wir von einer Funktionsdeklaration durch Angabe einer Rechenvorschrift. Vereinfacht gesprochen ist eine Funktionsdeklaration durch eine Rechenvorschrift ein speziell geformtes syntaktisches Konstrukt über einer Signatur. Gibt man eine Algebra zu dieser Signatur an, definiert die Rechenvorschrift dann eine Funktion auf den Trägermengen der Algebra. Sind eine Signatur und eine Klasse von dazugehörigen Algebren durch eine algebraische Spezifikation gegeben, so definiert die Rechenvorschrift eine Funktion für jedes Modell der algebraischen Spezifikation. Gleichzeitig definieren die so deklarierten Rechenvorschriften einen Algorithmus, der etwa durch Termauswertung Ergebnisse berechnet. Wir sprechen von operationeller Semantik. Eine Rechenvorschrift definiert also sowohl eine operationelle Semantik, gegeben durch die dadurch definierten Auswertungsregeln, als auch eine „denotationelle“ Semantik, die durch die Funktion f gegeben ist, die durch die Deklaration beschrieben ist. Wichtig ist, © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019 M. Broy, Logische und Methodische Grundlagen der Programm- und Systementwicklung, https://doi.org/10.1007/978-3-658-26302-7_4

187

funktionale Programmierung

Funktionsdeklaration durch Rechenvorschrift

188

4 Funktionale Programmierung

dass operationelle und denotationelle Semantiken übereinstimmen, das heißt, dass durch die Berechnung stets die Werte der Funktion f berechnet werden. Auf die genaue Definition einer Funktionsdeklaration durch Rechenvorschrift und ihrer Semantik gehen wir in den Abschnitten 4.1.1 und 4.2 ein. Beispiel 4.1 (Eine Rechenvorschrift über TREE). Über den Modellen der algebraischen Spezifikation TREE deklarieren wir eine Funktion prefix durch eine Rechenvorschrift. Der Aufruf prefix(t, r) stellt fest, ob der Baum t mit dem oberen Bereich des Baumes r übereinstimmt. fct prefix

= ( t : Tree α, r : Tree α) Bool : if isetree (t) then true else if isetree (r) then false else and( root (t) == root(r), and( prefix ( left (t), left (r)), prefix ( right (t), right (r)))) fi fi

Hierbei nehmen wir an, dass für Einträge der Sorte α das Gleichheitssymbol == : α, α → Bool vorhanden ist. Wie wir zeigen werden, entspricht die Funktionsdeklaration im Sinne der axiomatischen Spezifikation der Einführung einer neuen Funktionsbezeichnung prefix, wobei die damit verbundene Funktion durch eine Gleichung spezifiziert wird. Durch die Deklaration wird gleichzeitig ein Berechnungsverfahren, ein Algorithmus, festgelegt, um die Funktion für beliebige Parameter der angegebenen Sorten zu berechnen. Dazu verwenden wir die Gleichung prefix(t, r) = if isetree(t) then . . . fi

die es uns erlaubt, bei der Auswertung von Termen, die Funktionsaufrufe von prefix enthalten, diese Funktionsaufrufe durch die (geeignet angepasste) rechte Seite zu ersetzen. Es entsteht eine Berechnungsfolge. Anschaulich gesprochen gelten zum Beispiel folgende Aussagen: (1) prefix ist eine totale Funktion – Aufrufe (genauer, Berechnungen) terminieren. (2) prefix definiert eine partielle Ordnung auf der Sorte Tree α. Diese Aussagen kann man erst dann formal beweisen, wenn für die Funktionsbezeichnung prefix eine formale Definition festgelegt wird, also wenn sie an eine bestimmte mathematische Funktion gebunden wird. Im Folgenden werden wir uns mit formaler Semantik solcher Deklarationen beschäftigen.  Obwohl funktionale Programme der mathematischen Beschreibung von Funktionen durch Gleichungen entsprechen, ist es auch für funktionale Programme, wenn sie umfangreich oder kompliziert werden, nicht immer leicht, festzustellen, welche Funktionen sie beschreiben und ob sie das Gewünschte leisten („die richtige Funktion berechnen“) und die erwarteten Ergebnisse liefern. In solchen

4 Funktionale Programmierung

189

Fällen ist es angebracht, die Funktionen mit möglichst klaren Mitteln, etwa der Prädikatenlogik, zunächst zu spezifizieren und die Rechenvorschriften dann auf dieser Basis zu konstruieren und zu verifizieren. Im Folgenden führen wir Konzepte und Methoden ein, die festlegen, welche Funktion durch eine Deklaration definiert wird, sodass wir auf dieser Basis Beweise für Aussagen über funktionalen Programmen führen können. Diese Beweise haben zum Ziel, logische Aussagen über Funktionen, die durch funktionale Programme beschrieben sind, zu konstruieren. Wir erkennen im obigen Beispiel der Deklaration der Funktion prefix unschwer die typischen Konzepte funktionaler Programme: • • • •

Funktionsdeklaration, Funktionsanwendung, bedingte Ausdrücke, Rekursion.

Auch Rekursion und bedingte Ausdrücke durch if_then_else_fi können als spezielle Formen der Funktionsanwendung verstanden werden. Im Folgenden behandeln wir die Konzepte funktionaler Programme im Detail. Beispiel 4.2 (Gleichungsspezifikation von prefix). Wir können die Deklaration der Funktion prefix aus Bsp. 4.1 wie folgt unmittelbar schematisch in eine algebraische Spezifikation überführen. Zunächst geben wir die Funktionalität an: prefix : Tree α, Tree α → Bool Diese ergibt sich unmittelbar aus der Funktionsdeklaration. Dann spezifizieren wir die Funktion prefix rein algebraisch durch die folgenden Gleichungsaxiome prefix(etree, t) = true prefix(cons(l1, i1, r1 ), etree) = false prefix(cons(l1, i1, r1 ), cons(l2, i2, r2 )) = and(i1 == i2, and(prefix(l1, l2 ), prefix(r1, r2 )))

Die algebraische Spezifikation ist eine logische Charakterisierung der durch die Deklaration gegebenen Funktion und enthält keine explizite Berechnungsstrategie, obwohl man die Gleichungen auch als Termersetzungsregeln nutzen kann. Sie entspricht aber einer eins-zu-eins Übersetzung des obigen Programms. Erst die Festlegung einer Auswertungsstrategie, etwa durch Textersetzung, führt auf eine Berechnung. Für die deklarierte Rechenvorschrift existiert eine universelle Form der Textersetzung. Die Rechenvorschrift prefix terminiert demnach immer. Die Axiome beschreiben in diesem Fall eine totale Funktion eindeutig. Bei Rechenvorschriften, die nicht für alle Eingaben terminieren, werden wir dies bei der Überführung in eine algebraische Spezifikation besonders berücksichtigen müssen.  Das Beispiel prefix zeigt, dass aus einer Funktionsdeklaration unmittelbar eine algebraische Spezifikation für die Funktion abgeleitet werden kann. Allerdings

190

4 Funktionale Programmierung

drücken diese Eigenschaften – wie wir sehen werden – die Terminierung und damit die Partialität der betrachteten Funktion unter Umständen unvollständig aus. Wenn die deklarierte Rechenvorschrift für bestimmte Argumente nicht terminiert, wird das durch die Gleichungen in der algebraischen Spezifikation in der Regel nicht explizit erfasst. Es entsteht eine unterspezifizierte Beschreibung der Funktion. Für die Resultate, für die die Funktion terminiert, sind die Werte auch durch die algebraische Spezifikation festgelegt. Solange funktionale Programme für alle Argumente terminieren, ist somit die Umsetzung in algebraische Spezifikationen, und somit Gleichungen, die die Funktionen eindeutig charakterisieren, schematisch möglich. Funktionale Eigenschaften des Programms lassen sich dann allesamt aus der definierten Gleichung und der zugeordneten algebraischen Spezifikation beweisen. Solche Beweise sind vor allem dann von Bedeutung, wenn die Bedeutung und die Korrektheit einer rekursiven Deklaration nicht offensichtlich ist. Ein Beispiel für eine Funktionsdeklaration mit weniger offensichtlich deklarierten Funktionen ist die Quersumme einer Zahl (Bsp. 4.3). Beispiel 4.3 (Rekursive Funktion). Die Funktionsdeklaration fct quer = (n: Nat ) Nat : if n < 9 then n else quer ( quer (n ÷ 10) + mod(n, 10)) fi

beschreibt rekursiv die Rechenvorschrift quer. Ein Aufruf quer(n) mit n ∈ Nat liefert für n in Dezimaldarstellung die iterierte einstellige Quersumme.  Das Beispiel zeigt eine eher seltene Form der Rekursion, die geschachtelte Rekursion, in der im Argument eines rekursiven Aufrufs ein weiterer rekursiver Aufruf auftritt. Der Beweis der Korrektheit kann durch Induktion über die Anzahl der Stellen der Zahl erfolgen.

4.1 Konzepte funktionaler Programmierung Funktionale Programme bestehen in der Regel aus Deklarationen. Im Rumpf der Deklarationen finden sich Terme, die aus einer Reihe von Anwendungen von Funktionssymbolen der Grundfunktionen aufgebaut sind, aus denen sich durch Zusammensetzung (Funktionsapplikation) die gewünschten funktionalen Programme ergeben. Auch die typischen Sprachkonstrukte funktionaler Programme wie bedingte Ausdrücke lassen sich als polymorphe Funktionssymbole algebraisch definieren.

4.1 Konzepte funktionaler Programmierung

191

4.1.1 Funktionsdeklaration Funktionen lassen sich bequem durch Funktionsabstraktionen selbst wieder als Terme darstellen. Damit folgen wir Ideen des λ-Kalküls. Definition 4.1 (Funktionsabstraktion). Sei t ein Term der Sorte Mn+1 mit freien Identifikatoren aus der Menge gesorteter Identifikatoren X, dann ist (x1 : M1, . . . , xn : Mn ) Mn+1 : t ein Term mit freien Identifikatoren aus X \ {(x1, M1 ), . . . , (xn, Mn )}, genannt Funktionsabstraktion, der Sorte M1, . . . , Mn → Mn+1 . Dabei nehmen wir an, dass Mi die Sorte von xi in dem Term t wiedergibt. Die Identifikatoren x1, . . . , xn heißen dann formale Parameter und gebunden in dem Term  (x1 : M1, . . . , xn : Mn ) Mn+1 : t Die Funktionsabstraktion ist also eine spezielle Schreibweise, die auf Terme führt, die Funktionen repräsentieren. Im Folgenden geben wir einige einfache Beispiele für Funktionsabstraktionen. Beispiel 4.4. Über der Spezifikation SEQ kann die Funktion append durch folgende Gleichung über eine Funktionsabstraktion append = (a : α, s : Seq α) Seq α : a ◦ s



spezifiziert werden.

Mit der Funktionsabstraktion mit Sorten über der Signatur Σ und einem Term über Σ (x1 : M1, . . . , xn : Mn ) Mn+1 : t (4.1) verbinden wir für eine gegebene Σ-Algebra A und eine gegebene Belegung β die Festlegung A,β

f : M1, . . . , Mn → Mn+1 mit f (a1, . . . , an ) = t[a1 /x1, . . . , an /xn ] = ϕMn+1 [t] mit β  = β[a1 /x1, . . . , an /xn ]. Dabei seien a1, . . . , an Werte zu den Sorten M1, . . . , Mn .1 Funktionsabstraktionen laut (4.1) fassen wir also als Terme der funktionalen Sorte M1, . . . , Mn → Mn+1 auf. Definition 4.2 (Funktionsdeklaration – Angabe einer Rechenvorschrift). Gegeben sei eine Signatur Σ = (S, F). Eine Funktionsdeklaration durch Funktionsdeklaration A, β 

1 ϕM n+1 bezeichnet die Interpretationsfunktion von Termen der Sorte Mn+1 in der Algebra A mit Belegung β , vgl. Abschnitt 3.1.1.2.

192

4 Funktionale Programmierung

Angabe einer Rechenvorschrift ist ein syntaktisches Konstrukt der Form fct f = (x1 : M1, . . . , xn : Mn ) Mn+1 : t ,

(4.2)

wobei •f  F; • M1, . . . , Mn, Mn+1 ∈ S ; • t ein Term über Σ, dem Funktionssymbol f : M1, . . . , Mn → Mn+1 , den Funktionssymbolen if_then_else_fi : Bool, M, M → M für alle M ∈ S  und den Identifikatoren x1 : M1 , . . . , xn : Mn ist. Durch (4.2) wird das Funktionssymbol f mit der angegebenen Sorte eingeführt („deklariert“) und damit Teil der Signatur. Wir werden später (im Abschnitt 4.2.4) einer solchen Deklaration und einer gegebenen Σ-Algebra eine Funktion zuordnen, die die Gleichung f (x1, . . . , xn ) = t erfüllt. Somit können wir eine rekursive Funktionsdeklaration als Erweiterung einer Rechenstruktur um eine Funktion, spezifiziert durch Gleichungsaxiome, verstehen. Verschränkt rekursive Funktionsdeklarationen lassen sich in der gleichen Form als Erweiterungen der Rechenstruktur um einen Satz von Funktionen verstehen. Funktionsdeklarationen haben durch die damit gegebenen Rechenvorschriften auch einen operationellen, algorithmischen Gehalt. Es wird dadurch ein Verfahren zur Berechnung der Funktion festgelegt. Wie bereits angesprochen, wird durch die Auffassung als Gleichung die Funktion f nicht notwendigerweise eindeutig charakterisiert. Die durch die Deklaration festgelegte Rechenvorschrift hat, wie wir sehen werden, einen induktiven Charakter. Beispiel 4.5 (Nichteindeutigkeit der Gleichungscharakterisierung rekursiver Deklarationen). Wir betrachten ein einfaches Beispiel: fct mod = (x , y: Nat ) Nat : if x 0 gilt, ist durch diese bedingten Gleichungen der Wert von mod(x, y) eindeutig bestimmt, wie man unschwer durch Induktion beweist. Für y = 0 gilt stets ¬(x < y). Damit ist nur die zweite Formel anwendbar und wir erhalten die Gleichung mod(x, y) = mod(x − y, y),

und, da y = 0 ist, erhalten wir mod(x, 0) = mod(x, 0) was eine Trivialität (Tautologie) darstellt. Über den Wert von mod(x, 0) ist durch diese Gleichung nichts festgelegt. Berechnungstechnisch bedeutet das, dass eine Berechnung von mod(x, 0) mit der durch die Deklaration festgelegten Rechenvorschrift nicht terminiert. Betrachtet man lediglich die algebraischen Gesetze für die Funktion mod, so ist über den Wert von mod(x, 0) nichts ausgesagt. Für jede Funktion mod, die die Gleichungen erfüllt, erfüllt auch für jede Zahl z ∈ N die Funktion mod mit

mod  (x, y) falls y > 0  mod (x, y) = z sonst die Gleichungen. Dies zeigt, dass es eine unendliche Menge von Funktionen gibt, die die Gleichungen erfüllen. Die Gleichungen charakterisieren den Wert von mod(x, 0) also nicht. Aus Sicht der Berechnung, die wir mit der rekursiven Deklaration verbinden, gilt: mod(x, 0) terminiert nicht; die Berechnung von mod(x, 0) endet nicht.  Das Beispiel zeigt zwei Probleme der rekursiven Deklarationen von Funktionen auf: (1) Es können bei der Auswertung von Termen nichtterminierende Berechnungen auftreten. (2) Die Gleichungen, die sich aus den Deklarationen ableiten lassen, spezifizieren im Allgemeinen eine Funktion nicht eindeutig. Die Probleme verschärfen sich in bestimmten Fällen sogar zu Widersprüchen, wie folgendes Beispiel zeigt. Beispiel 4.6 (Deklarationsgleichungen und Widerspruchsfreiheit). Nun betrachten wir folgende Funktion für die natürlichzahlige Division: fct

div = (x , y: Nat ) Nat : if x0)) ∧ h(x, y) ≤ i) ⇒ DEF[f(x, y)] zeigen wir ∀ x, y ∈ Nat : ((x⊥y ∧ (x=0 ∨ y>0)) ∧ h(x, y) = i+1) ⇒ DEF[f(x, y)]. Seien also x, y ∈ Nat mit x⊥y ∧ (x=0 ∨ y>0) ∧ h(x, y) = i+1 gegeben. Die Voraussetzung h(x, y) = i+1 liefert die Aussage if x ≤ y then 0 else x − y fi = i+1

Dies ist gleichwertig zu der Aussage x > y∧x−y =i+1

(4.17)

Es gilt somit f(x, y) = if x ≤ y then y else f(x, 2 ∗ y) fi = f(x, 2 ∗ y)

Es reicht also, DEF[f(x, 2 ∗ y)] zu zeigen. Dafür genügt es, die Prämisse h(x, 2 ∗ y) ≤ i

224

4 Funktionale Programmierung

der Induktionsvoraussetzung zu zeigen. Dies ergibt sich aus der Aussage (Entfalten von h(x, 2 ∗ y)) if x ≤ 2 ∗ y then 0 else x − 2 ∗ y fi ≤ i

die wegen 0 ≤ i und (es gilt y > 0 da x  0 ist) x − 2 ∗ y = (y + i + 1) − 2 ∗ y

nach (4.17)

= i + (1 − y) ≤i

da y > 0 und y ∈ Nat

gilt. Somit gilt nach Induktionsvoraussetzung DEF[f(x, 2 ∗ y)] und damit DEF[f(x, y)].  Das angegebene Verfahren läuft auf den Beweis hinaus, dass in allen rekursiven Aufrufen f (x1, . . . , xn ) im Rumpf t die Werte der Funktion h(x1, . . . , xn ) streng monoton abnehmen. Beispiel 4.21 (Aufsummieren der Inhalte von Bäumen (Terminierungsbeweis)). Wir betrachten die Rechenvorschrift fct sum = ( t : Tree Nat , s : Seq Tree Nat ) Nat : if isetree ( t ) then if iseseq (s) then 0 else sum( first (s) , rest (s) ) fi else root ( t ) + sum( left ( t ) , s ◦  right ( t ) ) fi

Behauptung: sum(t, s) terminiert stets, d. h. (DEF[t]∧ DEF[s]) ⇒ DEF[sum(t, s)]. Beweis: Als Prädikat des Terminierungsbereichs wählen wir p mit def

p(t, s) ⇐⇒ (DEF[t] ∧ DEF[s]) (für s ∈ Tree Nat, t ∈ Seq Tree Nat) und als Abstiegsfunktion h : Tree Nat, Seq Tree Nat → Nat ' (t, s) → 2 ∗ hs(s[i]) + #s + 2 ∗ hs(t) i