
Swift und Scala – Ein Vergleich
In den letzten Monaten konnten wir einige Erfahrungen mit Apples Swift 1.0 bis 1.2 für eine Mobile Applikation in iOS sammeln. Scala war uns schon vorher bekannt und wurde in diesem Projekt – zusammen mit Play und Scala.js – für ein Web-basiertes Back-End eingesetzt.
Da Swift auf den ersten Blick mit seiner aufgeräumten und kompakten Syntax stark an Scala erinnert, gibt es hier zunächst eine Gegenüberstellung der wichtigsten Sprachmerkmale. Danach folgt eine Zusammenfassung der “Developer Experience” (endlich gibt es ein Wort für die User Experience als Entwickler :)) mit beiden Sprachen im jeweiligen Mikrokosmos, d.h. im praktischen Einsatz mit IDE und APIs.
| Sprachmerkmal | Scala | Swift |
|---|---|---|
| Typinferenz | ✅ | ✅ |
| Newline trennt Befehle (Semikolon überflüssig) | ✅ | ✅ |
| Implizite Typkonvertierungen * | ✅ | ❌ |
| Standard Zugriffslevel (Access Modifier muss nur angegeben werden, falls es vom definierten Standard abweicht) | ✅ | ✅ |
| Funktionen als “Elemente erster Klasse” (Funktionen sind als Argumente und als Rückgabewert möglich. Ebenso die Erzeugung einer Funktion und Zuweisung zu einer Variablen zur Laufzeit) | ✅ | ✅ |
| Closures | ✅ | ✅ |
| Curried functions | ✅ | ✅ |
| Nested Functions | ✅ | ✅ |
| Newline trennt Befehle (Semikolon überflüssig) | ✅ | ✅ |
| Benannte Parameter * | ✅ | ✅ allerdings als fester Teil der Methoden-Signatur |
| Optionals * | ✅ über die Option[T] Klasse | ✅ mit dedizierter Syntax |
| Switch-Befehl mit Pattern Matching | ✅ | ✅ |
| String-Interpolation | ✅ s”Hello $name” | ✅ “Hello \(name)” |
| Keyword für Variablendefinition | var radius = 2 | var radius = 2 |
| Keyword für unveränderliche Variablen | val pi = 3.14 | let pi = 3.14 |
| Array literal | Array(1,2,3) | [1,2,3] |
| Map literal | Map(1->”a”, 2->”b”) | [1:”a”, 2:”b”] |
| If condition muss vom Typ boolean sein | ✅ | ✅ |
| Tupel | ✅ (ohne benannte Elemente) | ✅ |
| Ranges | for i 0 until 4 | for i in 0…40..<4 |
| Konstructor Syntax | def this() | init() |
| Getter/Setter-Aufruf wie einfacher Variablenzugriff | ✅ | ✅ mit Observer (willSet() didSet() events) |
| Interfaces | trait | protocol |
| Generics | ✅ | ✅ |
| Erweiterung bestehender Typen | ✅ | ✅ |
| Struct | ❌ | ✅ |
| Enum | Indirekt per “Enumeration” Klasse | ✅ |
| “Any” Typ | Any, AnyVal, AnyRef | Any, AnyObject (eine Instanz eines Klassentyps) |
| Zugriff auf eine Instanz eines eigenen Typs per Schlüssel in Klammern, wie bei Arrays oder Maps üblich | obj(index) ruft die Methodeobj.apply aufobj(index) = newValueruft die Methode obj.update(index,newValue) auf | subscript(i:T) -> T2{get {…}set(newValue) {…}} |
| Memory Management | JVM Garbage Collection | Automatic Reference Counting |
Implizite Typkonvertierungen
In Swift muss jede Typkonvertierung explizit programmiert werden. Sogar bei unterschiedlichen numerischen Typen innerhalb eines arithmetisches Ausdrucks wird die Konvertierung nicht vorgenommen, im Gegensatz zu Scala und auch Java. So führt der folgende Code beispielsweise zu einem Compiler-Fehler:
In Swift muss jede Typkonvertierung explizit programmiert werden. Sogar bei unterschiedlichen numerischen Typen innerhalb eines arithmetisches Ausdrucks wird die Konvertierung nicht vorgenommen, im Gegensatz zu Scala und auch Java. So führt der folgende Code beispielsweise zu einem Compiler-Fehler:
1 | let a = 1.0 |
|---|---|
| 2 | let b = 2 |
| 3 | let c = a + b // compiler error : cannot invoke ’+’ with an argument list of type ’( @lvalue Double , @lvalue Int) |
Damit der Variablen c eine 3 als Integer zugewiesen wird, muss a nach Integer konvertiert werden. Für eine 3 als Double, muss der Summand b nach Double konvertiert werden.
1 | let c = Int(a) + b // ok , c is 3 Int |
|---|---|
| 2 | let d = a + Double (b) // ok , d is 3.0 Double |
Obwohl es anfangs etwas frustrierend sein kann, Compiler-Fehler dieser Art zu begegnen, gewöhnt man sich doch recht schnell daran, Variablen in den gewünschten Typ zu casten. Ein Vorteil dieses Ansatzes ist, dass der Entwickler den Ergebnis-Typ sofort sieht, ohne sich sprachspezifische Regeln merken zu müssen.
Im Gegensatz dazu hat Scala ein mächtiges Konzept für implizite Typkonvertierungen. Zusammen mit Operator-Overloading ermöglicht es dem Entwickler, schöne interne DSLs (Domain Specific Languages) zu entwerfen, die an natürlicher Sprache erinnern. Allerdings schrieb auch M. Odersky in seinem Buch “Programming in Scala: A Comprehensive Step-by-step Guide.” 2010 dazu “… bear in mind that with power comes responsibility. If used unartfully, both operator methods and implicit conversions can give rise to client code that is hard to read and understand.”.
Optionals
Variablen kann in Swift standardmäßig nicht der Wert null (bzw. nil, wie das Schlüsselwort in Swift heisst) zugewiesen werden. Der folgende Code lässt sich somit nicht kompilieren:
1 | var name = "Swift" |
|---|---|
| 2 | name = nil // compile time error : Type ’String ’ does not conform to protocol ’NilLiteralConvertible ’ |
Das bedeutet, dass jederzeit auf die Variable “name” zugegriffen werden kann, ohne Gefahr zu laufen, dass eine NullPointerException bzw. ein “nil runtime error” generiert wird.
Um einer Variablen nil zuzuweisen zu können, muss bereits bei der Deklaration der entsprechende Variablentyp mit einem “?” Postfix gewählt werden.
1 | var name : String ? = "Swift" |
|---|---|
| 2 | println(name) // prints ’Optional("Swift") ’ to the console |
| 3 | println(name!) // prints ’Swift’ |
| 4 | name = nil // ok |
| 5 | let statement = name + " is great" // compile time error: value of optional type ’String?’ not unwrapped |
| 6 | println(name!) // runtime error: unexpectedly found nil while unwrapping an Optional value |
Der erste println-Befehl in Zeile 2 gibt ‘Optional(“Swift”)’ aus, weil der String Wert innerhalb des Optionals eingebettet ist und zunächst per !-Operator (=”forced upwrapping”) entpackt werden muss. Der Versuch, auf diese Weise auf ein Optional mit dem Wert nil zuzugreifen, führt zum altbekannten Laufzeitfehler (Zeile 6).
Eine sehr angenehme und kompakte Art, auf Funktionen oder Eigenschaften von Optionals zuzugreifen ist durch das sogenannte “Optional Chaining” möglich. Nehmen wir beispielsweise an, ein Objekt “circle” hat einen optionalen benutzerdefinierten Stil, der einen Rand enthalten kann, der wiederum optional eine benutzerdefinierte Farbe hat. Um die Existenz der benutzerdefinierten Farbe zu prüfen, müsste man ohne Optional Chaining schreiben:
1 | if circle != nil && circle.style != nil && circle.style.border != nil && circle.style.border.color != nil |
|---|
Mit Optional Chaining ist folgende Abfrage möglich
1 | if circle?.style?.border?.color != nil |
|---|
Falls irgendein Element dieser Kette nil ist, wird die Kette ohne Laufzeitfehler abgebrochen und ergibt den Wert nil.
Ein weiteres interessantes Konzept in Zusammenhang mit Optionals sind Konstruktoren, die fehlschlagen können (In Swift “Failable Initializer” genannt). Ein solcher Konstruktor wird mit dem Schlüsselwort init? definiert, und gibt als Rückgabewert die Optional-Variante der Klasse zurück, die er konstruieren bzw. initialisieren sollte. Das ist hilfreich, um beispielsweise unzulässige Konstruktor-Parameter oder Initialisierungsprobleme zu behandeln.
Optionals werden in Swift und in den iOS APIs sehr häufig verwendet. Die Syntax mit den “?” und “!” ist eines der ersten Dinge, die einem beim ersten Kontakt mit Swift ins Auge fällt. Meiner Meinung nach hilft die Nutzung von Optionals, sich bewusster über die An- oder Abwesenheit von Werten zu sein und beim programmieren vorsichtiger zu sein. Natürlich können Optionals nur dann einen echten Mehrwert liefern, wenn diese nicht einfach nur blind entpackt werden um alle Compilerfehler verschwinden zu lassen.
Ausnahmebehandlung
Eine Ausnahmebehandlung existiert in Swift schlichtweg nicht – es gibt keine try/catch oder ähnlichen Konstrukte. Leider gibt es auch keine offizielle Erklärung von den Sprachdesignern zu dieser Entscheidung.
Als (nicht vollwertige) Alternative können Tupel verwendet werden, um einen optionalen Rückgabewert im Erfolgsfall mit einem optionalen Fehler-Objekt, der weitere Details enthält, zurückzugeben. Die Signatur einer solchen Funktion sähe dann beispielsweise so aus:
1 | func doSomething(param: String) -> (result: String?, error: NSError?) |
|---|
Eine weitere Alternative ist es, ein Enum mit einem Success und einem Error Member als Rückgabe-Typ zu benutzen. Per Switch-Statement und Pattern Matching kann dann zwischen dem Fehler- und dem Erfolgsfall unterschieden und gleichzeitig der eigentliche Rückgabewert einer Variablen zugewiesen werden.
1 | enum Result {case Success(res :Int) |
|---|---|
| 2 | case Error(msg: String)} |
| 3 | // This is an exemplary function returning a Result |
| 4 | func next() -> Result { |
| 5 | return Result.Success(res :3)} |
| 6 | // The next function can be used like thisswitch next() {case .Error(let msg):println("Something went wrong:" + msg)case .Success(let res):println ("OK: " + String(res))} |
Diesen Alternativen fehlt allerdings die Fähigkeit, einige Level des Call-Stacks gleichzeitig zurückzubauen, ohne ein Fehler-Objekt explizit die Aufrufkette Level für Level nach oben reichen zu müssen.
Benannte Argumente
In Scala können Funktionen und Methoden optional auch unter Benutzung des Parameternamens aufgerufen werden. Dies kann die Bedeutung der einzelnen Argumente beim Aufruf verdeutlichen und ist außerordentlich nützlich in Kombination mit Standardparametern. Beispielsweise kann beim Aufruf lediglich der zuletzt definierte Parameter übergeben werden falls die führenden Parameter Standardwerte besitzen.
Swifts Parameternamen funktionieren komplett anders und können ziemlich verwirrend sein. Eine Funktion bzw. Methode kann externe Parameternamen haben, die sich vom internen Namen unterscheiden (vgl. The Swift Programming Language. 1st. Swift Programming Series. Apple Inc., 2014, Kapitel “Local and External Parameter Names for Methods / for Functions” und Kapitel “Initialization Parameters”). Mehrere Regeln, die sich für Funktionen, Methoden und Konstruktoren unterscheiden, definieren, wann die Parameternamen Teil der Signatur werden und beim Aufruf zwingend angegeben werden müssen.
Im Gegensatz zu Scala kann trotz benannter Argumente deren Reihenfolge beim Aufruf nicht geändert werden. Ebenso nicht möglich ist das überspringen von führenden Parametern, die einen Standardwert haben.
Dieser Aspekt ist bei Scala meiner Meinung nach eindeutig schöner gelöst – ich verstehe aber, dass es eine Altlast von Objective-C ist, wo auch schon externe Parameternamen Teil der Methoden-/Funktionssignatur sein konnten.
Das Vermächtnis von Objective-C
Der schwierigste Aufgabe beim Erlernen einer neuen Programmiersprache ist nicht das Lernen der Syntax und Grammatik an sich, sondern die Einarbeitung in die zugrunde liegende Standardbibliothek und die vielzahl individueller APIs für alle Möglichen Aufgaben, die eine Platform heutzutage bewältigen kann. Swift benutzt alle bereits existierenden Objective-C Bibliotheken, deren Schnittstellen automatisch in Swift übersetzt werden. Dabei werde eine Reihe von Regeln angewendet, um die Namensgebung von Methoden und Parametern zu verbessern.
So wird beispielsweise aus
1 | UITableView *myTableView = [[UITableView alloc] initWithFrame: CGRectZero style: UITableViewStyleGrouped]; |
|---|
in Objective-C der folgende Aufrunf in Swift.
1 | let myTableView = UITableView(frame: CGRectZero, style: .Grouped) |
|---|
Der “initWith” Präfix des ersten Parameters wurde vom Swift Compiler automatisch entfernt, damit dieser besser in die Syntax zur Objektinitialisierung in Swift passt. Der Enum Wert “Grouped” wurde ebefalls ohne das gemeinsame Präfix “UITableViewSyle” nach Swift übersetzt (vgl. “Using Swift with Cocoa and Objective-C. Swift Programming Series. Apple Inc., 2014).
Die automatische Übersetzung vorhandener APIs führt aber auch dazu, dass schlechte Konzepte aus den Objective-C APIs in Swift eins zu eins übernommen werden. Ein solches Konzept sind die sogenannten Selektoren: Strings, die eine Callback-Methode rein über Methoden- und Parameternamen identifizieren. Über diesen String wird die Methode von der Bibliothek auf dem übergebenen Objekt aufgerufen. Dazu kommt, dass in Swift die Klasse, deren Methode über einen Selektor aufgerufen werden soll, mit dem Attribut (analog Annotation in Java) “@objc” markiert werden muss, so dass die Klasse für die Objective-C Bibliothek sichtbar wird. Wird das Attribut vergessen oder gibt es einen Tippfehler bzw. ein vergessenes “:” im Selektor-String, kann der Code trotzdem problemlos kompiliert werden. Erst zur Laufzeit stürzt die Applikation mit einem Fehler ab.
Es wäre sehr förderlich für Swift, die vorhandenen Objective-C APIs sauber nach Swift zu portieren, und dabei Selektoren durch Funktionen als Parameter oder Closures zu ersetzen.
Durch den breiten Einsatz vorhandener Objective-C (Standard-)Bibliotheken, bedeutet der Einstieg mit Swift in die iOS Entwicklung automatisch – zumindest zu einem gewissen Grad – auch das Lernen von Objective-C.
Fazit: Swift und XCode
Swift ist aktuell die modernste native Programmiersprache, die für die Entwicklung auf einer Mobile Computing Platform verfügbar ist. Trotz seiner übersichtlichen und kompakten Syntax und Tools wie dem Playground oder der REPL (Read-Eval-Print-Loop), bleibt Swift eine kompilierte Sprache. Wie Objective-C wird Swift vom LLVM compiler direkt in Maschinencode übersetzt. Der Compiler arbeitet dabei angenehm schnell – verglichen mit der Geschwindigkeit von frühen Versionen des Scala Compilers liegen Welten dazwischen.
Da es sich um eine sehr junge Sprache handelt, ist das gesamte Tooling noch nicht sehr ausgereift und stabil. So verabschiedet sich Apples XCode IDE mehrmals am Tag, unterstützt noch kein Refactoring für Swift und die Code-Completion funktioniert oft nicht. Auch die Fehlermeldungen des Compilers sind oft kryptisch und nicht zielführend. Manche Sprachkonstrukte werden sogar mit einem “Not yet implemented”-Fehler quittiert.
Trotz dieser Probleme ist Swift eine tolle Sprache mit viel Potential, und ist jetzt schon ein riesen Schritt nach Vorne im Vergleich mit der Vorgängersprache Objective-C. Wichtig ist, dass die Apple-Entwickler die Standardbibliothek weiterentwickeln, Plattform APIs für Swift neu implementieren und sich dabei von veralteten Konzepten wie Selektoren verabschieden.
Swift vs. Scala
Beide Programmiersprachen gehören zu den Besten, die ich bis heute kennen lernen durfte. Wenn ich mich für eine Sprache entscheiden müsste, würde ich Scala wählen. Probleme können in Scala eleganter gelöst werden, mit der vollen Flexibilität seiner impliziten Konvertierungen, impliziter Parameterlisten, der Infix-Notation, partiellen Funtionen als case-Sequenzen und Konzepte zur asynchronen Codeausführung wie Futures und Promises. Ich schätze die zusätzliche Ausdrucksstärke, die diese Elemente ermöglichen, sehr und finde es nicht überzeugend, diese erst gar nicht anzubieten, weil die Gefahr besteht, dass sie falsch angewendet zu schwerer lesbarem Code führen können.
Meiner Meinung nach ist die Qualität und der Erfolg einer Sprache auch eng mit ihrer Standardbibliothek verknüpft. Aktuell bietet Scala die bessere Standardbibliothek. Sie ist besser komponiert, gewohnte Konzepte findet man als Teil anderer wieder. Sprachkonstrukte, die zunächst wie eine dedizierte Syntax wirken, entpuppen sich mit besserem Verständnis der Sprache zu Elementen der Standardbibliothek. Auch der Umfang der Standardbibliothek ist weit größer als in Swift, wie allein am Beispiel der Collections klar wird. Während Scala verschiedenste Implementierungen allein für das Map trait anbieten (HashMap, TreeMap, ListMap etc. – jeweils mutable und immutable), gibt es in Swift nur zwei Collections: Dictionary (=Map) und Array. Mit Swift 1.2 ist immerhin eine Set-Collection hinzugekommen.
Das hinterlässt für Scala den Eindruck einer mächtigen, runden und konsistenten Sprache.
Es bleibt aber sehr spannend zu sehen, wie die Sprache Swift, die Standardbibliothek und das Tooling weiterentwickelt wird. Wir werden bei contexagon am Ball bleiben und setzen für die Entwicklung Mobiler Apps für iOS weiterhin auf Swift.