Observations on Swift

06 Oct 2014

I’m sure not all will agree, but I’ve hinted at having issues with the language for a couple of months now, so I thought I’d try to cover what I’ve been talking about.

Background:

Coming from a C# background, and having dabbled in Obj-C for years, I was excited to see a new language be supported for iOS/OS X development. After having spent some time playing with the language, I’ve noticed a number of percieved shortcomings.

Before I continue, please keep in mind that a lot of this is based on coming from a language that I love, C#, to Swift. I think it’s fair to contextualize a lot of shortcomings and trade-offs in the history and use of Obj-C, but I don’t think Swift should just get a pass, these were solved problems in other languages.

The good:

It’s commendable that Apple has attempted to bridge Swift and Obj-C, and there are certainly cases where this required modifications to the Swift language to support that interop. As such, Swift is a language of trade-offs, and latitude must be given its designers. I also think that one of Swift’s key improvements is strong-typing, which allows for more robust applications - especially where the team and codebase grow too large to keep track of everything in your head.

Swift also attempts to reduce the need to deal with pointers (directly) even further than ARC, a pain point for beginners and seasoned professionals alike. Fewer pointers makes Swift slightly more approachable than Obj-C, and playgrounds are somewhat helpful in exploring the features of the language.

The Swift design team also seems to have been responsive to the community of Obj-C developers, after Apple had incubated the language privately for several years.

I think that a lot of my gripes will be addressed over time, but it’s clear that fundamental design decisions were made in the language. I am concerned that those decisions are going to hinder the progress of the language.

Observations

Async:

There’s no native support for “async.” Yes, we got closure completions, but threading has always been an important part of Run-loop applications. Closures (and blocks) simplify “callback hell,” but do not eliminate it (especially in even marginally “advanced” animations). The simplicity of having first class support for async/continuations can’t be overstated, as demonstrated in this hypothetical version of async syntax:

async func animateView()->Void{

	// Run-loop continues while animation runs.
	await UIView.animate(self.view, duration: 0.25, 
		animations: { self.view.alpha = 0.5 })

	// Run-loop returns after animation runs.
	UIView.animate( self.view, duration:0.25, 
		animations: {self.view.alpha = 1.0 })
}

Inclusion of “real” async support would have been a significant advance for the platform.

Operators:

Support for custom operators allows for “opaque” syntaxes. Take this one, for example:

{ 
	//do some work 
} ~>
{
	//do some more work
}

This above operator (~>) was suggested as a way to support an async “background worker -> foreground continuation” pattern, since the language lacks it.

As I mentioned, I think async should have been a first-class component of the language, but this syntax will be completely non-obvious for a beginner. It’s compounded by the fact that every single project you work with may choose to define a different operator for this.

While I think it’s likely that these “high-level” operators will become idiomatic and most projects will adopt the same notation, I see this as more of an opportunity for making a quagmire, than convenience.

Documentation:

There’s no support for “first-class” documentation. The IDE should provide a way to produce ‘javadoc’ style comments.

I’ll admit, I have always felt that the documentation from Apple has been too focused on tasks, and not focused enough on learning the API. I think you need both, but learning to develop for iOS/OSX is about learning the API, and a way to survey the API is to read through the types and their members.

First-class documentation would emphasized the need to document the purpose and capabilities of both Apple and Third-party APIs.

Reflection:

Swift is a strongly-typed language, but there’s no simple way to get the type of an instance at runtime. The counter-argument is that you shouldn’t “care” about the type, and just call “respondsToSelector” or chain the safe-navigation operator (‘?’), but sometimes you really do need the type information.

Events/KVO:

KVO, an extremely powerful and important feature of Obj-C, is effectively not supported in native Swift.

What replaces KVO on the Swift side? Nothing.

Swift does include a useful syntax for observing changes on self’s own properties (which, I actually really like). Unfortunately, this willSet/didSet property syntax doesn’t allow for an elegant way to update the UI when a model changes.

If you’re trying to go “swift-only”, you’ll probably want some sort of PubSub/Event-sourcing to deal with this, so you’ll need to write your own.

Exceptions:

While I agree that throwing exceptions was overused in Java, and even in C#, I feel that excluding them entirely was an incorrect decision - here’s when exceptions are useful:

  • Exhaustion of physical resources (RAM, HD, maybe CPU)
  • An external process is not accessible (SQL Database, web service)

In more general terms, these are “unrecoverable errors.” The process cannot really do anything to make more hard drive space, and shouldn’t be responsible for rebooting a SQL server. These are exceptional circumstances. Worse, the runtime can produce “exceptions” when you do something illegal. Unfortunately, there’s no way to actually catch those and log them, or even easily debug them, because Swift has no notion of “exception”.

Additionally, the exclusion of an exception system serves continuing “Nil-lust” - Where, when assertions for inputs are not met, “Nil” is returned. Nil/null means something very specific: “The answer is undefined”, and this should be handled differently than “The answer was undefined, because you didn’t provide good inputs.” The language’s idioms don’t recognize the difference.

Yes, we can have NSError bleed through our APIs, but this is really not different than declaring all the types of exceptions that a method in Java could throw. I don’t know what the right answer is, but excluding the concept entirely doesn’t seem to be it.

Incomplete Type Inference/Excessive Coercion:

Type inference. I love type inference, I think it’s hugely valuable in making working with strong type systems more fluid. However, the removal of implicit casting when it is safe, diminishes the benefits of inference. For example, C# allows for implicit casting when precision would not be lost (i.e. Int -> Double). As a general rule, if you need explicit casting, you’re “doing it wrong.”

Multiple Inconsistent Syntaxes:

Swift has many syntaxes to achieve the same end result. This expands the overall syntax “surface,” making it challenging for learners of the language to read code.

For struct, the keyword needed to get a static member is:

struct AStructure{
	//note that "static" is the right keyword here, not "class"
	static var StoredProperty = 0
}

But, for classes, the “class” keyword will be used:

class AClass{
	// Note, as of XCode 6.1 GM, "class" 
	// and "static" aren't supported on class anyway.
	// Error implies this will be the syntax.
	class var StoredProperty = 0
}

This inconsistency requires a human to shift from right-brain activity, where algorithms and data structures get written (i.e. real work), to the left-brain activity, where the actual construction of syntax gets managed.

Specifying the keyword on a class doesn’t alter the class, but modifies its members. There’s a cognitive disconnect between the thing you’re touching, and the thing you’re modifying:

class AClass{
	//public
	func AFunc() -> Void
}

vs.

public class AClass{
	//changes to private
	func AFunc()->Void
}

Optional, and non-optional parameter names in functions contribute to the mess. Suppose I have these (fictional, and maybe nonsensical) methods included in a UIView category:

func addSubviewWithName(name:String, view:UIView) -> Void {}

vs.

func addSubview(_ name:String, view:UIView) -> Void {}

In the first, calling view.addSubviewWithName(“foo”, view:v2) results in a compiler error, while in the second, calling view.addSubview(name:”foo”, view:v2) results in an error. I now need to remember which syntax to use in each case and that will be driven by the function definition. Given the history of Obj-C and Swift, the parameter naming is imporatant, but I’m still unsure whether “_” was the right solution.

The constructions for function definitions and closures are unnecessarily different:

Given the following function:

func ALongRunningTask(completion:(Bool)->Void) -> Void { completion(true) }

This is the call for it:

ALongRunningTask(){ b in NSLog("%@", b)}

The syntax could have been

// Isn't very pretty, but doesn't require another syntax construction
ALongRunningTask() b -> { NSLog("%@", b)}

Generics aren’t described the same way between classes, protocols, and extensions:

For Structs:

//type parameter required
struct Stack<T> {
    var items = [T]()
    mutating func push(item: T) {
        items.append(item)
    }
    mutating func pop() -> T {
        return items.removeLast()
    }
}

For Extensions:

//no type parameter required
extension Stack {
    var topItem: T? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

For Protocols:

//no type parameter required, but "typealias" must be specified.
protocol Container {
    typealias ItemType
    mutating func append(item: ItemType)
    var count: Int { get }
    subscript(i: Int) -> ItemType { get }
}

Again, I have to remember more syntax for effectively the same concept.

Fixing the Un-broken:

The Swift syntax also seems to have attempted to solve problems we didn’t know we had, and, in my opinion, creates some that were non-existant:

Optional semi-colons. Touted as a feature, this seems like a waste of engineering effort. Require the semi-colons, and your checks for “is this a valid statement” become dramatically simpler. As programmers, we know how to type, ‘;’ - it doesn’t even require coordination with a shift key, semi-colons are required in a huge number whitespace-insensitive languages (C, Obj-C, C++, C#, PHP, Javascript*). Worse, in making ‘;’ optional seems to lead to spurious or incorrect compilation errors - often focusing attention on the wrong syntax problem in the IDE. I’d even go so far as to argue that excluding them will lead to lower “readability”, as lines glob together.

Unneccessary changes to syntax in the name of “readability” (Though, I would like to see actual data to prove this. For example:

//what language uses this syntax?
var l = [NSObject]() 

// C-drived languages, your developer's bread-and-butter, 
// do it like this:
var l = NSObject[]

I also generally disagree that the order of the function definition makes sense for someone learning the API. This is bound to be a “holy-war” inducing argument. You have been warned:

In OOP, frequently, your task is to acquire (or be handed) the correct instance of a certain type of object, and then send messages to it to achieve your goal. The extremely verbose nature of the method names, combined with the de-emphasis of what the return value is, makes the func syntax unwieldy.

I understand the parameter naming is to support Obj-C compatibility, but I think that the focus still needs to be on acquiring objects of certain types and then working with those objects.

The way I personally work is to think about my destination (a specific application state), and work back from there to the input parameters I have. So, when I’m looking at an API, I’m following a chain back to the inputs I’m handed. By putting the parameters before the return type, I need to sift through the whole API to find candidate methods. In addition, the the function syntax is completely inconsistent with C and its derived languages.

I also think the return type serves to emphasize the difference between a stateful or functional interaction, and is therefore even more important in a language that tries to support both.

Conclusions:

  • Swift feels like a very piecemiel language, where the syntax was concieved in parts, and lack consistency in their design. The syntax feels “deliberately different,” which I think is a solution in search of a problem.
  • Swift attempts to eliminate “unsafe” concepts, without providing reasonable alternatives (like Exceptions)
  • Swift serves too many audiences. Swift is not a language for beginners; The syntax surface is too large, and the number of paradigms it supports is too broad. At the same time, core features of the iOS/OSX platform(s) are not supported, which limits its value to advanced developers that already know Obj-C.
  • As of 10/6/2014, the Swift compiler/runtime are very buggy, compiler errors are misleading, which leads to wild-goose chases to get the “magic” syntax correct. In just the little bit of playing I’ve done, I’ve seen reference assignments get optimized out, leading to messages being sent to the wrong objects, leading to “Exceptions” that can’t be caught and are hard to debug, because they originate in object code.
  • The core of iOS/OS X has been Obj-C for (technically) more than 15 years. You will need to use Obj-C, you can’t “just use Swift.” - Until you can build an elegant, reliable app in Swift, it’s probably not worth your frustration to use it.

Finally, I realize all of this is an “outsider’s” perspective, based on my experience in other platforms. I’d love to get your feedback in understanding the language, platform, and context better. Feel free to send me (love) notes on Twitter: @atheken. And to correct my assumptions in the comments.

//Andrew


comments powered by Disqus