Einführung in Apples neue Programmiersprache Swift, Teil 2

Seite 2: Monaden & Closures

Inhaltsverzeichnis

Wer Swift-Beispiele genauer betrachtet, hat unbewusst das Gefühl, dass hier etwas fehlt. Ausnahmen (Exceptions) und Try-catch-Blöcke gibt es nicht. Swift implementiert stattdessen das Null Object Design Pattern und nennt das Ergebnis Optional, und zwar deshalb, weil es bestehende Datentypen um ein Element erweitert. Bei funktionalen Programmiersprachen ist dieses Pattern ein Beispiel für eine Monade. An einer leicht verständlichen Erklärung von Monaden haben sich schon viele versucht, weshalb der Autor es erst gar nicht probiert, sondern den Schwarzen Peter an Wikipedia weitergibt. Wenn Optional die Lösung ist, was genau ist das Problem? Es sind Abfragen wie die folgende:

// Code aus der OO-Steinzeit:
var channel = receiver.connect("Scotty")
if channel != 0
{
var sendung = receiver.send(channel, "Beam me up, Scotty")
if sending == nil
...
}
else
{
println("Faszinierend!+-")
}

Hier gibt es zwei unterschiedliche Fälle der Ergebnisbehandlung. Im Erfolgsfall, also im then-Zweig, kann der Code die zurückgelieferte Information weiter verarbeiten. Der Fehlerfall ist separat zu behandeln. Er ist im else-Zweig angedeutet.

Oft liefern aufgerufene Funktionen individuell festgelegte Fehlerwerte wie 0, -1 oder 42 zurück. Wünschenswert wären hier mehr Vereinheitlichung und Transparenz. Und genau das leistet Optional. Zu jedem Datentyp gibt es eine Optional-Version. Den Einsatz von Optional-Typen erkennen Entwickler daran, dass dem Basistypnamen ein Fragezeichen (oder ein Ausrufezeichen) angehängt wird. Als Beispiel sei Int? genommen, das als Resultatstyp der Methode connect fungieren soll:

var channel = receiver.connect("Scotty")

Ob channel einen gültigen Wert enthält, lässt sich mittels if-Anweisung überprüfen, weil channel einen booleschen Wert zurückliefert, nämlich bei Fehlern false und ansonsten true:

if channel {
// erfolgreicher Verbindungsaufbau
receiver.send(channel!, "Beam me up, Scotty")
}
else {
// Fehler aufgetreten
}

Am besten sind Optionals verständlich, wenn man sie sich als Aufzählungstyp vorstellt. Entweder ist der Wert eines Optional eine Instanz des gewählten Basistyps wie im Beispiel Int oder nil. Letzteres hat keinen Bezug zu NULL in anderen Sprachen, darf also nur im Kontext von Optionals auftauchen. Beispielsweise ist es nicht erlaubt, einer Referenzvariablen nil zuzuweisen. Ganz im Gegensatz zu NULL, das nicht nur für die Initialisierung von Referenzen essenziell ist. Der Default-Wert bei der Vereinbarung von Optionals ist selbstredend nil.

Beim Lesen des obigen Beispielcodes ist das Optional[ sichtbar, weil zum Zugriff auf den Inhalt von channel ein Ausrufezeichen anzuhängen ist:

receiver.send(channel!, "Beam me up, Scotty")

Um sich das zu ersparen, benutzen Entwickler ein implizit enthülltes Optional (Implicitely Unwrapped Optional). Dazu hängt sie bei der Typvereinbarung beim Datentyp, den sie optional machen möchte, statt des Frage- ein Ausrufezeichen an:

var x : String! = .... 
println(x)

Nun entfällt also das Ausrufezeichen bei jedem Zugriff auf x. Ein Problem bleibt aber noch – das der verketteten Aufrufe. Hierzu ein triviales Ausgangsszenario:

class Kunde {
var adresse : Adresse?
}

class Adresse {
var strasse : Strasse?
}

class Strasse {
var schufaEinschätzung : Int
}

Angenommen, dass zum fehlenden Glück noch Pech hinzukommt, weil alle auftretenden Optional-Typen nicht zur Menge der Implicitely Unwrapped Conditionals gehören. Theoretisch wäre bei der Abfrage der Kreditwürdigkeit daher wie folgt vorzugehen:

var kunde : Kunde? = ...
if kunde { // es gibt sie oder ihn ?
var adresse = Kunde.adresse
if adresse { // Heureka
var strasse = andresse.strasse
if strasse { // Sie haben Ihr Ziel erreicht
return strasse.schufaEinschätzung
}
else { // Fehler in Strasse
}
}
else { // Fehler in Adresse
}
}
else { // Fehler in Kunde
}

Das macht keinen Spaß, weil es in jedem Zweig die Abfrage geben muss, ob sie ein gültiges Ergebnis oder einen Fehler zurückliefert. Daher lässt Swift eine Abkürzung zu, die sich Optional Chaining nennt. Würde sich der Aufrufer zum Beispiel für die Straße eines Kunden interessieren, könnte er stattdessen schreiben:

if let strasse = kunde?.adresse?.strasse {
/* auf Google Maps anzeigen */
}

In diesem Fall meldet der Aufruf entweder ein gültiges Ergebnis oder einen Fehler, der auf dem Weg bis zum letzten Zugriff in der Kette aufgetreten ist. Die Zwischenstufen sind nicht mehr zu behandeln. Statt eines Baums von Abfragen gibt es durch dieses Linearisieren nur noch eine einzige Abfrage.

Das if-let-Konstrukt im obigen Beispiel heißt Optional Binding und schlägt zwei Fliegen mit einer Klappe. Zum einen erfolgt im Konstrukt die Überprüfung, ob überhaupt ein gültiges Resultat vorliegt, also ungleich nil. Zum anderen wird eine lokale Konstante in diesem Fall mit dem Resultat des Funktionsaufrufs initialisiert. strasse ist dann im then-Zweig sichtbar und gültig.

Was ist von diesem Ansatz im Zusammenhang mit Fehlerbehandlung zu halten? Im Internet gibt es zurzeit heftige Diskussionen darüber, ob Exception Handling oder Optionals der bessere Ansatz zur Fehlerbehandlung ist. Für benutzerdefinierte Ausnahmen ist der Optional-Mechanismus jedenfalls eine gute Lösung. Der Autor hegt aber seine Zweifel, dass dies auch für Systemausnahmen ein guter Weg ist.

Neben Funktionen stellt Swift Closures zur Verfügung. Genau genommen bilden Closures eine Obermenge von Funktionen. Sie sind Code-Blöcke mit abgeschlossenem Sichtbarkeitsbereich, die sich wie Funktionen beispielsweise als Parameter oder als Variablen nutzen lassen. Sie können dabei auf alles zugreifen, was in der Nachbarschaft ihrer Vereinbarungsstelle zugreifbar ist, und bilden daher einen abgeschlossenen Gültigkeitsraum. Deswegen die Bezeichnung "Closure". Ihre Anwendung ist überall dort sinnvoll, wo konventionelle Funktionen zu aufwendig wären. Ein typisches Einsatzszenario ist die map-Funktion für Container-Datentypen. Sie iteriert über den übergebenen Container, wendet auf jedes gefundene Element eine Funktion beziehungsweise ein übergebenes Closure an und sammelt die Ergebnisse in einem Container, den sie an den Aufrufer zurückliefert:

let intArray = [0,1,2,3,4,5,6,7]

let squares = intArray.map {
(n) -> Int in
n * n
} // squares = [0, 1, 4, 9, 16, 25, 36, 49]

Ein return ist in diesem Fall nicht nötig, da der Compiler den letzten Ausdruck innerhalb des Closures als Ergebnis interpretiert. n bezeichnet eine lokale Variable, in der das Closure auf den von außen übergebenen Parameterwert zugreifen kann. In Beispiel hier übergibt map das aktuell gelesene Element als Parameter n an das Closure. Das Schlüsselwort in legt den Gültigkeitsbereich des Parameters fest. Closures werden in geschweiften Klammern notiert. Da in diesem Fall der Aufrufparameter von map, (d. h. das Closure) leicht abzugrenzen ist, sind noch nicht einmal die runden Klammen des Funktionsaufrufs notwendig.

Es wäre auch möglich gewesen, eine Funktion square() zu vereinbaren und sie als Parameter an map() zu übergeben. Das ist aber reiner "Overkill", wenn die Funktionalität nur an dieser einen Stelle benötigt wird und daher unnötig den Namensraum aufblähen würde.

Es geht sogar noch eleganter und kürzer, wie die Funktion sort() demonstriert, die Felder von Gleitkommazahlen sortiert. Als erstes Element erwartet die Funktion ein Feld von Gleitkommazahlen, als zweiten Parameter den gewünschten Vergleichsoperator, im nachfolgenden Beispiel den Kleiner-als-Operator:

var result =
sort(doubleArray,{(x:Double, y:Double)->Bool in return x < y})

Der Compiler kann hier unschwer feststellen, welche Signatur das Closure besitzen muss, weshalb sich das Beispiel noch kürzer schreiben lässt:

var result = sort(doubleArray, {x , y in x < y})

Leider ist das immer noch zu lang, und lässt sich jedoch durch nummerierte Argumente weiter abkürzen. $0 steht dabei für das erste übergebene Argument, $1 für das zweite, und so weiter:

var result = sort(doubleArray, {$0 < $1})

So ist das schon erheblich kompakter, aber es gibt noch einen letzten Optimierungsschritt:

var result = sort(doubleArray, < )

Das Prinzip lautet also, den Compiler arbeiten zu lassen statt die Entwickler. Es sollte Entwicklern aber immer bewusst sein: Closures und Funktionen können ausschließlich auf Elemente zugreifen, die an ihrem Definitionsort sichtbar sind, nicht aber auf die in der Ausführungsumgebung.