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.