2.2 Miranda

In dem Buch Functional Programming with Miranda [FPWithMiranda] wird nicht nur die rein-funktionale Programmiersprache Miranda vorgestellt, es geht auch darum, verständlichen Quelltext zu schreiben, der an der Mathematik angelehnt ist. Dazu wird beispielsweise auf Funktionen und Rekursion zurückgegriffen.

Programme, die in funktionalen Programmiersprachen geschrieben sind, bestehen aus Funktionen, die Eingabe-Werte auf Ausgabe-Werte abbilden. Das Attribut rein-funktional fordert, dass alle Funktionen referenziell transparent sind.

Gleichungen bestehen aus dem Musterabgleich (Pattern matching) auf der linken Seite und der Berechnungsvorschrift auf der rechten Seite.

Dies stellt den Bezug zur Definition Formulieren von Regeln und Zusammenhängen her: Regeln werden durch Muster auf der linken Seite formuliert und die rechte Seite stellt den funktionalen Zusammenhang dar. Es können für eine Funktion mehrere Gleichungen geschrieben werden, um Fälle zu unterscheiden. Wie in anderen funktionalen Programmiersprachen werden anstelle von Schleifen-Konstrukten rekursive Funktionen verwendet.

Referenzielle Transparenz verhindert Seiteneffekte Eine Manifestation von Seiteneffekten ist das Verändern des Zustandes eines Objektes, welches unter einem Alias, d.h. mehr als einem Namen [SICP, S. 295] zugreifbar ist. und führt zur Gleichbehandlung von Gleichheit und Identität von Werten. Außerdem bringt sie noch den Vorteil der Wiederverwendung von Datenstrukturen. Nur wenn auf Seiteneffekte verzichtet wird, ist verzögerte Auswertung (laziness) sinnvoll, welche wiederum die Arbeit mit unendlichen Listen ermöglicht. Gleichzeitig werden änderbare Arrays als wichtigste imperative Datenstruktur verhindert [FPWithMiranda, S. 93]Stattdessen werden oft verschiedene Arten von Bäumen verwendet.. Eine gute Übersicht, wie mit dem Problem umgegangen wurde und welche Arten von funktionalen Arrays entwickelt wurden, gibt [FuncArrays, S. S. 2-3]. In neueren multiparadigmatischen Sprachen wie F# und Scala gibt es jedoch die Möglichkeit, auf imperative Arrays wie in C# oder Java zuzugreifen, weshalb nicht weiter auf funktionale Arrays eingegangen wird.

Miranda verfügt als rein-funktionale Programmiersprache nicht über Zuweisungen. Stattdessen gibt es Variablenbindungen, die einen Bezeichner an einen Wert binden. Dank Laziness wird der Wert erst dann berechnet, wenn er wirklich zur Ermittlung eines Berechnungsergebnisses benötigt wird.

Eine Besonderheit der Sprache Miranda ist, dass auf stukturierende Klammern wie etwa in if (Condition) { ThenBlock } else { ElseBlock } verzichtet wird. Stattdessen wird die sogenannte off-side rule verwendet, in der die Einrückungstiefe den Geltungsbereich von Variablen bestimmt, sodass gänzlich auf strukturierende Block-Klammern verzichtet werden kann.Die off-side rule wird ebenfalls in F# verwendet und bezieht sich dort unter anderem auf die Einrückungstiefe bei let-Bindungen. [FSharpSpec, vgl. S. 232ff.]

Eine zweite Besonderheit ist die Typinferenz, dank derer auf Typ-Annotation verzichtet werden kann, wodurch der Code von Typisierungs-Details befreit wird. Allein aus der Verwendung von Parametern kann deren Typ abgeleitet werden. Werden im Quellcode keine besonderen Operationen mit Werten durchgeführt, die auf den Typ schließen lassen, bleibt der Typ offen und funktioniert für alle konkreten Typen (generische Programmierung).

Diese Eigenschaften führen zu schönen Programmen und Programmbestandteilen, wie etwa die bekannten Funktionen Map, Filter, Fold, die viele Iterationsaufgaben elegant lösen. Dazu werden Listen rekursiv verarbeitet, indem eine Funktion auf den Kopf und Rest der Liste hat zugreift und diese zu einem Ergebnis transformiert.

||| Abbildung einer Liste mit Transformationsfunktion
||| Beispiel: map (+1) [1,2,3,4] -> [2,3,4,5]
map f [] = []
map f (head:tail) = (f head) : (map f tail)

||| Filterung einer Liste mit Prädikat
||| Beispiel: filter (>1) [1,2,3,4] -> [2,3,4]
filter pred [] = []
filter pred (head:tail)
= head : (filter pred tail), if pred(head)
= filter pred tail,          otherwise

||| Zusammenfalten einer Liste mit Startwert und 2-stelliger Funktion
||| Beispiel: fold 0 (+) [1,2,3,4] -> 10
fold seed f [] = seed
fold seed f (head:tail) = fold (f seed head) f tail

Die Funktionsweise von Map, Filter und Fold lässt sich wie in Abbildung # illustieren.

mapfilterfold
Vereinfachte Funktionsweise von Map, Filter und Fold [FPWithMiranda, nach S. 42ff.].

Map, Filter und Fold sind sogenannte Funktionen höherer Ordnung, weil sie als Parameter Funktionen aufnehmen (hier f und pred).
Pattern matching wird ähnlich wie in den Prolog-Prädikaten aus Abschnitt Prolog wieder benutzt, um den Rekursionsabbruch darzustellen.

Mit partieller Anwendung von Funktionen (currying) lassen sich aus vorhandenen Funktionen neue Funktionen erstellen, die weniger Argumente benötigen. In dem Beispiel fold 0 (+) [1,2,3,4] wird deutlich, dass man die Summe einer Liste bilden möchte. Mathematiker kennen dafür die (n-stellige) Summe, die mit \sum dargestellt wird. Programmierer sollten auch Zugriff auf dieses Konstrukt haben und könnten sich eine entsprechende Funktion definieren. Für das (n-stellige) Produkt gibt es \prod, welches sich auch definieren ließe. Die Integration von erweiterten Unicode-Zeichen in Programmiersprachen ist noch nicht gelungen, was an Schwierigkeiten bei der Eingabe mit der Tastatur und unflexiblen Parsern liegt.Eine Betrachtung des Themas Unicode in Programmiersprachen findet in [FunktProg, S. 4-5] statt. Daher werden sprechende Namen statt mathematischer Symbole verwendet:

sum = fold 0 (+)    ||| n-stellige Summenfunktion
prod = fold 1 (*)   ||| n-stellige Produktfunktion

Obwohl fold eine Funktion ist, die zur Berechnung drei Argumente benötigt, lassen sich auch weniger Argumente übergeben, sodass eine Funktion konstruiert wird, die nur noch auf das letzte Argument wartet, um das Ergebnis zu berechnen. Zwecks Dokumentation verzichtet man in Funktionsdefinitionen gelegentlich auf diese Eigenschaft und schreibt sum numbers = fold 0 (+) numbers.

Abgesehen von Typen, die später genauer betrachtet werden, sind dies die Kernideen vieler funktionaler Programmiersprachen. Eine kurze Auflistung von Eigenschaften funktionaler Programmierung, mit Miranda als Beispiel für eine rein funktionale Programmiersprache, bietet [FPWithMiranda, S. 8-9]:

  • Algorithmen können funktional mit Klarheit und Kürze formuliert werden.
  • Kürzere Programme sind leichter zu schreiben, zu testen, anzupassen und zu warten.
  • Funktionale Programmiersprachen sind mathematisch formalisierbar und lassen sich durch referenzielle Transparenz leichter parallelisieren.
  • Die Nachteile gegenüber prozeduralen Programmen waren jedoch geringere Verfügbarkeit (der Laufzeitumgebung für Miranda beispielsweise), fehlende Interoperabilität zur Systemprogrammierung (Betriebssystem-, Datenbankbibliotheken) und geringere Performanz.

Die eben genannten Nachteile wurden durch intensive Forschung reduziert und neue funktionale Programmiersprachen lösen diese Anforderungen zufriedenstellend. Die Integration mit anderen Programmiersprachen und Interoperabilität von Programmbibliotheken ist auch durch gemeinsame Frameworks und Laufzeitumgebungen gelungen.