functional programming in pythonhg51/veranstaltungen/npp/npp.pdfth letschert, fh giessen–friedberg...

167
Nichtprozedurale Programmierung in Python und anderen Sprachen Thomas Letschert FH Giessen–Friedberg Version 1.0 vom 16. Juni 2012

Upload: others

Post on 24-Jan-2021

0 views

Category:

Documents


0 download

TRANSCRIPT

  • Nichtprozedurale Programmierungin Python und anderen Sprachen

    Thomas LetschertFH Giessen–Friedberg

    Version 1.0 vom 16. Juni 2012

  • Inhaltsverzeichnis

    1 Funktionale Programme 1

    1.1 Prozedurale und Nicht–Prozedurale Programmierung . . . . . . . . . . . . . . . . . . . . . . . . 5

    1.1.1 Prozedurale Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

    1.1.2 Deklarative Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

    1.1.3 Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

    1.1.4 Compilierte und Interpretierte Sprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

    1.2 Ausdrücke und ihre Auswertung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

    1.2.1 Definitionen und Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

    1.2.2 Statische und Dynamische Bindung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

    1.2.3 Statische und Dynamische Typisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

    1.2.4 Kopiersemantik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

    1.2.5 Funktionen und ihre Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

    1.2.6 Auswertung von Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

    1.2.7 Weitere Notationen und Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

    1.3 Funktionales Programmieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

    1.3.1 Algorithmenschemata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

    1.3.2 Numerisches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39

    1.3.3 Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

    1.3.4 Daten und Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

    1.3.5 Datenabstraktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

    1.3.6 Verwaltung von Zustandsvariablen im funktionalen Stil . . . . . . . . . . . . . . . . . . . 51

    1.4 Funktionen in OO–Sprachen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

    1.4.1 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

    1.4.2 Funktionen als Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

    1.4.3 Funktionale Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

    1.4.4 Dynamisch erzeugte Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60

    1.4.5 Konstruktion Funktionaler Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63

    1.4.6 Closures, Funktionale Objekte und Bindungen . . . . . . . . . . . . . . . . . . . . . . . 67

    1.5 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71

    1.5.1 Rekursion, Induktion, Iteration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71

    1.5.2 Rekursive Daten und rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . 73

    2

  • Th Letschert, FH Giessen–Friedberg 3

    1.5.3 Klassifikation der Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

    1.5.4 Fortsetzungsfunktionen und die systematische Sequentialisierung . . . . . . . . . . . . . 81

    1.5.5 Entwicklung Rekursiver Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86

    1.6 Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

    1.6.1 Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

    1.6.2 Iteratoren in Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94

    1.6.3 Bäume und Baum–Besucher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95

    1.6.4 Bäume und Baum–Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

    1.7 Generatoren und Datenströme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102

    1.7.1 Generatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102

    1.7.2 Generator–Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

    1.7.3 Implementierung von Generatoren durch Threads . . . . . . . . . . . . . . . . . . . . . . 107

    1.7.4 Datenströme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114

    2 Relationale Programme 118

    2.1 Sprachen mit Mustererkennung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119

    2.1.1 Reguläre Ausdrücke: Reguläre Textstrukturen erkennen . . . . . . . . . . . . . . . . . . . 119

    2.1.2 XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

    2.2 Suche, Nichtderminismus, Relationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

    2.2.1 Mustererkennung als Suchproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

    2.2.2 Nichtdeterministische Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139

    2.2.3 Relationale Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

    2.3 Logik–Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144

    2.3.1 Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144

    2.3.2 Unifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150

    2.3.3 Prolog–Interpreter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

    2.4 Constraint Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

    2.4.1 Programmieren mit Beschränkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

    2.4.2 Lösungssuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

  • Kapitel 1

    Funktionale Programme

    1

  • 2 Nichtprozedurale Programmierung

    “Sie sind doch geschickt darin, Wörter zu erklären, Herr Goggelmoggel”,sagte Alice. “Können Sie mir freundlicherweise sagen,

    was das Gedicht Der Zipferlake bedeutet?”

    “Nur heraus damit”, sagte Goggelmoggel“Ich kann alle Gedichte erklären, die jemals gedacht worden sind –

    und noch eine ganze Menge, bei denen das Erdenken erst noch kommt.”

    Lewis Carroll, Alice hinter den Spiegeln

    Vorbemerkung

    Die Entwicklung der Programmiersprachen wurde in der Vergangenheit stets aus zwei Richtungen beeinflusstund vorangetrieben. Zum einen von der Seite der Hardware, zum anderen von der Seite der Theorie mit ihremNachdenken darüber, was denn ein Programm eigentlich ist und was es ausdrücken soll. In den frühen Jahren derInformatik war der Einfluss von Seiten der Hardware absolut dominant. Die ersten Programmiersprachen warennichts anderes, als das Instruktionsrepertoir bestimmter Maschinen und sie sollten nichts anderes zum Ausdruckbringen können, als deren elementare Operationen; und die waren und sind prinzipiell recht einfach. Die Maschi-nen hatten bestimmte prinzipielle Charakteristika, an denen sich im Laufe der Jahre wenig änderte: im Regelfallhat man es mit Hardware zu tun, die heute, so wie vor 50 Jahren, der sogenannten “Von–Neumann–Architektur”entspricht, und deren Funktionsweise sich über viele Abstraktionsstufen in den Programmiersprachen und Pro-grammen wiederfindet.

    Vektorrechner, Parallelrechner, verteilte Systeme und Ähnliches an Hardwarestrukturen drängt allerdings zuneh-mend nach vorn und es ist zumindest zweifelhaft, ob der von–Neumann–Rechner, als Grundmuster eines Rechnersund der Programmierung, für alle Zeiten ohne ernsthafte Alternativen bleiben sollte. Schon heute folgt also dieHardware gar nicht mehr so recht dem klassischen Vorbild und es kommt vor, dass die Programmierer den Par-allelismus einer Problemstellung beim Verfassen eines sequenziellen Programms mühsam eliminieren und diesesProgramm dann aufwändig vom Compiler in ein Maschinenprogramm übersetzt wird, das den Parallelismus derHardware auszunutzen versucht. Da wäre es vielleicht doch angemessen die parallele Problemlösung direkt aufdie parallele Hardware zu bringen.

    Ein Rechner der klassischen Bauart besteht, sehr abstrakt gesehen, aus einer Menge von Speicherplätzen, deren In-halt durch Befehle modifiziert, hin– und herkopiert sowie von externen Quellen eingelesen und dorthin ausgegebenwerden kann. Die Modifikation ist dabei bei einer “Von–Neumann–Architektur” auf Register beschränkt, also aufsehr wenige schnelle Speicherstellen innerhalb der CPU. Sie werden von einem Maschinenprogramm zyklisch mitWerten aus den anderen Speicherstellen im Hauptspeicher “geladen”, ihr Inhalt wird verarbeitet und das Ergebniswieder gespeichert.

    Während die Techniker mit Programmen als Sequenzen von Lade–, Verarbeite– und Speicherbefehlen zufriedenwaren, wollten die Anwender sich von der Maschine lösen und Notationen nutzen, die der Problemwelt ange-messener sind, d.h. sie wollten in “höheren Sprachen” programmieren. Das älteste Hilfsmittel der höheren Pro-grammierung ist die Prozedur. Bereits in den 40–er Jahren entwickelten Computerpioniere wie Turing und Zuseunabhängig von einander Programmiertechniken, bei denen größre Aufgaben mit Hilfe von speziellen oder stan-dardisierten Unterroutinen gelöst wurden. Eine Unterroutine, eine Prozedur, war dabei nichts anderes, als eineSequenz von Maschinenanweisungen. Auch wenn die Programmierung mit Hilfe von Prozeduren erheblich ver-einfacht wurde, so blieben die Programme doch weiterhin bis weit in die 50–er (jetzt strukturierte) Sequenzenvon Maschinen–Anweisungen. Es waren und blieben rein prozedurale Programme: Programme deren Inhalt dieBeschreibung einer Folge von Speicherplatzmanipulationen darstellt.

    Nach und neben den Prozeduren wünschten sich die Autoren der frühen Programme, dass sie mathematischeBerechnungen in der gewohnten Art durch Formeln ausdrücken können. FORTRAN FORmula TRANslator bot,als erste erfolgreiche Programmiersprache, die Möglichkeit arithmetische Ausdrücke in mathematischer Notati-on zu verwenden. Mit diesen “Formeln” enthielten FORTRAN–Programme Elemente die definitiv nicht von derHardware und ihren Konzepten hergeleitet waren, sondern aus dem Problembereich kamen. Die erste “höhere”Programmiersprache war geboren und sie enthielt mit den arithmetischen Ausdrücken, den “Formeln”, ein nicht-

  • Th Letschert, FH Giessen–Friedberg 3

    prozedurales Element.

    Gleichzeitig mit der Entwicklung von FORTRAN, also noch in den 50–ern, konzipierte der Visionär McCarthyeine Programmiersprache namens LISP (LISt Processor) die den Weg, weg von der Maschine, hin zu einer pro-blemorientierten Formulierung von Programmen, weit radikaler ging. LISP–Programmierer wollten sich nicht nurdem damals üblichen Geschäft des Lösens schnöder Rechenaufgaben widmen. Ihr Anspruch war weit umfassen-der: “Wissen” sollte dargestellt und verarbeitet werden, die Maschinen sollten “intelligent” werden. Die “wissen-den” und “denkenden” Programme waren zunächst nichts anderes als Programme, die sich nicht wie damals aufdie Manipulation numerischer Werte beschränkten, sondern allgemeine Datenstrukturen mit alphanumersichenWerten verarbeiteten. Die grundlegende Datenstruktur in LISP war und ist immer noch der, “Liste” genannte,Binärbaum. Eine gleichzeitig mächtige wie allgemeine und einfache Datenstruktur. Aus heutiger Sicht ist die Idee,mit Datenstrukturen und Zeichenketten zu arbeiten, eher trivial und die Verknüpfung dieser Idee mit Intelligenzund Wissen klingt nach Hybris und Größenwahn. Man sollte jedoch bedenken, dass die Idee einer Datenstruk-tur damals völlig neu war und erst die Perspektive eröffnete, Computer zu anderem als zum Rechnen oder demVerwalten von Inventarlisten auf Magnetbändern zu verwenden.

    Neben der Innovation der Liste, als Datenstruktur ohne direke Entsprechung in der Hardware, demonstrierte Mc-Carthy, dass sich auch die Kontrollstruktur der Programme von den Vorgaben der Maschine lösen kann. In Lispgeben die von der Mathematik inspirierten Funktionen den Takt der Programme an. Variablen, Anweisungen,Schleifen, Sprünge sind bestenfalls noch als unterprivilegierte Kellerkinder geduldet. Wegen dieser Bedeutung derFunktionen werden LISP und seine Verwandten funktional genannt. Sie waren von Anfang an in engem Kontaktzur künstlichen Intelligenz und blieben es über die Jahre.

    In der Folge kam es dann zu einem permanenten Wettstreit zwischen den Verfechtern des prozeduralen Stilsmit ihrem von “der Maschine” abgeleiteten Konzept eines Programms und den Verfechtern alternativer, nicht–prozeduraler Programmierstile, die dafür plädieren, dass Programme in einer “natürlichen” und dem “Problemangemessenen” Form formuliert werden sollten. Mit den intellektuellen Großwetterlagen schwankte der Zeitgeistzwischen den Lagern. Als in den 80–ern die damals dominierende Technologie–Macht Japan die Entwicklung vonComputern der “fünften Generation” ankündigte, die ungeahnte Dinge mit Konzepten der künstlichen Intelligenzbewerkstelligen sollten, hatte auch die funktionale Programmierung ein Jahrhundert–Hoch. Künstliche Intelligenzund funktionale Sprachen entstammen schließlich dem gleichen intellektuellen Umfeld. Die Vorstellung geheim-nisvoller asiatischer Supercomputer und Roboter ließ amerikanische und europäische Politiker Unmengen vonFördergeldern in die aride1 Landschaft der KI-Forscher streuen.

    Eine etwas bescheidenere Blüte hatten die funktionalen Sprachen kurz vorher, Ende der 70–er. Es ging dabei nicht,wie bei der KI, um grandios–schaurige Visionen, sondern um tröge Programm–Korrektheit. Nach einer, damalsschon seit Jahren anhaltenden “Software–Krise”, mit Kuren wie Programmverifikation, systematischem Program-mieren, abstrakten Datentypen, Spezifikationssprachen, etc. wurden immer noch schlechte und fehlerhafte Pro-gramme geschrieben. Nun, so meinten viele, war es Zeit für eine Radikalkur: Die alten schlechten Programmier-methoden können nur dann mit Stumpf und Stiel ausgerissen werden, wenn die Quelle allen Übels, der prozeduraleStil, eliminiert wird. Programme haben dann die Reinheit und Klarheit mathematischer Formeln. Von diesen weißjeder, der niemals ernsthaft Mathematik betrieben hat, dass sie von allen kompetenten Wissenschaftlern schnellund sicher in richtig und falsch geschieden werden können. Wenn Programme wie mathematische Funktionengeschrieben werden, dann kann man endlich jeden Fehler mehr oder weniger sofort entdecken. In diesem Argu-ment steckt ein wahrer Kern, aber zu glauben, dass die Abschaffung der Zuweisung letztlich alle Probleme desSoftware–Engineering löst, ist schlicht lächerlich – wenn auch nicht wesentlich lächerlicher als der Gedanke, dassdie allgemeine Verwendung von UML die ultimative Lösung aller Probleme darstellt.

    Mit den Kursen japanischer Aktien schmolz die Begeisterung für KI und damit für funktionale Programmierungdann dahin. Im trockenen Wind von Prädikatenlogik, Lambda–Kalkül und Gödelschen Beweistechniken verdorr-ten die Förderoasen der KI nach dem Ausbleiben der Subventionswässer. Die Software-Techniker fanden mit derObjekt–Orientierung auch schnell ein Betätigungsfeld mit höherem Sexappeal als Korrektheitsargumente bietenkönnen. Die funktionale Sicht der Welt, mit Programmen als Folgen von Eingabe, Verarbeitung und Ausgabe, warauch jetzt einfach von altmodischer Beschränktheit im Vergleich zu den moderneren Konzepten der Verteiltheitoder der reaktiven Programmierung. In der Softwaretechnik wurden die mit dem funktionalen Stil verbundenen undkurz zuvor noch hochgelobten “strukturierten Techniken” von objektorientierter Analyse und Entwurf abgelöst und

    1 arid = “trocken”, aus dem Lateinischen, botanische Bezeichnung für den Charakter von Wüsten und Halbwüsten.

  • 4 Nichtprozedurale Programmierung

    zu Weisheiten graubärtiger alter Männer. Der für die Informatik so typischen maßlosen Überschätzung von damalsstand plötzlich und bis heute andauernd eine ebenso typische übertriebene Ignoranz gegenüber. Alle “abweichen-den” Konzepte werden von der aktuellen Inkarnation des prozeduralen Stils, der prozeduralen Programmierung,der Objektorientierung, plattgewalzt. Konzepte, die zwar nicht die ultimative Lösung aller Probleme bringen, abereinen wichtigen Bestandteil des Wissens– und Erfahrungsschatzes der Informatik darstellen, werden weitgehendignoriert.

    FORTRAN, die erste erfolgreiche höhere Programmiersprache umfasste interessanterweise gleichzeitig funktiona-le und prozedurale Ausdrucksstile. Es gab Ausdrücke, die uns heute so selbstverständlich als Programmelementesind, aber damals einen revolutionären Bruch im Programmierstil darstellten. Ebenso gab es die damals gewohntenAnweisungen: Zuweisungen, Sprünge und bedingten Anweisungen. Lisp und seine Verwandten und Nachfolgermit rein funktionalem Charakter führen und führten schon immer ein Randdasein. Umgekehrt ist die Vorstellung,Anwendungen in rein prozeduralen Sprachen zu schreiben, also beispielsweise ohne Ausdrücke, völlig absurd. Ra-dikale Positionen sind im Streit der Programmierstile so unsinnig wie auch sonst im Leben. Die Objektorientierungist heute zweifellos das dominierende Konzept. Funktionale Programmiertechniken sind keine Konkurrenz zur Ob-jektorientierung. Die Konzepte sind orthogonal.2 Beide Stile liefern komplementäre Denkmuster von bleibendemWert.

    Der Autor dankt allen kritischen Lesern des Skripts und Teilnehmern der Veranstaltung, für die vonihnen aufgedeckten Unrichtigkeiten, Nachlässigkeiten und Missverständlichkeiten und, noch mehr,für ihren nüchtern skeptischen Enthusisasmus mit dem sie zu Inhalt und Qualität der Veranstaltungbeigetragen haben.

    2 “Orthogonal” bedeutet senkrecht zueinander. Im übertragenen Sinn sind orthogonale Konzepte solche, die sich nicht gegenseitig in dieQuere kommen.

  • Th Letschert, FH Giessen–Friedberg 5

    1.1 Prozedurale und Nicht–Prozedurale Programmierung

    Computer science is no more about computersthan astronomy is about telescopes.

    E.W. Dijkstra

    1.1.1 Prozedurale Programmierung

    Variablen und Werte

    Die Welt besteht aus Dingen, die sich im Laufe der Zeit verändern können. Auf dieser Grundüberzeugung beruhtdie prozedurale Programmierung. In einem einfachen prozeduralen Programm sind die veränderlichen Dinge dieVariablen. Sie werden von Anweisungen verändert. Wer eine prozedurale Sprache erlernt, muss als erstes diesesKonzept der Variablen und Anweisungen verstehen. Die meisten von uns haben nach Jahren der Programmierer-fahrung vergessen, dass dies mit einer gewissen Anstrengung verbunden war. Je erfolgreicher der Mathematikun-terricht war, umso fester hat sich im Bewußtsein des Programmierlehrlings festgesetzt, dass eine Variable einenWert bezeichnet. Im Programmierunterricht muss dann wieder umgelernt werden: Das mathematische Konzept derVariablen muss ersetzt werden durch Variablen, deren Wert vom Zustand des Programms abhängt.

    a

    a

    2.0

    2.0

    mathematische Variable

    Variable in einem prozeduralen Programm

    Abbildung 1.1: Variablen mit ein- und zweistufiger Wertzuordnung

    Dies wird meist mit dem “Behälterkonzept” der Variablen erläutert (siehe Abbildung 1.1).

    • In der Mathematik sind Variablen Namen für Werte.

    • In (prozeduralen) Programmen sind Variablen die Namen von Behältern (Speicherplätzen) die mit Wertenbelegt sind.

    Damit wird die Beziehung von Namen zu Werten zweistufig. Ein Name bezeichnet einen Speicherplatz und dieserist mit einem Wert belegt (siehe Abbildung 1.2). Die Beziehung von Namen zu Speicherplätzen ist relativ fest. Dievon Speicherplätzen zu Werten ändern sich häufig und schnell.

    a 2.0

    Behälter WertName

    Variable in einem prozeduralen Programm

    Wert

    a 2.0

    Name

    mathematische Variable

    Abbildung 1.2: Ein– und zweistufige Zuordnung von Werten

  • 6 Nichtprozedurale Programmierung

    Deklarationen ändern die Bedeutung von Namen. Anweisungen ändern die Belegung von Speicherplätzen. Dieaktuelle Belegung der Speicherplätze nennt man oft (Programm–) Zustand. Anweisungen transformieren damitden Zustand und ein Programm ist eine Beschreibung von möglichen Zustandsänderungen (siehe Abbildung 1.3).

    a −> 1b −> 3

    a −> 4b −> 2

    if (a>0)a=a+b; b=a/2;

    Zustand Zustandprozedurales Programm

    Abbildung 1.3: Das Grundkonzept eines prozeduralen Programms

    Prozeduren

    Komplexe Zustandsänderungen, also komplexe Programme, werden als Sequenzen elementarer Zustandsänderun-gen, den Anweisungen, aufgebaut. Dabei können beliebige Teilsequenzen zu Prozeduren zusammengefasst wer-den, die dann selbst wieder als Bestandteile komplexerer Prozeduren dienen können. Dieses Prinzip der prozedura-len Abstraktion bildet die Basis aller klassischen prozeduralen Sprachen. Ein Programm beschreibt das Verhalteneiner Maschine als Übergang von einem Ausgangs– zu einem Endzustand. Ein Programm gliedert sich dabei inUnterprogramme (Prozeduren), Unter–Unterprogramme und so weiter. Das Gesamtprogramm kann man dann alshypothetische Maschine verstehen, deren Ausgangszustand sich abhängig von der Eingabe in einen Endzustandtransformiert (siehe Abbildung 1.4). Das Gesamtprogramm entspricht einer hypothetischen Maschine, deren Ver-halten von der realen Maschine auf Basis ihrer Beschreibung – des Programms – simuliert wird.

    a −> 4b −> 2

    if (a>0)a=a+b; b=a/2;cout >> b;

    if (a>0)a=a+b; b=a/2;cout >> b;

    cin >> a >> b;

    Programmlauf mit Eingabe 1 und 3

    cin >> a >> b;

    a −> 0b −> 0 Programm im Ausgangszustand

    Programm im Endzustand

    Abbildung 1.4: Prozedurale Programme als hypothetische Maschinen

    Ein prozedurales Programm definiert ein Objekt

    Das Gesamtprogramm definiert damit ein Objekt: Etwas mit einem Zustand und einem Verhalten. Der Zustandist die aktuelle Variablenbelegung. Das Verhalten ist die Veränderung des Zustands bei Eingabe eines Wertes.Das Objekt “Gesamtprogramm” ist in der klassischen prozeduralen Programmierung aus Prozeduren aufgebaut.Prozeduren sind als “Verhaltensmuster” zu verstehen.

    Die prozedurale Programmierung hat den großen Vorteil, sehr nahe an der realen Maschine zu sein. Jeder Rechnerist ein physikalisches Objekt, das sich bei Programmstart in einem bestimmten Zustand befindet, der sich im Laufeder “Rechenarbeit” verändert. Das Konzept einer Maschine, die sich in wechselnden Zuständen befindet, ist sehrintuitiv. Der Mensch bewegt sich seit Jahrtausenden in einem Umfeld von Objekten mit Zustand und Verhalten.

  • Th Letschert, FH Giessen–Friedberg 7

    Objektorientierte Programme: Objekte mit Unterobjekten

    In der klassischen Programmierung ist nur das Gesamtprogramm eine Maschinenbeschreibung, also ein Objekt.Seine Komponenten, die Prozeduren, sind dagegen Verhaltensbeschreibungen. In der objektorientierten Program-mierung wird die Intuition der Maschine noch weiter getrieben. Nicht nur das Gesamtprogramm ist jetzt eineMaschine, auch die Teile sind Maschinen. Das Programm ist ein Objekt: eine “Maschine” mit einem bestimmtenVerhalten – die sich selbst wieder aus Objekten, den “Untermaschinen” zusammensetzt. Die Objektorientierungist damit der ultimative Höhepunkt der prozeduralen Programmierung: Programme sind Objekte, Dinge mit einemZustand und einem Verhalten das sich beim Einfluss äußerer Einwirkungen als Änderung dieses Verhaltens zeigt.Das Programm und all seine Objekte können selbst wieder aus Objekten zusammengesetzt sein.

    1.1.2 Deklarative Programmierung

    Funktionale Programmierung

    Die prozedurale Programmierung hat das Konzept der Maschine verwendet und, in sukzessiver Ablösung und Ent-fernung von der realen Grundlage der Hardware, weiterentwickelt zum Paradigma eines Programms und seinerKomponenten als hypothetischen Maschinen (Objekten). Die Entwicklung der nicht–prozeduralen Sprachen hatdagegen gleich in den luftigen Höhen der Abstraktion mit der Überzeugung begonnen, dass eine höhere Program-miersprache vollständig frei sein sollte von allen Bezügen zu einem realen Rechner und seinem Maschinencode.

    Es war naheliegend die gesuchte universelle “maschinen–ferne” Notation bei den reichhaltigen und teilweise jahr-hundertelang erprobten Ausdrucksmöglichkeiten der Mathematik zu suchen. Als erstes wurden arithmetische For-meln wie etwa 2 + a ∗ (b − c) als elegante, übersichtliche und bewährte Art entdeckt, Rechenvorschriften zuformulieren. Das Problem, derartige Formeln in elementare Maschinenbefehle zu übersetzen, war bereits Anfangder 50–er gelöst und bald erschienen die ersten höheren Programmiersprachen, in denen arithmetische Ausdrückein Zuweisungen verwendet werden konnten. Sprachen deren Programme also den direkten Bezug zum Maschinen-code verloren hatten und nur nach einer komplexeren Übersetzung oder durch einen Interpreter ausführbar waren.Prominentester Vertreter dieser Sprachen ist FORTRAN mit den ersten Implementierungen ab Mitte der 50–er.

    Arithmetische Ausdrücke lassen sich nahtlos in prozedurale Programme einbetten. Sie wurden darum anstands-los akzeptiert. Funktionen als primäres Ausdrucksmittel einer Programmiersprache taten sich etwas schwerer.Das praktisch gleichzeitig mit FORTRAN konzipierte LISP hatte Funktionen und Ausdrücke als einzige Aus-drucksmittel und Paare (Binärbäume) als einzige Datenstruktur.3 Es ist erstaunlich, dass dieses minimalistischeSprachkonzept ausreicht, komplexeste Programme zu schreiben. Weniger erstaunlich ist, dass sich nicht jeder fürdiesen Minimalismus begeistern kann. Die Abwesenheit von Schleifen beispielsweise erzwingt den Einsatz vonRekursion als Ersatz in einem Ausmaß, das die Produktivität nicht jedes Programmierers steigert.

    Ein Beispiel für ein funktionales Programm in Lisp ist:

    (define (quadrat x) (* x x)) -- Def. Funktion quadrat(define (quadratsumme x y) (+ (quadrat x) (quadrat y))) -- Def. Funktion quadratsumme(quadratsumme 3 4) -- Funktionsaufruf

    Dieses Programm liefert den Wert 25 (25 = 3 ∗ 3 + 4 ∗ 4 = quadrat(3) + quadrat(4) = quadratsumme(3, 4))Zuerst wird die Funktion quadrat mit Parameter x und quadratsumme mit den Parametern x und y definiert,dann wird die zweite Funktion auf 3 und 4 angewandt. Lisp verwendet eine irritierende Fülle an Klammern undeine nicht wesentlich weniger irritierende Prefix–Notation bei Operatoren und Funktionsaufrufen. So hätte manstatt

    (+ (quadrat x) (quadrat y)) (Prefix–Notation)

    in anderen Sprachen sicher

    quadrat(x) + quadrat(y) (übliche Aufruf– und Infix–Notation)

    geschrieben. Diese Extravaganzen sind nicht essentiell für das funktionale Programmieren. Sie haben vielmehr mit

    3 LISP war allerdings die erste Sprache die überhaupt eine Datenstruktur zu bieten hat.

  • 8 Nichtprozedurale Programmierung

    der Philosophie von Lisp zu tun, alle Daten, auch die Programme selbst, als Listen darstellen zu wollen, sowie derPriorität der Einfachheit der Analyse gegenüber der Lesbarkeit.

    Sieht man von Dogmatikern der Objektorientierung ab, dann ist das Konzept der (freien) Funktionen allgemeinbekannt und wird oft eingesetzt. Funktionale Sprachen werden darum allgemein nicht als etwas angesehen, dasneue Ausdrucksmöglichkeiten bietet, sondern als Beschränkung, als etwas Mangelhaftes, dem es an bekannten undguten Ausdrucksmitteln fehlt. Variablen sind wie in der Mathematik Bezeichner für Werte und nicht wie üblich“Behälter” für Werte. Als Konsequenz gibt es keine Zuweisung. Der Begriff eines sich ändernden Zustands desGesamtprogramms oder seiner Teil–Objekte ist den mathematisch orientierten funktionalen Sprachen fremd. Dasschlägt sich nieder als Fehlen von Anweisungssequenzen, speziell als Fehlen von Schleifen. Man kann versuchendiesen Mangel als Vorteil zu verkaufen. Das Fehlen des Zustandskonzepts macht die Analyse von Programmeneinfacher und erhöht so die Chance korrekte Programme zu schreiben. Wirklich überzeugend ist dieses Argumentaber nicht.

    Relationale Programmierung: Programmieren mit Prädikation und Relationen

    Die Definition und Verwendung von Funktionen als Mittel zur Formulierung von Algorithmen ist intuitiv einsich-tig, wenn auch etwas gewöhnungsbedürftig. Vor allem, wenn sie das einzige Ausdrucksmittel ist und dazu noch,wie in Lisp, alles konsequent vollständig geklammert und in Prefix–Notation zu verfassen ist. Bei der logischenProgrammierung wird die Sache etwas exotischer. Statt Funktionen werden Ausdrücke der Prädikatenlogik ausder Mathematik übernommen und als algorithmische Notation verwendet. Nehmen wir als Beispiel den Satz desPythagoras:

    a2 + b2 = c2

    hier wird eine Beziehung (Relation) zwischen a, b und c definiert. Diese Beziehung kann als Prädikat pythagorasaufgefasst werden:

    pythagoras(a, b, c) gdw. a2 + b2 = c2

    So, wie eine Funktion in einer funktionalen Sprache (aber nicht nur dort) benutzt werden kann, um einen Wert zuberechnen, z.B. den Wert in Lisp:

    (quadratsumme 3 4) -> 25

    so kann ein Prädikat dazu benutzt werden, um den Wahrheitsgehalt einer Aussage zu testen, z.B. in einer logischenSprache wie PROLOG (PROgramming in LOGic):

    phytagoras(3,4,5) -> true

    Das Prädikat wird hierbei wie eine Funktion mit Booleschem Ergebnis verwendet. Der Aufruf wird etwas interes-santer, wenn eines der Argumente eine Variable ist. Das System sucht dann eine Belegung der Variablen, für diedie Aussage wahr wird. Z.B:

    phytagoras(3,4,c) -> c = 5

    aber auch:

    phytagoras(3,b,5) -> b = 4

    und sogar

    phytagoras(a,b,5) -> a = 3, b = 4

    Bei logischen Programmen sucht das System also nicht, wie bei einer Funktion, ausgehend von den Argumen-ten “vorwärts” nach dem Wert, sondern versucht “rückwärts” vom Ergebnis Variablenbelegungen zu finden, fürdie die Aussage wahr ist. Die logische Programmierung ist somit in gewisser Weise eine Verallgemeinerung derfunktionalen Programmierung.

    Bei der funktionalen und der logischen Programmierung werden Algorithmen nicht direkt angegeben. Man de-klariert statt dessen gewisse Sachverhalte, eine Funktion oder ein Prädikat. Logische und funktionale Sprachennennt man darum auch deklarative Sprachen. Sie gelten oft als “höherwertig”, weil in ihnen nicht das Wie einerBerechnung, sondern das Was angegeben wird.4 Im Gegensatz dazu werden bei der prozeduralen Programmierung

    4 Die Grenzen zwischen Was und Wie sind letztlich natürlich fließend.

  • Th Letschert, FH Giessen–Friedberg 9

    Algorithmen in Form zustandverändernder Anweisungen angegeben. Prozedurale Sprachen heißen darum auchimperativ (= befehlend).

    prozedurale Sprachen(imperative Sprachen) (deklarative Sprachen)

    nichtprozedurale Sprachen

    klassisch prozedural objektorientiert funktional logisch

    Programmiersprachen

    z.B. C++z.B. C z.B. Prologz.B. Lisp

    . . . andere deklarative Stile . . .

    Abbildung 1.5: Klassifikation der Programmiersprachen

    SQL: Programme beschreiben Relationen

    Eine deklarative Programmiersprache, die in der Bedeutung sowohl Lisp als auch Prolog bei weitem übertrifft istSQL. In SQL werden Relationen durch Selektion (WHERE), Projektion (SELECT), Vereinigung (JOIN) aus ande-ren Relationen gebildet. Die Konstruktion von Relationen (Tabellen) wird dabei nicht algorithmisch in Schleifenund Zuweisungen ausprogrammiert, sondern in Form von SQL–“Anweisungen” deklariert. Mit einem Ausdruckwie etwa

    SELECT StudentId, NoteFROM KlausurErgebnisseWHERE Kurs="NPP"

    wird eine Relation auf eine andere abgebildet. Man deklariert das gewünschte Ergebnis ohne die zu dessen Be-rechnung notwendige Schleife über komplexen Datenstrukturen explizit (prozedural) anzugeben. Das Konzept derrelationalen Datenbanken und SQL sind sicherlich bekannt. Wir gehen darum nicht weiter darauf ein.

    SQL basiert auf der Theorie relationaler Datenbanken. Beides, SQL und die relationalen Datenbanken, sind in-nerhalb der Informatik mit die wichtigsten Belege dafür, dass nichts so praktisch ist wie eine gute Theorie. Impraktischen und alltäglichen Umgang mit Datenbank–Tabellen vergisst man es leicht, aber relationale Datenbankenbasieren auf dem klaren, mathematisch orientierten Konzept der Relationen–Algebra, das vor der Implementierungund dem praktischen Einsatz entwickelt wurde.5

    XSLT: Programme definieren Baum-Muster und ihre Transformationen

    Neben Lisp, Prolog und SQL wurden und werden weiterhin eine Vielzahl deklarativer Sprachen definiert. In letzterZeit machen speziell XML–Technologien von sich reden. Ein XML–Dokument wie etwa folgendes

    Busch Wilhelm

    Langeweile

    Guten Tag Frau Eule! Habt Ihr Langeweile? -

    5 Eine Vorgehensweise, die trotz (wegen ?) der inflationären Vermehrung wissenschaftlich ausgebildeter Informatiker leider vollkommenaus der Mode gekommen ist.

  • 10 Nichtprozedurale Programmierung

    Ja eben jetzt, solang Ihr schwaetzt.

    stellt einen Baum dar (siehe Abbildung 1.6).

    TextName

    Text Text

    strophe

    Gedicht

    autor

    vorname

    titel

    zeile

    Text Text Text Text

    zeile zeile zeile

    Abbildung 1.6: XML Dokument als Baum

    Im Fall von SQL haben wir es mit Ausdrücken zu tun, die Relationen verarbeiten und zu neuen Relationen ver-knüpfen. XML–Dokumente sind hierarchisch strukturierte Informationen – kurz Bäume.6 Die Verarbeitung vonBäumen, Baumtransformationen also, kann man ebenso wie die von Relationen deklarativ beschreiben. Ein Pro-gramm in der deklarativen Sprache XSLT, das eine XHTML–Version des Gedichts von oben erzeugt ist:

    Gedicht

    .... etc ....

  • Th Letschert, FH Giessen–Friedberg 11

    Ohne zu sehr ins Detail gehen zu wollen, sehen wir hier eine Folge von Baum-Mustern. Das letzte Muster ist

    Es passt zu allen Zeilen–Knoten und definiert eine Aktion auf diesen Knoten, hier die Ausgabe von

    Inhalt des Zeilen–Knotens

    Das XSLT–Programm insgesamt beschreibt ein Durchwandern des XML–Baums mit einer Aktivierung dieserMuster. Es ist an dieser Stelle nicht notwendig diesen Mechanismus im Detail zu verstehen. Man sieht aber, dassein XSLT–Programm eine komplexe Datenstruktur durchwandert. Dies wird nicht mit prozeduralen Elementen,Schleifen, Variablen, Zeigern, etc. programmiert, sondern durch einem System von Baummustern mit zugehörigenAktionen. Für jeden Teilbaum, auf den ein angegebenes Muster passt, wird die entsprechende Aktion aktiviert.

    Fassen wir zusammen. In Programmen in deklarativem Stil wird das gewünschte Ziel definiert, ohne anzugeben,wie das Ziel zu erreichen ist. Das kann sehr unterschiedliche Formen annehmen und in ganz unterschiedlichenAnwendungsbereichen auftreten. Einige Beispiele wurden hier genannt. Darunter waren solche wie Prolog dienur völlig vergeistigte Informatiker anlocken und solche für Anwender, die Programmen normalerweise nur imSchutzanzug und mit Schweißerbrille gegenüber treten. Man nennt deklarative Programmierstile auch nichtproze-dural. Allen nichtprozeduralen Sprachen ist gemeinsam, dass sie von der Anwendung her kommen und prozedu-rale Konzepte wie Variablen als Namen von Speicherplätzen oder Anweisungen gar nicht, oder nur widerwilligunterstützen.

    1.1.3 Typen

    Typen von Werten, Ausdrücken und Variablen

    Typen sind Kategorien von Dingen. Jede ganze Zahl gehört zur Kategorie der ganzen Zahlen und hat folglich denTyp ganze Zahl. Ein Typ ist zunächst einmal etwas, das einem Wert zukommt. Ein Wert, die Zahl eins beispiels-weise, hat einen Typ. Typen geben Operationen einen Sinn. So ist die Operation + in x+y davon abhänig, ob x undy Zeichenketten, Listen, oder Integerwerte bezeichnen. Viele Operationen sind nicht möglich, weil die Typen derOperanden nicht passen, man braucht die Werte gar nicht anzusehen, schon ihr Typ sagt, dass die Operation nichtgelingen kann. Allen Varianten von Programmiersprachen kennen diese Art von Typen als Attribute von Werten.

    a 2.0

    Behälter WertAusdruck (hier Name)

    Typ des Wertes

    Typ der Variable

    Typ des Ausdrucks

    Abbildung 1.7: Typkonzepte

    Sehr oft hat man dieses rein semantischen Konzept von Typ erweitert und auf die syntaktische Ebene der Pro-grammkonstrukte übertragen: Ausdrücke wie “1” oder “2*f(12,4)+3” wird dort ebenfalls ein Typ zugewiesen.Das ist eine abgeleitete Konstruktion, die aber in vielen Programmiersprachen – vor allem in prozeduralen aberauch in manchen nicht–prozeduralen – definiert ist und die der Korrektheit und der Effizienz der Programme dient:

    • Korrektheit Stellt sich bei der Analyse des Programms durch einen Compiler heraus, dass einem Ausdruckkein Typ zugewiesen werden kann, dann gilt das Programm als falsch: Der Fehler wurde frühzeitig entdeckt.

    • Effizienz Ist der Typ jeden Ausdrucks bekannt, dann können viele Verarbeitungsprozesse schon zur Überset-zungszeit stattfinden. Ist beispielseise schon von x und y und damit von x+y als Ausdruck der Typ Integer

  • 12 Nichtprozedurale Programmierung

    bekannt, dann muss nicht zur Laufzeit, jedes Mal wenn x+y ausgeführt wird, aufwändig analysiert werden,welche Art von Addition auszuführen ist.

    Beides hat seinen Preis. Der Compiler geht auf Nummer sicher und lehnt alles ab, was nicht garantiert korrektist – unabhängig davon, ob zur Laufzeit ein entsprechender Typfehler tatsächlich auftreten würde oder nicht. DieProgrammiererin hat dafür zu sorgen, dass alle Ausdrücke die richtigen Typen haben und muss dazu oft intellektuellanspruchsvolle Typsysteme für die Programme erfinden.

    Wir wollen hier nicht in die komplexe Welt der Typen einsteigen, aber zumindest klar unterscheiden, was da einenTyp hat:

    • Typ eines Werts: natürliches Konzept.

    • Typ eines Ausdrucks: (allgemeinster) Typ aller Werte, die der Ausdruck jemals im Laufe des Programmshaben kann.

    • Typ einer Variablen: (allgemeinster) Typ aller Werte, die jemals im Laufe des Programms die Variable bele-gen können.

    In der exakten Beschreibung einer Programmiersprache wird meist nur Ausdrücken ein Typ zugewiesen. Bezeich-nern (Namen), als elementaren Ausdrücken, wird in prozeduralen Sprachen in der Regel ein fester Typ zuordnet.Aus den Typen der Bezeichner wird dann der Typ der komplexeren Ausdrücke abgeleitet. Hinter dem formalenTyp von Bezeichnern steckt die Intuition, dass Variablen einen Typ haben: den Typ der Werte die sie aufnehmenkönnen oder dürfen.

    In nicht–prozeduralen Sprachen wird meist auf den “abgeleiteten” Begriff von Typ verzichtet. Werte haben auchdort einen Typ, Ausdrücke, inklusive Bezeichner (Namen), dagegen nicht.

    1.1.4 Compilierte und Interpretierte Sprachen

    Compilierte und Interpretierte Sprachen

    Programme in klassischen Programmiersprachen wie C, C++, Pascal, Java, etc. werden in Dateien abgespeichert,von einem Compiler intensiv untersucht, in Zwischen– und/oder Maschinencode übersetzt und dieser wird dannschließlich von einer realen oder virtuellen Maschine ausgeführt. Interpreter führen dagegen dagegen das Quellpro-gramm direkt aus. Ohne sich lange mit irgendwelchen Analysen des Quellprogramms aufzuhalten, werden dessenAnweisungen oder Ausdrücke sofort ausgeführt. Sprachen, deren Programme vor der Ausführung typischerweisevon Compilern übersetzt werden, nennt man compilierte Sprachen, die anderen sind die interpretierten Sprachen.Im Prinzip lassen sich die Programme jeder Programmiersprache in beiden Varianten bearbeiten. Der Charaktereiner Sprache und die Art der Verarbeitung hängen aber so eng zusammen, dass im Regelfall nur entweder dieÜbersetzung oder die Interpretation sinnvoll ist.

    Klassische Compilierte Sprachen

    Die klassische Art, ein Programm auszuführen, ist, es in Maschinencode zu übersetzen und diesen dann von einerrealen Maschine interpretieren zu lassen. Reale Maschinen, richtige Hardware also, die ein Programm einer höher-en Programmiersprache direkt ausführen können, mögen theoretisch denkbar sein, technologische und ökonomi-sche Gründe sprechen aber dagegen, dies wirklich zu versuchen. Die Programme müssen also übersetzt werden.Dabei versucht man in jedem Fall so viel Arbeit wie nur irgend möglich von der Ausführungszeit in Übersetzungs-zeit zu verlagern. Compilierte Sprachen sind auch stets so konzipiert, dass tatsächlich viel zur Übersetzungszeitpassieren kann. Dieses Ziel beeinflusst den Charakter einer Sprache ganz wesentlich. Bei einer Zuweisung wie

    x = y

    die möglicherweise sehr oft ausgeführt wird, muss beispielsweise jedesmal festgestellt werden, wie viele Bytesab wecher Adresse zu welcher Adresse zu bewegen sind und dann müssen sie bewegt werden. Das tatsächlicheBewegen der Bytes kann nur bei jeder Ausführung der Anweisung erfolgen. Bei einer compilierten Sprache wird

  • Th Letschert, FH Giessen–Friedberg 13

    Compiler: Analyse, Optimierung, Transformation

    Quellcode

    Cods): Ausfuehren(Interpreter desreale Maschine (CPU)

    Maschinen−Code

    Abbildung 1.8: Compilierte Sprache der klassischen Art

    aber bestrebt sein, dass die Quell– und Zieladressen und die Anzahl der Bytes schon bei der Übersetzung festlie-gen. Derartige statischen Berechnungen7 sind aber möglich, wenn die Sprache in ihren Ausdruckmöglichkeiteneingeschränkt wird.

    Bei einer interpretierten Sprache nutzt es nichts oder nur wenig, wenn bei jeder Ausführung einer bestimmtenZuweisung immer die gleiche Zahl von Bytes von der gleichen Quell– zur gleichen Zieladresse bewegt werden.Interpretierte Sprachen haben es dagegen nicht nötig, den Charakter ihrer Programme so zu beschränken, dass vielArbeit zur Übersetzungszeit getan werden kann – es gibt ja keine Übersetzungszeit.

    Die Entscheidung zwischen Flexiblität auf der einen und Effizienz und Sicherheit auf der anderen Seite ist immereine Gradwanderung. Klassische compilierte Sprachen bieten ihren Programmierern die Möglichkeit mit Konver-sionen, Zeigern, Polymorphismus und Ähnlichen aus ihrem engen Gehäuse auszubrechen.

    Compiler: Analyse, Optimierung, Transformation

    Quellcode

    Cods): Ausfuehren(Interpreter des

    reale Maschine (CPU)

    Zwischen−Code

    Daten Anweisungen

    Madchinen−Code

    virtuelle Maschine /Common Language Runtime (CLR)

    Abbildung 1.9: Compilierte Sprache der modernen Art

    7 Als statisch bezeichnen die Compilerleute alles, was vor der Laufzeit des Programm festliegt, bzw. festgestellt (berechnet) werden kann.

  • 14 Nichtprozedurale Programmierung

    Compilierte Sprachen der modernen Art

    Heute gibt es mit Java und C# compilierte Sprachen, die interpretiert werden. Von ihrem Grundcharakter hersind beide immer noch compilierte Sprachen. Der Compiler versucht viel an Prüfung und Vorausberechnung zuübernehmen und die Sprachen sind so konzipiert, dass dies auch möglich ist. Der erzeugte Zwischencode ist einabstrakter Maschinencode, der eine Unabhängigkeit von der realen Hardware und der Betriebsumgebung liefernsoll und der dadurch auch relativ leicht “im Fluge” (oder just in time) in echten Maschinencode umgewandeltwerden kann.

    Compiler und Typen

    Das Typsystem des Programms spielt eine wichtige Rolle beim Zurechtstutzen einer Sprache auf Compilations-tauglichkeit. Allen Variablen, formalen Parameter und Funktionsergebnissen müssen eindeutige Typen zugewiesenwerden. Die Typen sind nicht nur eine enorme Hilfe bei der Konzeption der Programme im Kopf der Program-mierer, sie ermöglichen es dem Compiler auch, eine Vielzahl von Programmierfehlern zu entdecken und für dieKonstrukte effiziente Übersetzungen in Maschinen– (oder Zwischen–) Code zu finden.

    Die Typen der Ausdrücke sind dabei letztlich nichts anderes, als die – zur Übersetzungszeit verfügbaren – kon-densierten Informationen über die – zur Laufzeit – möglichen Werte der Ausdrücke. Diese Informationen könnenzu Prüf– und Optimierungzwecken eingesetzt werden. In ernsthaften Programmen sind die Typen ihrer Konstrukteaber keineswegs trivial, im Gegenteil, die Kunst des Programmierens in einer compilierten Sprache besteht ganzwesentlich darin, ein geeignetes Typsystem zu erfinden, in dessen Rahmen jedem Ausdruck ein eindeutiger Typzukommt.

    Das Einzwängen in ein Typsystem bringt immer eine Beschränkung der Ausdrucksmöglichkeiten mit sich. Jeflexibler und fostgeschrittener das System ist, um so geringer sind diese Beschränkungen. Man kann dann so etwaswie

    ...x = new Viereck;x = new Dreieck;

    sagen, erkauft sich das aber damit, dass man einen Typ GeometrischesObjekt für x erfinden und eine Pro-grammiersprache beherrschen muss, in der dies möglich ist.

    X

    1

    2X

    1

    GeoObjekt

    0. GeoObjekt = ????

    1. GeoObjekt x;1. x = new Viereck;

    2. x = new Dreieck; 2. x = new Viereck;

    3. x = new Dreieck;

    2

    3

    Abbildung 1.10: Nicht-typisierte und typisierte Programmierung

    Interpretierte Sprachen

    Compilierte Programmiersprachen spielen eine dominierende Rolle in der Ausbildung – und das zu Recht. Diehohe Kunst der Informatiker, die Konstruktion großer und effizienter Programmsysteme, braucht Sprachen mitderen Eigenschaften.

    Nun sind aber nicht alle nützlichen Programme groß. Effizienz ist auch nicht immer ein Thema und Nicht–Informatiker sind oft von den Typ–Konzepten moderner Hochsprachen überfordert. Für diese Anwendungsfälleund Anwender gibt es eine Vielzahl an alternativen Sprach–Konzepten. Diese kleinen, leichten Sprachen für ad–hoc Anwendungen und ad–hoc Programmierer werden meist von einem Interpretierer verarbeitet. Das hat zweiwichtige Gründe: das Verhältnis von Aufwand und Ertrag bei der Übersetzung und die Natur der kleinen Spra-chen.

  • Th Letschert, FH Giessen–Friedberg 15

    Zunächst einmal will man einen kleinen Auftrag eingeben und sofort seine Ausführung sehen, statt mühsam einenCompiler zu aktivieren und dann das erzeugte Programm zu starten. Selbst wenn die sofortige Ausführung – dieInterpretation – durch einen Interpretierer um den Faktor 1000 langsamer sein sollte, als die Ausführung von äqui-vivalentem Maschinencode, bei winzigen Programmen lohnt sich der Aufwand der Übersetzung trotzdem nicht.In weitverbreiteten und unter Nicht–Informatikern oft und gerne benutzten Systemen, wie etwa MATLAB, gibtman einen mathematischen Ausdruck ein und das System berechnet ihn sofort, bzw. erzeugt sofort eine graphischeDarstellung. Der Gedanke, diesen Ausdruck in ein zu übersetzendes Programm einbetten zu müssen, ist für die An-wender solcher Systeme völlig absurd. Unter Informatikern bekanntere Skript–Sprachen wie Perl, TCL, Python,AWK, etc. liegen auf der gleichen Linie. Die Shell eines Unix–artigen Systems ist ebenfalls eine Programmier-sprache, die eingegebene Kommandos sofort verarbeitet.

    Die Verarbeitung der genannten Sprachen durch einen Interpreter hat nicht nur ergonomische Gründe. Die Spra-chen selbst machen die Verarbeitung durch einen Compiler schwierig bis sinnlos.8 Klassischen Programmierspra-chen liegt das “Behälter–Konzept” von Variablen zugrunde. Behälter haben einen Typ. Das schränkt die Varianzder in ihnen speicherbaren Werte ein. Der Compiler kann die Typinformationen zur Übersetzungszeit verwenden,um korrekten und effizienten Code zu erzeugen. Informatikern mag das Behälterkonzept in Fleisch und Blut über-gegangen sein, für “normale Menschen” ist aber eine Variable ein Name für einen Wert. Der Typ eines Behältersmacht Sinn, nicht aber der Typ eines Namens für Werte. Wenn Variablen aber Namen für Werte sind, dann ist esunsinnig zu verlangen, dass es beim Wechsel der Bedeutung einer Variablen eine Typbeschränkung geben sollte.Warum soll nicht etwa a einen Integer–Wert bezeichnen, dann eine dreidimensionale Matrix und schließlich eineFunktion.

    Prozedural und Compiliert, Deklarativ und Interpretiert

    Funktionale und andere deklarative Sprachen sind typischerweise interpretierte Sprachen. Die strenge Typisie-rung ist ein Konzept das ursprünglich von der Maschine kam, vom Ziel einen Compiler schreiben zu können,der effizienten Code erzeugt. Deklarative Sprachen kommen dagegen von der Anwendungsseite. Ihnen liegt dasmathematische Konzept einer Variablen zugrunde. Variablen sind Namen für Werte. Eine Variable kann dannkonsequenterweise jeden beliebigen Wert bezeichnen, Typbeschränkungen gibt es nicht. Die Übersetzung wirdunter diesen Bedingungen schwierig und sie bringt auch nicht so viel an Effizienzgewinn. Die Werte selbst habennatürlich weiterhin Typen, aber wenn Variablen kein Typ zugeordnet wird, dann kann zur Übersetzungszeit nichtmehr vorausgesehen werden, welchen Typ der Wert einer Variablen zur Laufzeit haben wird – die entsprechendeInformation muss dynamisch verwaltet werden.

    Kurz und gut, dekarative Sprachen sind meist nicht typisiert, d.h. ihre Ausdrücke haben keine fixen im Vorausbestimmbaren Typen und sie werden meist interpretiert. Beides ist aber kein unbedingtes Muss. Deklarative Spra-chen können und sind gelegentlich streng typisiert und compiliert und Typen kommen zwar von den Compiler–Techniken in die Programmiersprachen, Typen sind aber weit mehr als nur Hilfsmittel für Compiler.

    8 Aber nicht unmöglich: alles was interpretiert werden kann, kann auch übersetzt werden.

  • 16 Nichtprozedurale Programmierung

    1.2 Ausdrücke und ihre Auswertung

    Abstrahieren heißt die Luft melken.F. Hebbel

    1.2.1 Definitionen und Ausdrücke

    Ausdrücke

    Ein funktionales Programm ist im einfachsten Fall ein Ausdruck. Die Ausführung des Programms besteht schlicht-weg darin, dass dieser Ausdruck ausgewertet wird. So ist beispielsweise

    2 + 3 ∗ 4ein triviales funktionales Programm das zu 14 ausgewertet wird. Wenn auch nicht jede funktionale Sprache in-terpretiert wird und nicht jede interpretierte Sprache funktional ist, so ist es doch ein typisches Charakteristikumfunktionaler Sprachen, dass sie einen Interpretierer zur Verfügung stellen. Dieser erlaubt es, solche Ausdrückeinteraktiv auszuführen. Ihre Programme werden entweder interaktiv eingegeben oder in einer Datei gespeichert. Inein Datei gespeichert, nennt man sie gelegentlich “Skripte” und die entsprechenden Sprachen “Skript–Sprachen”.

    Manche Sprachen bestehen auf einer besonderen Notation für ihre Ausdrücke. Lisp beispielsweise will alle Aus-drücke vollständig geklammert und in Präfix-Notation sehen. Statt 2 + 3 ∗ 4 übergibt man also dem Interpreter>>> (+ 2 (* 3 4))

    zur Auswertung ein und erhält die Antwort

    14

    Die vollständig geklammerte Präfix–Notation in Lisp hat ihre besonderen Gründe, die aus ganz speziellen Ei-genschaften und der Historie dieser Sprache folgen und nicht auf einem allgemeinen Prinzip funktionaler Spra-chen beruhen. Python ist eine prozedurale Skript-Sprache mit den wesentlichen Möglichkeiten einer funktionalenSprache, ohne auf das “Funktionale” beschränkt zu sein. Sie erlaubt Ausdrücke in normaler Infix–Notation mitPräzedenz–Regeln:

    > pythonPython 2.2.2 ...>>> 2+3*414>>>

    Definitionen

    Ausdrücke allein sind etwas langweilig als Programme. Sie werden etwas interessanter, wenn ihnen Definitionenvorausgehen. Beispielsweise werden in folgendem Pythoncodestück zwei Namen definiert x und f, die dann imdarauf folgenden Ausdruck verwendet werden:

    def f(n):if n==0:

    return 1else:

    return n*f(n-1)

    x=5

    f(x)+f(x-1)

  • Th Letschert, FH Giessen–Friedberg 17

    Freie und gebundene Variablen

    Definitionen sind dazu da, um freien Variablen eine Bedeutung zu geben. In

    f(x)+f(2*x)

    sind sowohl x als auch f frei. Nur mit diesem Ausdruck allein haben sie keinen Wert und damit ist der ganzeAusdruck undefiniert. Klar, wir müssen die Definitionen hinzunehmen, um dem Ausdruck insgesamt einen Wertzuweisen zu können.

    Frei oder gebunden sind relative Begriffe. In f(x)+f(x-1) sind die Variabelen f und x. Im gesamten Programmdagegen nicht. Die Definitionen vorher binden sie an eine Bedeutung. Alle Variablen sind damit gebunden.

    Ähnlich verhält es sich in der Funktionsdefinition. In

    n==0

    ist n frei. In der gesamten Funktionsdefinition ist es an die Bedeutung “Funktionsparameter” gebunden.

    Ein– und Zweistufige Wertzuordnung

    Definitionen ordnen Namen eine Bedeutung (einen Wert) zu. Beispielsweise wird in folgendem C–Beispiel demNamen x ein Speicherplatz als Wert/Bedeutung zugeordnet:

    int x;

    In einem zweiten Schritt kann dann der Speicherplatz mit einem Wert belegt werden:

    x = 5;

    Vom Namen zum Wert 5 gelangt man dann in zwei Schritten:

    x→ Variable/Speicherplatz→ 5Um die beiden “Werte” von x zu unterscheiden spricht man in C und C++ vom l-Wert und vom r–Wert. Der l–Wertist der Speicherplatz und der r–Wert die 5. Der l-Wert eines Namens ist relativ stabil, der r–Wert kann dagegen mitjeder Zuweisung wechseln. Aber Achtung, nicht nur Namen haben l-Werte, auch ein Ausdruck kann einen l-Werthaben. In

    a[x+i] = 5;

    hat a[x+i] einen l–Wert der höchst dynamisch sein kann.

    In typfreien Sprachen wie Python wird nicht zwischen l– und r–Wert unterschieden. Einem Namen wird eineeinzige Bedeutung zugeordnet.

    x = 5

    Vom Namen zum Wert gelangt man dann in einem Schritt:

    x→ 5

    Umgebung

    Die von einer oder mehreren Definitionen erzeugte Abbildung von Namen auf ihre Bedeutung nennt man Umge-bung. Die Umgebung enthält die Bedeutung (Definition) der freien Variablen eines Ausdrucks. In

    f(x)+f(2*x)

    sind f und x frei. Der Ausdruck enthält damit freie Variablen und kann darum nur in einer Umgebung ausgewertetwerden, die x und f definiert. In der von den Definitionen

    def f(n):if n==0:

    return 1else:

    return n*f(n-1)

  • 18 Nichtprozedurale Programmierung

    x=5

    erzeugten Umgebung u mit

    u = [f → 5, f → Fakultätsfunktuion]hat f(x)+f(2*x) den Wert 144.

    Andere Definitionen erzeugen andere Umgebungen, die zu anderen Werten des gleichen Ausdrucks führen.

    Zustand

    In Sprachen mit zweistufigem Variablenkonzept gibt es zwei Abbildungen. Die erste ordnet einem Namen einenSpeicherplatz zu, die zweite dem Speicherplatz einen Wert. Die erste wird Umgebung genannt. Die zweite ist deraktuelle Maschinen–Zustand oder kurz Zustand. In C muss man, um den Wert eines Ausdrucks wie

    2*x

    zu bestimmen, wissen welche Speicherstelle (l–Wert) mit x gemeint ist – das ist die Umgebung. Kennt man dannnoch die aktuelle Belegung (r–Wert) dieser Speicherstelle – das ist der Zustand – dann lässt sich der Wert be-rechnen. Also: in einer Umgebung, in der x sich auf die Speicherstelle 0x4711 bezieht und einem aktuellenMascheinzustand, in dem 0x4711 den Wert 5 hat, wird 2*x zu 10 ausgewertet.

    1.2.2 Statische und Dynamische Bindung

    Statische Bindung

    In allen relevanten Programmiersprachen können Prozeduren bzw. Funktion definiert werden und in der Regeldürfen sie freie Variablen (freie Bezeichner) enthalten. In der C–Funktion

    inf f(int x) { return x+y; }

    ist beispielsweise y frei. Generell stellt sich die Frage, was ein freier Bezeichner zu bedeuten hat, oder an welcheDefinition seine Verwendung, jeweils gebunden ist?

    In unserem kleinen C–Programm oben, sind in return x+y; sowohl x und y frei – es sind Verwendungsstellender Bezeichner x und y. Die Bindungsregeln von C sagen, dass x in return x+y; an die Parameterdefinitionim Kopf der Funktion gebunden wird. Einfach ausgedrück, mit x ist der Parameter gemeint. y hat keine Bindunginnerhalb der Funktionsdefinition. Es ist nicht nur, wie x, im Funktionskörper, sondern in der gesamten Funkti-onsdefinition frei. Jeder C–Programmierer weiß, und die anderen ahnen, wie zu diesem freien y eine Bindung zusuchen ist: Es kann nur eine globale Variable sein und man kann sie ausfindig machen, indem man den Programm-text analysiert. Wir brauchen dazu auch gar nicht Bedracht zu ziehen, wo wann oder ob die Funktion f jemalsaktiviert wird.

    In C und ähnlichen Sprachen ist die Bindung eines Bezeichners immer klar durch den Programmtext bestimmt.Ein freier Bezeichner in einer Funktion bezieht sich immer auf das, was an deren Definitionsstelle gültig ist. ImC–Beispiel oben ist mit y der Speicherplatz gemeint auf den sich y an der Definitionsstelle von f bezieht, egal inwelchem Kontext f aufgerufen wird und welche Definition von y dort zuletzt ausgeführt worden sein mag. Die“Bindung” von y erfolgt an der Definitionsstelle. Sie ist unveränderlich und kann zur Übersetzungszeit bestimmtwerden: sie ist “statisch”.

    C, C++, Java, und viel andere Sprachen definieren eine statische Bindung der freien Bezeichner. Folgendes C++–Programm gibt darum den Wert 10 (und nicht -7) aus:

    #include

    using namespace std;

  • Th Letschert, FH Giessen–Friedberg 19

    int x = 10;

    void f(){cout

  • 20 Nichtprozedurale Programmierung

    Wir dürfen uns nicht davon irritieren lassen, dass jede Modifikation des gloablen x sich auf f auswirkt. DasProgramm

    x = 10def f():

    print xdef g():

    x = -7f()

    x=0g()

    würde selbstverständlich 0 ausgeben – aber im C++–Beispiel gilt das genauso.

    In Python definiert also jede Funktion ihren eigenen Gültigkeitsbereich und erzeugt somit ihre eigene Umgebung.Ein Name kann darum in mehreren unterschiedlichen Umgebungen definiert sein. Freie Variablen werden in derUmgebung gesucht (= gebunden), die ihrer Verwendungstelle statisch (textuell) am nächsten ist.

    Lokal und Global in Python

    In C++ werden Definitionen und Zuweisungen unterschieden. Wenn in einer Funktion ein globales x mit einemWert belegen werden soll, dann schreibt man

    ...void f(){

    x = 5;...

    }...

    Soll dagegen ein lokales x erzeugt und belegt werden, dann schreibt man

    void f(){int x = 5;...

    }

    In Python gibt es den Unterschied zwischen Definition und Zuweisung nicht. Jede erste Zuweisung in einemGültikeitsbereich wird als Definition interpretiert.

    x = 0 # Definion globales xx = 5 # Zuweisung an globales xdef f():

    x = 5 # Definition eines lokalen xx = 6 # Zuweisung an lokales x

    }

    Will man auf eine nicht–lokale Variable schreiben zugreifen, dann muss explizit gesagt werden, dass es sich nichtum eine Definition handelt. Der Zugriff auf einen globalen Namen muss explizit gekennzeichnet werden. Mit

    def f():x = 5...

    wird ein lokales x erzeugt und gleichzeitig mit einem Wert versehen. Will man das nicht, dann muss man es sagen.Mit

  • Th Letschert, FH Giessen–Friedberg 21

    def f():global xx = 5...

    bezieht sich jede Verwendung von x in f auf das x der globalen Umgebung, es wird keine lokale Variable erzeugt.

    Verschachtelte Gültigkeitsbereiche in Python

    Gültigkeitsbereiche können üblicherweise verschachtelt werden. In

    int x = 3;void f () {

    int z = 2;for ( int z=1; z

  • 22 Nichtprozedurale Programmierung

    x = 5 # globales xdef f():

    x = 6 # f::x, das x von f -- in g nicht schreibend zugreifbardef g(y):

    global xx = 0 # das globale x wird 0

    ...

    oder man lässt global weg und bezieht sich auf eine neue lokale Variable:

    x = 5 # globales xdef f():

    x = 6 # f::x, das x von f -- in g nicht schreibend zugreifbardef g(y):

    x = 0 # ein neues lokales g::x wird 0...

    Diese Beschränkung ist nicht zufällig, sie hat ihren Sinn auf den wir an anderer Stelle noch einmal zu sprechenkommen werden.

    1.2.3 Statische und Dynamische Typisierung

    Typisierung: Statisch oder Dynamisch

    Unter Typisierung versteht man die Zuordnung eines Typs zu einem Ausdruck. Auch hier hat man die beidengrundsätzlichen Optionen einer statischen oder einer dynamischen Typisierung. Während bei Frage der Bindungallgemeiner Konsens herrscht, dass statische Bindung vernünftig ist, werden darüber, wann und wie die Typisierungeines Programms zu erfolgen hat, heftige Debatten geführt. Die Positionen sind dabei oft genug dogmatisch fixund werden mit religösen Eifer vertreten. Dieser Eifer hat durchaus seine Berechtigung. Die Art der Typisierungist von fundamentaler Bedeutung für den Charakter der Sprache und damit für den Programmierstil.

    Auf der einen Seite gibt es die Mehrheitsfraktion der statischen Typisierer. Sie sind der Meinung, dass Programmestatisch, zur Übersetzungzeit, typisiert werden können und müssen: Jedem syntaktischen Konstrukt, d.h. jedemAusdruck und jedem Teilausdruck in einem Programm, muss ein fester Typ zugeordnet werden können, der sichwährend des Programmlaufs niemals ändert. Der Compiler stellt die Typen fest, prüft die korrekte Zusammenstel-lung des Programms in Bezug auf die Typen und benutzt die Typ–Information um effiziente Maschinenprogrammezu generieren.

    Bei der dynamischen Typisierung werden Typinformationen zur Laufzeit verarbeitet. Entsprechende SprachenSprachen werden oft auch (nicht ganz korrekt) typfrei genannt. Ausdrücke haben auch hier Werte und diese ha-ben einen Typ. Im Gegensatz zu Sprachen mit statischer Typisierung kann der Typ dieser Werte aber zur Laufzeitbeliebig wechseln und dazu noch bei manchen Programmläufen korrekt sein und bei anderen fehlerhaft.

    Kommt in einem Programm mit statischer Typisierung beispielsweise irgendwo so etwas wie

    a[i]+i

    vor, dann kann ich und der Compiler, allein aus der Betrachtung des Programms, schlı̈eßen, welchen Typ a[i] hatund ob a[i]+i insgesamt ein korrekter Ausdruck ist: Irgendwo sind a und i definiert und aus diesen Definitionenkann dann geschlossen werden, ob a[i]+i auswertbar ist oder nicht.

    Bei Sprachen mit dynamischer Typisierung gilt das nicht. Der Typ von a[i] ist der Typ seines Wertes und welcherdas ist, ist nicht vorhersehbar. In Python etwa ist folgendes Programmstück völlig legal:

    a = {’hoho’:’hallo’, 1:2, 2:’welt’}i = input()print a[i]+i

    Für manche Eingaben von i ist es korrekt, für andere bricht es mit einem Typfehler ab.

    Hinter den beiden Vorstellungen zur Typisierung stecken folgende Sprachkonzepte:

  • Th Letschert, FH Giessen–Friedberg 23

    • Statische Typisierer sind der Überzeugung, dass jeder Ausdruck und jeder Teilausdruck in einem Programmeinen ganz bestimmten festen Typ hat, und jeder Wert, den dieser Ausdruck oder Teilausdruck in einemProgrammlauf annehmen kann, hat ebenfalls diesen Typ.9

    • Dynamische Typisierer sind dagegen der Meinung, dass “Typ eines Ausdrucks” ein unsinniges Konzept ist.Ausdrücke haben keine Typen. Sie haben wechselnde Werte und nur diese Werte haben einen Typ.

    Bei Programmen mit statischer Typisierung kann der Compiler viele Programmierfehler aufdecken und deutlicheffizientere Programme erzeugen. Dafür muss der Programmierer ein Typsystem definieren, bei dem die Typen derAusdrücke und die ihrer Werte in der richtigen Beziehung stehen. Programme in Sprachen mit dynamischer Ty-pisierung sind meist wesentlich kürzer und einfacher zu schreiben. Die Konstruktion eines passenden Typsystemsfür die Ausdrücke entfällt. Dafür sind sie aber langsamer und fehlerträchtiger.

    Der schlimmste Makel von Sprachen mit dynamischer Typisierung ist vielleicht, dass sie als Sprachen von Hobby–Programmierern gelten. Typen sind schwierige Konstrukte. Jeder Ausdruck muss in ein Typsystem passen. Speziellin modernen Sprachen mit statischer Typisierung ist dazu ein Typsystem von gewisser Komplexität notwendig, dasdazu auch noch vom Programmierer selbst definiert werden muss. Wer diese Definition durch die Verwendungeiner Sprache mit dynamischer Typisierung vermeidet, gerät leicht in Ruf, zu ihr nicht fähig gewesen zu sein.

    Python ist eine dynamisch typisierte Sprache

    Interpretierte (“Skript”–) Sprachen arbeiten normalerweise ohne explizite Typangaben in ihren Programmen (Aus-drücken). Das heißt nicht, dass es keine Typen gibt, sondern dass man darauf verzichtet syntaktischen Konstruktenim Quelltext explizit einen Typ zuzuweisen. Es gibt trotzdem Typen, alle Werte haben einen Typ und auch Aus-drücke haben einen Typ. Der Typ der Ausdrücke ist aber nicht statisch im Quelltext festgelegt, er wechselt mit demTyp den der Wert des Ausdrucks zur Laufzeit annimmt.

    Im folgenden Beispiel wird der Python–Interpretierer mit Ausdrücken unterschiedlichen Typs konfrontiert. Die Ty-pen der Teilausdrücke bestimmen die Typen des Gesamtausdrucks und die Art der Operationen. Typfehler werdendabei festgestellt und zurückgewiesen:

    >>> 2 + 35>>> ’hallo’ + ’Welt’’halloWelt’>>> 3 * 412>>> 3 * ’hallo’’hallohallohallo’>>> 3 * 3.14159.4245000000000001>>> 3 * ’hallo’ + 4Traceback (most recent call last):File "", line 1, in ?

    TypeError: cannot concatenate ’str’ and ’int’ objects

    Ohne irgendwelche Deklarationen und Definitionen interpretiert das System die Operatoren hier jeweils in derrichtigen Weise als Operation auf Int–, Float, oder Stringwerten. Der Typfehler im letzten Ausdruck wird auchnicht bei einer Übersetzung oder sonstigen Analyse entdeckt, sondern bei dessen Ausführung.

    Der Typ eines Werts kann zwar nicht festgelegt, aber mit Hilfe der type–Funktion explizit abgefragt werden:

    >>> type(3 * ’hallo’)

    9 Das “hat diesen Typ” kann auch eine komplexe Vererbungsbeziehung oder eine Instanzierung eines Templates, oder sonst eine Relationin einem beliebig komplexen Typsystem sein.

  • 24 Nichtprozedurale Programmierung

    Basistypen

    Wie jede andere Sprache hat auch Python eine Grundmenge an einfachen und zusammengesetzten Datentypen mitden jeweils zugehörigen Operatoren und Operationen. Die Datentypen sind:

    • Zahlen

    – Ganze Zahlenmit Literalen in C/C++–Notation, 1, 02 (oktal), 0x13 (hexadezimal) und Werten im Bereich der Int–Werte von C.

    – Lange ganze Zahlensind ganze Zahlen, die keine Größenbeschränkung haben. Sie werden durch ein angehängtes L gekenn-zeichnet, Z.B. 9999999999999999L.

    – Fließkomma–Zahlenebenfalls mit Literalen im C–Stil (z.B. 1.0 oder 12.3E-10) und Werten die double–Werten in Centsprechen.

    – Komplexe Zahlenmit Fließkomma–Literalen denen ein “j” als Zeichen für

    √−1 angehängt wird. Z.B.: 2.5j, oder 0j,

    sie werden durch ein Paar von Fließkomma–Werten dargestellt werden.

    >>> 0.5j + 2(2+0.5j)

    – Boolesche WerteWerden wie in C als Int–Werte behandelt. Ab Version 2.3 gibt einen eigenständigen Typ für boolescheWerte mit den Literalen True und False.

    • Sequenzen

    – Zeichenketten (strings)sind in einfache oder doppelte Hochkommas eingeschlossene Folgen von Zeichen, z.B. ’hallo’

    – Tupelsind unveränderliche Folgen fester Länge, z.B. (1, 2.0, ’hallo’). Ein Tupel kann Elementevon unterschiedlichem Typ enthalten.

    – Listensind veränderliche Folgen variabler Länge, z.B. [1, 2.0, ’hallo’]. So wie Tupel können ListenElemente unterschiedlichen Typs enthalten.

    • Abbildungen (Dictionaries)sind Zuordnungen von Werten (beliebigen Typs) zu Schlüsseln fast beliebigen Typs,z.B.: {1:’hallo’, ’hallo’:’hugo’}Abbildungen sind veränderlich. Auch bei ihnen ist ein Typmixerlaubt.

    In Python sind Listen und Abbildungen Objekte, d.h. veränderliche Dinge. Ein Konzept, das der “reinen” funk-tionalen Programmierung zuwider läuft. Dort gibt es nur richtige Werte, die im mathematischen Sinn ewig undunveränderlich sind. Alles Veränderliche ist ein Objekt, etwas aus der Welt der prozeduralen Programmierung.Selbstverständlich können Listen und Abbildungen auch in rein funktionalen Programmen verwendet werden,folgt doch aus der Tatsache, dass Listen und Abbildungen verändert werden können, nicht, dass sie verändertwerden müssen.

    Details und weitere Beispiele entnehme man der Literatur oder einem Python–Tutorium.

    Vordefinierte Operatoren und Funktionen

    Auf den numerischen Werten sind die üblichen arithmetischen und relationalen Operatoren definiert, z.B.:

  • Th Letschert, FH Giessen–Friedberg 25

    >>> 5.0/2 > 5/21>>> 5.0/2 < 5/20

    An diesem Beispiel erkennen wir, dass es eine ganzzahlige und eine Fließkomma–Version der Division gibt, unddass boolesche Werte als Integerwerte bestimmt werden. Beides entspricht, so wie die weiteren Operatoren inPython, dem Vorbild C.

    Auf Listen– und Tupel–Elemente wird mit der Index–Notation zugegriffen:

    >>> [1, 2, 3][1]2>>> (2, 3.14, ’hallo’)[2]’hallo’

    in gleicher Weise wird der Wert zu einem Schlüssel in einer Abbildung selektiert:

    >>> {1:’hallo’, ’hallo’:’hugo’, 2.0:1}[’hallo’]’hugo’

    Listen und Tupel können mit + zusammengefügt werden:

    >>> [1, 2] + [’hugo’, ’egon’][1, 2, ’hugo’, ’egon’]>>> (’hugo’, ’egon’) + (’emilie’, ’karla’)(’hugo’, ’egon’, ’emilie’, ’karla’)

    Tupeln und Listen dürfen dabei nicht vermischt werden.

    Selbstverständlich gibt es auch in Python vordefinierte Funktionen. Mathematische Funktionen werden im Modulmath definiert. Z.B.:

    >>> import math>>> math.cos(3.1415926) + math.sqrt(3.1415926)0.77245383578811577

    oder

    >>> from math import sin>>> sin(3.1415)9.2653589660490244e-05

    Mächtige Ausdrücke

    Bei der ersten Begegung mit Sprachen wie Python fällt auf, dass dort mit sehr mächtigen Ausdrücken gearbeitetwerden kann. Man schreibt einfach

    m = {1:[’hugo’, ’emil’], 2:[’karla’, ’charlotte’], 3:[]}

    und hat schon gleich eine komplexe Datenstruktur erzeugt. Natürlich liese sich auch in C++ oder Java eine ent-sprechende Datenstruktur aufbauen, aber dort gibt es keinen Ausdruck, dessen Wert direkt eine solche beliebigkomplexe Struktur ist. Stattdessen müsste man Konstruktoren und diverse Einfüg–Operationen bemühen. Warumist das so?

    Ausdrücke gehören zum Kern einer Sprache. Python und vergleichbare Sprachen sind von Anfang an mit komple-xen Datenstrukturen wie Listen und Abbildungen ausgestattet. Ausdrücke mit Listen und Abbildungen als Wertsind darum selbstverständlich. Programmierprobleme wird man in einer solchen Sprache auch normalerweise mitder Grundausstattung an Typen lösen. Eigene Typdefinitionen sind, wenn überhaupt möglich, dann doch unüblich.

  • 26 Nichtprozedurale Programmierung

    C++ und Verwandte folgen einem völlig anderen Paradigma. Der Sprachkern ist vergleichsweise sparsam mitTypen, bietet aber ein reichhaltiges Instrumentarium zu deren Definition. Es ist zwar bei diesen Sprachen heuteauch üblich mit gut ausgestatten Klassenbibliotheken zu arbeiten, aber diese gehören nicht zur Sprache, es sindimmer Zusätze. Ein typisches Programm dieser Sprachen besteht darum im wesentlichen aus Typdefinitionen.

    1.2.4 Kopiersemantik

    Die Kopiersemantik einer Sprache beeinflusst deren Charakter ganz wesentlich. Programmierer mit mathemati-schem Hintergrund und ohne formelle Informatik-Ausbildung sind oft etwas überrascht, und manchmal wundernsich sogar Informatiker, wenn sie folgende Konversation mit ihrem Python–Interpreter führen:

    >>> l1=[1,2,3]>>> l2=l1>>> l2[1,2,3] -- OK>>> l1[0]=100>>> l1[100, 2, 3] -- OK>>> l2[100, 2, 3] -- UUPS

    Jeder Mensch hat wohl ein eigenes intuitives Verständnis von Werten und Objekten und dem was bei einer Zuwei-sung passiert. Im nicht–prozeduralen Verständnis liest man

    >>> l1=[1,2,3] -- l1 hat den Wert "Liste 1, 2, 3" (n Python FALSCHES Verstaendnis)>>> l2=l1 -- l2 hat den gleichen Wert wie l1 (in Python FALSCHES Verstaendnis)

    In diesem Verständnis wird l2 mit einer Kopie (einem “Klon”, einer tiefen Kopie) von l1 belegt. Nach derVeränderung von l1 mit

    >>> l1[0]=100

    erwartet man darum nicht, dass l2 mit verändert wird. Man erwartet referentielle Transparenz: l2 wurde nichtangefasst und sollte darum das Gleiche sein wie vorher.

    Tatsächlich ist Python aber eine prozedurale Sprache bei der die beiden Zuweisungen folgendermaßen zu verstehensind:

    >>> l1=[1,2,3] -- l1 bezeichnet das Objekt "Liste 1, 2, 3" (OK)>>> l2=l1 -- l2 bezeichnet das gleiche Objekt wie l1 (OK)

    l1 und l2 sind also Namen für das gleiche Objekt. Die zweite Zuweisung führt nur einen neuen Bezug, eine neueReferenz, auf das Objekt ein. In der Implementierung wird lediglich ein Zeiger kopiert (flache Kopie).

    Keine der beiden Sichten auf die Zuweisung ist “besser” oder “natürlicher” als die andere. Beide haben ihre Be-rechtigung und beim Blick auf ein Programm sollte man als Informatiker immer fragen, was genau a = b be-deuten soll. Nur Naive halten diese Frage für naiv. In Python ist besondere Vorsicht angebracht, da verschiedenePhilosophien des Kompierens angewendet werden. Man beachte:

    >>> l1 = [1,2,3]>>> l2 = l1>>> l1 = l1+l1>>> l2[0] = 100>>> l2[100, 2, 3]>>> l1[1, 2, 3, 1, 2, 3] -- + kopiert tief

    Die Zuweisung kopiert also flach und der +–Operator tief.

  • Th Letschert, FH Giessen–Friedberg 27

    1.2.5 Funktionen und ihre Definition

    Funktion, Algorithmus, Programm

    Funktionen sind das Herzstück der funktionalen Programmierung. Ein funktionales Programm besteht typischer-weise aus einer Serie von Funktionedefinitionen und einem Ausdruck, der mit diesen ausgewertet wird. Funktionenhaben natürlich nicht nur in der funktionalen Programmierung ihren Platz. Es gibt sie in allen Arten von Program-men und sogar in der Mathematik.

    In der Mathematik werden Funktionen üblicherweise nach folgendem Muster definiert:

    • Eine Menge ist ...

    • Das kartesische Produkt A×B zweier Mengen A und B ist ...

    • Eine Relation R zwischen zwei Mengen A und B ist eine Teilmenge des Kreuzprodukts A×B.

    • Eine Funktion f : A → B von einer Menge A nach einer Menge B ist eine rechts-eindeutige Relation ausA × B. D.h. zu jedem a�A gibt es genau ein (höchstens ein - bei partiellen Funktionen) Element b�B mit< a, b > �f ; dieses b wird mit f(a) bezeichnet.

    Eine mathematische Funktion ist also eine Menge von Paaren aus A×B mit der Eigenschaft, dass jedem Elementaus A genau (höchstens) ein Element aus B zugeordnet wird. Es ist also eine Zuordnung von Werten zu Werten.Für die Informatik ist dieser Standpunkt zu abstrakt, denn er lässt zwei für sie wesentliche Gesichtspunkte außerAcht:

    • Wie wird die Zuordnung vollzogen: Auf welche Art wird der Ergebniswert aus dem Ausgangswert bestimmt(berechnet)?

    • Wie wird die, im Allgemeinen ja unendliche, Funktion beschrieben?

    Natürlich wird auch in der Mathematik (gelegentlich) gerechnet und es werden auch Funktionen beschrieben,aber das wesentliche Grundkonzept ist die abstrakte, als Paarmenge definierte Zuordnung. In der Informatik gehtes aber um den konkreten Mechanismus, der die Zuordnung ausführt. Algorithmen mit ihrer Spezifikation undImplementierung sind die entsprechenden Konzepte.

    Vom “funktionalen Standpunkt” aus, hat ein Informatiker Algorithmen zu implementieren, die funktionale Zusam-menhänge realisieren. Diese Sicht gilt heute mit guten Grund als veraltet, da sich weder interaktive, noch verteilteSysteme (ohne heftige Verrenkungen) als Funktionen deuten lassen. Wenn der funktionale Standpunkt auch nicht(mehr) die Gesamtheit der Software beschreiben kann, wenn also nicht alles eine Funktion ist, so sind doch sehrviele Probleme als Funktionen zu verstehen.

    In diesem Kapitel gehen wir einfach davon aus, dass Programme Implementierungen von Algorithmen sind unddass alle Algorithmen Verfahren zur Zuordnung von Ergebnis– zu Argumentwerten sind. Wir beschränken uns alsoauf die funktionale Sicht, nach der jedes Programm eine Funktion implementiert.

    Funktionen höherer Ordnung, Funktionale

    Wir kennen eine Vielzahl von Bezeichnungen für Werte: 1, 2.0, π, ... und solche für Funktionen: +, √, ....Funktionen werden auf Werte angewendet und man erhält neue Werte. Funktions– und Werte–Bezeichner könnenkombiniert und somit zu Bezeichnungen für neue Werte verknüpft werden:

    1 + 7,√2, 2 ∗ π + r2, ...

    Im Vergleich zu der Vielzahl an Möglichkeiten mit Hilfe von Funktionen aus Werten neuen Werte zu erzeugen,ist die Zahl der Möglichkeiten, Funktionen zu neuen Funktionen zu verknüpfen, eher bescheiden. Ein bekanntesFunktional – so nennt man Funktionen verarbeitende Funktionen oft – ist die Hintereinanderausführung. Sind fund g Funktionen, so bezeichnet

    f ◦ g

  • 28 Nichtprozedurale Programmierung

    eine neue Funktion. Die Hintereinanderausführung, “◦”, ist dabei das Funktional, also die Funktion, die aus zweiFunktionen eine neue Funktion macht. Funktionen, die nicht (nur) Werte, sondern auch Funktionen (oder auchFunktionale) verarbeiten, nennt man Funktionen höherer Ordnung. Eine bekannte Funktion höherer Ordnung istmap, die eine übergebene Funktion auf alle Elemente einer Kollektion, z.B. einer Liste, anwendet:

    map(f, < x, y, z >) = < f(x), f(y), f(z) >

    In Python ist map eine vordefinierte Funktion (genauer ein vordefiniertes Funktional) Beispiel:

    >>> def f(x): return 2*x -- Definition der Funktion f...>>> map(f, [1,2,3] ) -- f auf alle Listenelemente anwenden[2, 4, 6]

    Ableitung und Integration sind weitere bekannte Beispiele für Operationen auf Funktionen, also Funktionen höher-er Ordnung, oder Funktionale.

    Funktionsdefinition durch Abstraktion

    Der heutige mathematische Begriff der Funktion ist in einem Prozess der Klärung und Reduktion in den letzten200 Jahren entwickelt worden, also für mathematische Verhältnisse sehr jung. Für die Mathematiker des 18–tenJahrhunderts war – genau wie für uns normale Nicht–Mathematiker – eine Funktion nichts anderes, als eine durcheine Formel ausgedrückte Abhängigkeit zwischen Werten.10 So bezeichnet der Ausdruck

    x2 + 2x− 3einen bestimmten Wert, wenn dem Symbol x ein Wert zugeordnet wird. Der Wert des Ausdrucks hängt vom Wertvon x ab. “Abstrahiert man von x”11, so gelangt man zur charakteristischen Eigenschaft des neuen Ausdrucks: erbeschreibt ein Verfahren, um aus einem Wert einen anderen zu berechnen. Die übliche Notation für eine solcheVerfahrensvorschrift ist:

    f(x) = (x2 + 2x− 3)Hier wird

    • eine Funktion durch Abstraktion definiert und gleichzeitig

    • dieser Funktion ein Name gegeben.

    Beide Prozesse trennend, hätte man schreiben können:

    f = [x → (x2 + 2x− 3)]um dam auszudrücken, dass f etwas ist, das einem beliebigen x den Wert (x2 + 2x − 3) zuordnet. In der Mathe-matik ist eine solche Trennung zwischen Definition und Benennung der Funktion weder üblich noch notwendig,genauso in gängigen Programmiersprachen. Unklarheiten können zwar leicht auftreten: Was ist beispielsweise mitder Gleichung

    g(x) = h(x)12

    gemeint? Sind hier zwei Werte (g(x) und h(x)) oder zwei Funktionen (g und h) gleich? Solche Fragen werdenin der Mathematik durch den Kontext oder durch mündliche oder schriftliche Erläuterungen geklärt. In gängigenProgrammiersprachen ist die Sache sowieso klar. Funktionen können nicht verglichen werden können und

    g(x)=h(x)

    ist darum entweder ein Vergleich von Werten, wenn “=” der Vergleichsoperator ist, oder eine Zuweisung.

    Bei einer intensiveren Beschäftigung mit Funktionen – sei es als Mathematiker oder als funktionaler Program-mierer – ist es nützlich, bei Funktionsdefinitionen zwischen Abstraktion und Namensgebung zu unterscheiden

    10 Nach Courant ([7]) war Leibniz der erste der das Wort Funktion in diesem Sinne benutzte11 Auch wenn Abstraktion ein so wichtiges Konzept der Informatik darstellt, so hätte man doch gelegentlich auch andere Begriffe verwenden

    können.12 Informale Alltagsnotation, kein C oder Python–Code. “=” soll hier “gleich” bedeuten!

  • Th Letschert, FH Giessen–Friedberg 29

    und unter Umständen auch beides zu trennen. Eine aus der Mathematik stammende weitverbreitete Schreibweisebenutzt die sogenannte Lambda–Notation zur Darstellung von Abstraktionen13. Wir schreiben statt

    [x → (x2 + 2x− 3)]in Lambda–Notation (λ–Notation):

    λx . x2 + 2x− 3und definieren so unsere Funktion f folgendermaßen:

    f = λx . x2 + 2x− 3f ist die Funktion, die einen Wert x nimmt und daraus den Wert x2 + 2x− 3 erzeugt. Der griechische Buchstabeλ wird “Lambda” ausgesprochen. Er hat keine weitere Bedeutung, als dass er die Funktionsdefinition einleitet unddem formalen Parameter vorangestellt wird.14 Der Ausdruck

    λx . x2 + 2x− 3bedeutet nichts anderes, als

    “Ich bin eine Funktion, leider habe ich keinen Namen, aber wenn man mich mit einem Argument xaufruft, dann liefere ich x2 + 2x− 3.”

    Der Begriff “Abstraktion” wird deutlich, wenn man sich klar macht, dass zwar

    x2 + 2x− 3 und y2 + 2y − 3normalerweise zwei sehr unterschiedliche Werte darstellen, aber

    λx . x2 + 2x− 3 und λ y . y2 + 2y − 3völlig identisch sind. x und y spielen hier die Rolle von Platzhaltern, von Parametern. Man hätte auch jedenbeliebigen anderen Bezeichner nehmen können, es kommt nicht auf ihn an: er wurde “weg–abstrahiert”.

    Im Gegensatz zu den meisten anderen Programmiersprachen erlaubt Python Lambda–Ausdrücke. Sie schreibt manin Python den Ausdruck λx . 2x− 3 einfach als

    lambda x: 2*x - 3

    Selbstverständlich erlauben auch funktionale Sprachen wie Lisp die Verwendung von Lambda–Ausdrücken. InLisp natürlich hübsch mit Klammern dekoriert und sortiert:

    (LAMBDA (x) (- (* 2 x) 3))

    Lambda–Abstraktionen: Funktionen als Werte von Ausdrücken

    Jeder, der auch nur mit minimalster mathematischer Bildung ausgestattetet ist, kann Funktionen definieren, oh-ne das Konzept der Lambda–Abstraktion zu kennen. Alle halbwegs ernst zu nehmenden Programmiersprachenerlauben die Definition von Funktionen. Nur einige wenige Exoten bieten dazu auch das Konzept der Lambda–Abstraktion. Was bringt es also, wenn so viele ohne es auskommen? Was kann mit Lambda–Abstraktionen aus-grückt werden, das nicht auch mit “normalen” Funktionsdefinitionen gesagt werden kann.

    Der entscheidende Unterschied zwischen einer Funktionsdefinition wie

    def f (x):return 2*x - 3

    13 Wir unterscheiden die Lambda–Notation und den Lambda–Kalkül. Die Lambda–Notation ist einen Art Funktionsdefinitionen bequem zunotieren. Der Lambda–Kalkül ist eine, in den 40–er Jahren des 20–ten Jahrhunderts entwickelte mathematische Theorie der Funktionen alsRegeln statt – wie sonst in der Mathematik – als Graphen (Tupel-Mengen). Er diente (auch) der Untersuchung von Fragen der Berechenbar-keit. (Alles was eine Turingmaschine kann, lässt sich mit λ–Ausdrücken beschreiben und umgekehrt.) Die Lambda–Notation wurde mit demLambda–Kalkül entwickelt, ist aber nur eine triviale Notation innerhalb der Darstellung einer ausgefeilten mathematischen Theorie.

    14Die Tatsache, dass ein griechischer Buchstabe verwendet wird, macht die Sache nicht zu komplizierter Mathematik. Also keine Angst – esist alles ganz einfach. Auch normale Menschen können es verstehen.

  • 30 Nichtprozedurale Programmierung

    und der Verwendung eines Lambda–Ausdrucks wie in

    f = lambda x: 2*x - 3

    ist,

    • dass im ersten Programmstück eine Funktion per Definition eingeführt wird

    • und im zweiten Programmstück die gleiche Funktion der Wert eins Ausdrucks ist.

    Definitionen verbinden einen Bezeichner statisch (d.h. zur Übersetzungszeit) mit einem Wert. Im ersten Beispielwird der Bezeichner f zur Übersetzungszeit mit der Funktion verbunden. Ein Ausdruck wird dagegen zur Lauf-zeit berechnet. Im zweiten Beispiel wird darum zur Laufzeit der Wert des Ausdrucks lambda x: 2*x - 3berechnet und der Variablen f zugewiesen. Das Ergebnis beider Aktionen ist sehr ähnlich: f wird mit dem Wert“λx . 2∗x−3”15 verbunden. Einmal durch eine (statische) Definition, einmal durch eine (dynamische) Zuweisung.In den meisten Sprachen können Funktionen nur definiert und nicht berechnet und zugewiesen werden. In die-sen Sprachen ist der Umgang mit Funktionen darum beschränkt. 16 Prozedurale Sprachen akzeptieren diese Be-schränkung. Die Verarbeitung der Programme im Rechner und im Geist der Programmierer wird durch diesenVerzicht auf Ausdrucksmöglichkeiten vereinfacht. Die Kernidee des funktionalen Programmierens besteht aberdarin, sie aufzuheben und auch Funktionen als gleichberechtigte Werte zu behandeln und dann zu sehen, welcheProgramme, Programmier– und Denkstile damit möglich werden. Der erste Schritt dazu ist, dass Funktionen alsdynamisch erzeugbare Werte behandelt werden und dazu braucht man Ausdrücke, deren Werte Funktionen sind.

    Natürlich kann jeder implementierbare Algorithmus auch ohne die Verwendung von Lambda–Abstraktion imple-mentiert werden. Aber genauso kann er auch ohne OO–Konzepte, ohne Funktionen, ohne Prozeduren, ohne Da-tentypen in reinem Maschinencode, wenn es sein muss in dem der Turingmaschine, implementiert werden. WelcheKonzepte ein Programmierer einsetzt ist eine Geschmacksfrage. Wieviele er kennt und einsetzen kann, bestimmtallerdings ganz entscheidend die seine Produktivität.

    Gebundene und freie Variable

    Im Körper einer (Funktions–) Abstraktion, also in dem Ausdruck der abstrahiert wurde (das hinter dem Punkt), istder Parameter eine gebundene Variable. Nicht gebundene Variablen heißen frei (siehe Abbildung 1.11).

    λ x . x + y − 3Körper der Abstraktion

    Abstraktion

    gebundene Variable

    freie Variable

    Parameter

    Abbildung 1.11:

    Gebundene Variablen sind an eine Bedeutung, eine Aufgabe, gebunden. Die Variable x ist in

    λx . x+ y − 3an ihre Aufgabe als Parameter gebunden. Dagegen ist y nicht gebunden, es hat keine festgelegte Bedeutung oderAufgabe in diesem Ausdruck. Es ist eine freie Variable: Der Kontext “ist frei” es in beliebiger Weise zu interpre-tieren. Fassen wir zusammen: Innerhalb eines Ausdrucks gibt es gebundene Variablen und freie Variablen:

    15 Ja, “λx . 2 ∗ x − 3” ist ein Wert! Kein einfacher Wert, aber ein Wert, ein Wert der eine Funktion ist. Ja das gibt’s. Wir müssen uns hierdaran gewöhne