Letztes Jahr hat Apple mit Swift eine neue Programmiersprache vorgestellt. Über kurz oder lang wird Swift die Standardsprache werden, um Apps für iPhone, iPad, Mac und Co zu entwickeln. Und Swift enthält viele Elemente der funktionalen Programmierung, Zeit genug also, dass wir in diesem Blog mal einen genaueren Blick auf die Sprache werfen.

Interessant ist, dass Apple in Swift nicht nur einfach ein paar liebgewonnene Features aus funktionalen Sprachen eingebaut hat. Nein, es scheint vielmehr, dass grundlegende funktionale Designparadigmen wie „Werte statt veränderbare Objekte“ und „Seiteneffekte ja, aber mit Disziplin“ auch in Apples Vorstellung von guter Softwarearchitektur eine große Rolle spielen. Exemplarisch seien hier zwei Vorträge der Apple Worldwide Developers Conference genannt. Im Vortrag Building Better Apps with Value Types in Swift geht es um die Vorteile von Werttypen (value types) in Swift, also von Typen deren Werte unveränderbar sind. Und der Vortrag Advanced iOS Application Architecture and Patterns schlägt in dieselbe Kerbe, hier geht es ebenfalls um Werttypen und Unveränderbarkeit (immutability).

Im heutigen Blogartikel schauen wir uns anhand eines Beispiels Swift etwas genauer an. Wir möchten eine kleine Bibliothek zum Zeichnen von Diagrammen designen und implementieren. Damit können wir dann z.B. solche Diagramme zeichnen:

Das geht natürlich auch mit herkömmlichen, imperativen Mitteln. Etwa so:

NSColor.blueColor().setFill()
CGContextFillRect(ctx, CGRectMake(0.0, 37.5, 75.0, 75.0))
NSColor.redColor().setFill()
CGContextFillRect(ctx, CGRectMake(75.0, 0.0, 150.0, 150.0))

Dieser Code benutzt die Mac API zum Zeichnen, aber das Grundprinzip ist in fast allen UI-Toolkits gleich: wir benutzen einen Grafikkontext ctx, um primitive Formen wie Rechtecke und Kreise auf den Bildschirm zu zeichnen. Wir sagen dem System also genau, wie gezeichnet werden soll.

Was passiert nun aber, wenn wir das Diagramm leicht ändern möchten und z.B. eine grünen Kreis zwischen die beiden Rechtecke einfügen wollen?

Mit dem imperativen Ansatz (wie wird gezeichnet) müssen wir nicht nur neuen Code für den Kreis schreiben, sondern wir müssen auch bestehen Code ändern, um das rote Rechteck weiter nach rechts zu schieben:

NSColor.blueColor().setFill()
CGContextFillRect(ctx, CGRectMake(0.0, 37.5, 75.0, 75.0))
// neuer Code
NSColor.greenColor().setFill()
CGContextFillEllipseInRect(ctx, CGRectMake(75.0, 37.5, 75.0, 75.0))
// alter Code, muss verändert werden
NSColor.redColor().setFill()
CGContextFillRect(ctx, CGRectMake(150.0, 0.0, 150.0, 150.0))

Mit einem funktionalen Design werden solche Probleme vermieden. Denn funktional gedacht spezifizieren wir lediglich was gezeichnet werden soll und überlassen das wie einer Bibliothek.

Im Folgenden schauen wir uns an, wie wir in Swift Diagramme deklarativ spezifizieren können und wie wir eine Bibliothek zum Umsetzen der Spezifikation in echte Bilder realisieren können. Die Idee zu diesem Beispiel stammt aus dem schönen Buch Functional Programming in Swift von Chris Eidhof, Florian Kugler und Wouter Swierstra, die Ideen sind aber auch z.B. schon in der Haskell Bibliothek diagrams zu finden.

Spezifikation von Diagramm

Um zu spezifizieren, was in einem Diagram enthalten sein soll, benutzen wir das enum-Feature von Swift. Wir beginnen mit einfachen geometrischen Formen:

enum Shape {
    case Ellipse
    case Rectangle
}

Die Enums können aber mehr als einfach nur verschiedene Fälle zu einem Typen zusammenzufassen. Wir können z.B. auch Werte mit einzelnen Fällen assoziieren. Exemplarisch hierfür definieren wir das Enum Attribut, welches wir später verwenden, um Diagramme einzufärben und um die Anordnung zu spezifizieren.

enum Attribute {
    case FillColor(NSColor)
    case Alignment(Align)
}
// Alignment auf der x- und y-Achse sind Werte zwischen 0 und 1, wobei
// 0 ganz links bzw. oben und 1 ganz rechts und unten bedeutet. Der Wert
// CGPointMake(x: 0.5, y: 1.0) spezifiziert als ein horizontal zentriertes
// und vertikal am unteren Rand fixiertes Alignment.
typealias Align = CGPoint

Es geht aber noch mehr! Enums können auch rekursiv sein, d.h. wir können innerhalb der Definition eines Enums das Enum selbst verwenden. Dazu brauchen wir das Schlüsselwort indirect.

indirect enum Diagram {
    case Primitive(CGSize, Shape)
    case Beside(Diagram, Diagram)
    case Below(Diagram, Diagram)
    case Annotated(Attribute, Diagram)
}

Ein Diagramm ist also entweder eine primitive Form mit einer Größe (die Größe ist nicht in Pixel angegeben, sondern relativ zu den anderen Diagrammelementen gedacht), oder zwei Diagramme neben- bzw. untereinander, oder ein annotiertes Diagramm. Für solche annotierten Diagramme benutzen wir das bereits definierte Enum Attribute.

Enums in Swift sind also viel mächtiger als reine Aufzählungstypen wie beispielsweise in Java oder C#. Das, was Enums in Swift sind, ist in funktionalen Sprache Standard; sie heißen dort algebraische Datentypen oder auch Summentypen.

Beispiel-Diagramme

Nun schauen wir uns an, wie wir mit diesen Enums ein Diagramm spezifizieren können:

let blueSquare = Diagram.Annotated(Attribute.FillColor(.blueColor()),
    Diagram.Primitive(CGSize(width:1, height:1), Shape.Rectangle))

Mit let führen wir eine neue Variable blueSquare ein, deren Wert nicht verändert werden kann. Diagram.Primitive erzeugt ein neues primitives Diagramm, dem wir dann mit Diagram.Annotated eine Farbe verpassen.

Das obige Diagram blueSquare ist sehr einfach und besteht nur aus einem blauen Quadrat. Trotzdem ist der Code zum Erzeugen etwas länglich. Wir können ihn vereinfachen, indem wir Hilfsfunktionen bereitstellen. In OO-Sprachen sagt man zu solchen Funtionen oft „smarte Konstruktoren“, in funktionalen Sprache werden sie auch „Kombinatoren“ genannt. Wir starten mit smarten Konstruktoren für einfache Formen.

func square(side: CGFloat) -> Diagram {
    return Diagram.Primitive(CGSize(width:side, height:side), Shape.Rectangle)
}

func circle(radius: CGFloat) -> Diagram {
    return Diagram.Primitive(CGSize(width:2*radius, height:2*radius), Shape.Ellipse)
}

func rectangle(width: CGFloat, height: CGFloat) -> Diagram {
    return Diagram.Primitive(CGSize(width:width, height:height), Shape.Rectangle)
}

Nachfolgend definieren wir auch smarte Konstruktoren für Farbe und Alignment.

extension Diagram {
    func fill(color: NSColor) -> Diagram {
        return Annotated(Attribute.FillColor(color), self)
    }
    func align(x: CGFloat, y: CGFloat) -> Diagram {
        return Annotated(Attribute.Alignment(CGPoint(x:x, y:y)), self)
    }
    func alignRight() -> Diagram {
        return align(1, y:0.5)
    }
    func alignTop() -> Diagram {
        return align(0.5, y:1)
    }
    func alignBottom() -> Diagram {
        return align(0.5, y:0)
    }
}

Wir haben diese Funktionen als Erweiterung (extension) von Diagram geschrieben, damit wir die „Dot-Notation“ verwenden können, um die Funktionen wie Methoden auf einem Diagram aufzurufen. Innerhalb einer solchen extension verwenden wir wie immer self, um auf das Diagram zuzugreifen, auf dem die Methode aufgerufen wurde.

Wir sehen die Dot-Notation gut an folgendem Beispiel:

let redSquare = square(2).fill(NSColor.redColor())

Wir konstruieren zuerst ein Quadrat, um dann auf dem resultierenden Diagramm fill aufzurufen. Wenn wir fill als globale Funktion geschrieben hätten, müssten wir stattdessen so etwas schreiben: fill(NSColor.redColor(), square(2)). Welche der beiden Schreibweisen wir wählen ist Geschmacksache, ich habe mich für die Dot-Notation entschieden, weil sie meiner Ansicht nach zu leichter lesbarem Code führt.

Es fehlen noch smarte Konstruktoren zur Platzierung von Diagrammen nebeneinander- bzw. untereinander. Diese realisieren wir als Operatoren. (Auch diese Entscheidung ist Geschmacksache, wir hätten genauso gut normale Funktionen verwenden können.) Die Operatoren sind dabei ||| für nebeneinander und --- für untereinander.

infix operator ||| { associativity left }
func ||| (l: Diagram, r: Diagram) -> Diagram {
    return Diagram.Beside(l, r)
}

infix operator --- { associativity left }
func --- (top: Diagram, bottom: Diagram) -> Diagram {
    return Diagram.Below(top, bottom)
}

Durch die associativity Notation lassen wir den Swift-Compiler wissen, dass er einen Ausdruck ohne Klammern wie z.B. blueSquare ||| redSquare ||| diag1 als (blueSquare ||| redSquare) ||| diag1 verstehen soll.

Jetzt können wir noch ein paar mehr Diagramme definieren:

let greenCircle = circle(0.5).fill(.greenColor())
let sampleDiagram1a = blueSquare ||| redSquare
let sampleDiagram1b = blueSquare ||| greenCircle ||| redSquare
let sampleDiagram2 =
    sampleDiagram1b.alignBottom() ---
    rectangle(10, height:0.2).fill(.magentaColor()).alignTop()

Die Diagramme sampleDiagram1a und sampleDiagram1b haben wir bereits weiter oben als Bilder gesehen. Das letzte Diagram sampleDiagram2 demonstriert die Verwendung von ---. Wir benötigen alignBottom und alignTop, damit der obere Teile des Bilds sampleDiagram1b und das langgezogene, magentafarbene Rechteck direkt übereinander liegen. So sieht sampleDiagram2 dann aus:

Bis jetzt haben wir gesehen, wie man mittels Enums Diagramme einfach und kompakt repräsentieren kann. Wie man diese Diagramme dann auf den Bildschirm zeichnet, das werden wir in einem Folgeartikel diskutieren. Bis dahin freue ich mich über Rückfragen und anderes Feedback. Viel Erfolg beim funktionalen Programmieren in Swift!

Übrigens: wir arbeiten gerade daran, den Objective-C Code für unser Produkt Checkpad MED zumindest teilweise auf Swift zu aktualisieren. Falls Sie Lust und Interesse haben, daran mitzuhelfen, dann freuen wir uns auf Ihre Bewerbung.