My Swift Dilemma

Author's Note

I've tried to use this piece to capture my current thoughts on Swift, the troubles that I've had with it, and provide a reflection of the state I'm currently in on the benefits of Swift. Your experience will undoubtedly be different than mine, but maybe someone will find this helpful in their pursuits.

The v1.0 release of Swift has come and gone, and v1.1 is right around the corner. My thoughts so far can be summed up as this: the hype of Swift is over for me – I want my ObjC 3.0 language. I'll keep trucking along in Swift for the projects I can, but at the end of the day, I'm, on the whole, fairly disappointed in the language.

The truth of the matter is that I really, really, wanted to like Swift. Much of the Objective-C syntax is clunky, bolted on, and downright infuriating at times (I'm looking at you block syntax). However, the power and flexibility the language provided in its relatively minuscule ruleset should not be overlooked.

But it is.

I've tried many different projects with Swift from algorithms to data structures to solutions for working with structured data like JSON; pretty much everything short of a full-blown app. Around every corner I've been met with frustration due to design limitations, bugs, performance issues, poor debugger support, and what ultimately comes down to design choices. It's this last group that has me the most disheartened.

I look at the feature set of Swift, and I have to ask myself the question: what's the point? What is really trying to be solved? And does it provide significant benefits over languages that already exist? Does it provide a significant benefit over Objective C?

If we stand back and objectively evaluate Swift, I cannot find anything that is truly notable between itself and other languages besides the built-in ObjC interop. However, we know why this is the case: the language is designed by Apple, all of Apple's APIs are written in ObjC, and Swift has no alternative to those APIs. Thus, for Swift to ever have a hope of becoming the de-facto language for Apple development, Objective-C interop is simply a must have.

I said it in my initial gut reaction to Swift, and after mustering through the full 1.0 release of Swift and part of the 1.1 release, my initial reaction is still pretty much the same: we didn't get a better ObjC; we got Apple's take on a better, more modern C

I, for one, am not overly encouraged by that.

Examples

Let's take a look at some of the examples that represent the "goodness" of Swift.

Throughout, I'll be using the label "objc3" when referring to my envisioned improvements over ObjC 2.0. I'll also be throwing in "fobjc" for my take on a functional version, just for my own amusement, of ObjC heavily influenced by Haskell.

Modern Syntax

This is taken directly from the front page about Swift: Introducing Swift.

let stringArray = [ "Bob", "Frank", "Anne" ]
var sortedStrings = sorted(stringArray) {
    $0.uppercaseString < $1.uppercaseString
}

swift

What we see here is the simplest way Swift can express a function that takes a closure as a trailing parameter, infers the types of the arguments, and makes use of operator overloading to actually perform the comparison. I will not argue that this is not indeed extremely clean and terse. However, I would argue that it is loaded with an extreme amount of hidden complexities and ambiguities.

The first, what is the type of $0 and $1? In this case, because the definitions are so close, it's obvious that they are String instances. But code is never this clean.

The second, what < operator is actually getting called? No idea… but since we can infer that the types are Strings, we can make a guess that < is one defined to take two String arguments. However, we cannot actually go to the definition of it to make sure.1

For the third, we just happen to know that there are only two arguments, well, at least only two arguments that we care about, in the closure that is being passed in.

The fourth is the type of stringArray variable – it's an array of Strings because the values in the array are all of the type String.

It's not that any single one of these ambiguities cause issues, it's that your code becomes littered with all of these little subtleties. Sure, one can CMD+Click to see what the compiler thinks the type is, but that only works when working within Xcode. For code reviews on GitHub? Forget about it. Also, this can have a severe impact on the performance of code compilation. There's little I hate more in programming than changing valid syntax to something else simply to make the compiler happy.

Ok, so let's take a look at the code with the ambiguities removed:

let stringArray: [String] = [ "Bob", "Frank", "Anne" ]
var sortedStrings = sorted(stringArray, { (lhs: String, rhs: String) in
    return lhs.uppercaseString.compare(rhs.uppercaseString) == NSComparisonResult.OrderedDescending
});

swift

Well, that certainly sucks compared to the first example2. But this is one of my points: it's the syntactic sugar that makes the syntax modern, not the feature set within Swift itself. A cleaner ObjC could have done the same thing.

For good measure, let's take a look at two ObjC implementations if we were to create our own high-level sorted function.

NSArray *stringArray = @[ @"Bob", @"Frank", @"Anne" ];
NSArray *sortedStrings = sorted(stringArray, ^NSComparisonResult(NSString *lhs, NSString *rhs) {
        return [lhs compare:rhs];
    });

objc

We could strip the types and just use the loose types as well:

id stringArray = @[ @"Bob", @"Frank", @"Anne" ];
id sortedStrings = sorted(stringArray, ^NSComparisonResult(id lhs, id rhs) {
    return [lhs compare:rhs];
});

objc

There's no question that the first Swift example is the cleanest, but not by much. Really, the biggest eyesore in the last ObjC example is the @ requirement for arrays and strings. Simply removing that would result in this code:

let stringArray = [ "Bob", "Frank", "Anne" ]
let sortedStrings = sorted(stringArray, ^NSComparisonResult(id lhs, id rhs) {
    return lhs.compare(rhs)
});

objc2+

If we had some of the Swift features and created a functional version of ObjC, the code might look like this:

let stringArray := [ "Bob", "Frank", "Anne" ]
let sortedStrings := sorted (lhs rhs => compare lhs rhs) stringArray

fobjc

This uses the following constructs from Swift:

  1. The let construct to define an immutable instance. The stringArray contents can never be changed. 2. Type inference for declaration when it is explicit. The [] brackets return an Array instance, as well as the sorted function. 3. The block definition doesn't need to be repeated in the declaration. 4. Dropping the @ for type declarations. 5. Drop the semicolons.

There are a few other changes as well:

  1. The usage of := for assignment. This reduces the problem with = not being associative. For example, x = 5 and 5 = x are not the same thing in computer languages, but in all fields of mathematics, they are. 2. Closure declarations use the => (fat arrow) syntax. 2. Functions are structured functionally, that is, the data being operated on are the last inputs.

Any way you look at it, this example is an extremely poor example of how "Swift" modernizes our existing ObjC code.

Optional Types

Another change in Swift was the advent of Optional. The problem is not with the concept, but the application and the attempt and creating the ? and ! operators/syntax to work with them all over the place. The biggest source of these issues arise from the ObjC bridging as the APIs in ObjC can sometimes return nil even when they shouldn't. This results in the oh-so-awesome "implicitly unwrapped optionals".

Here's the rub, it's never safe to use implicitly unwrapped optionals because they can actually be nil. Using these nil optionals will result in a runtime crash. That's bad.

func foo(string: String!) {
    assert(string != nil)
}

let str1 = "Some String"
let str2: String! = nil

foo(str2)

swift

Fortunately, the use of these should be limited to ObjC bridging. However, the construct is available for general use in Swift; that is not good.

The other side of the problem comes with how we use and unpack optionals. There is only one straight forward way3 to do it that is safe.

let string: String? = "Some optional string"
if let string = string {
    // now we can use the non-optional value of string
}

swift

If we want to do anything in the error case, we need to construct the if-let-else block4.

let string: String? = "Some optional string"
if let string = string {
    // now we can use the non-optional value of string
}
else {
    // handle the case where there is no value
}

swift

However, this is seldom the code we ever want to write as we often times want to do validation on the optionals.

if string == nil {
    // handle the error case
}

// later in the code

if let string = string {
    // use the non-optional value
}

swift

In the above case, we end up working the optionals in various ways. Over the various Xcode betas, this pattern has changed because of various usability problems with each of the various incarnations.

For me, the problems really boil down to this:

  1. The equality check against nil is semantically wrong. It's not nil, it has no value. 2. To avoid nested if-let constructs, we need to use the unwieldy switch statement syntax. 3. The existence of implicitly unwrapped optionals defeats the entire purpose of the safety they are supposed to be provide in the first place.

Functional

Swift is said to open up the world of functional programming to Apple developers. It is definitely true that some things are easier to do with Swift, especially if you make use of operator overloading. But really, how much better is it over Objective C?

High Order Functions

Let's take a look at map as one of the examples.

The map function takes a set of data, performs an operation on each component, and returns a new set of data back to the caller.

NSArray *map(id (^transform)(id element), NSArray *array);

The ObjC version simply takes a block to transform the element, the array to work on, and returns a resulting array.

func map<S : SequenceType, T>(source: S, transform: (S.Generator.Element) -> T) -> [T]

The Swift version does essentially the same thing, though in a slightly different order.

Let's say we want to return a new array of all uppercase strings.

NSArray *names = @[ @"Bob", @"Frank", @"Anne" ];
NSArray *unames = map(^(id element) { return [element uppercaseString]; }, names);

objc

let names = [ "Bob", "Frank", "Anne" ]
let unames = map(names) { $0.uppercaseString }

swift

There is a pattern emerging here… Again, I will not argue that the Swift code does not look nicer, because it does. However, if we were building a truly modern ObjC language, we could have still done all of these niceties.

let names := [ "Bob", "Frank", "Anne" ]
let unames := map(x => x.uppercaseString(), names)

objc3

let names := [ "Bob", "Frank", "Anne" ]
let unames := map (elem => uppercase elem) names

fobjc

Chaining

The other aspect we'll look at it is chaining and we'll do this in the context of applying transformations to an object.

Our scenario will be simple; we'll start off with a list of names and the goal will be to apply the following filters to the list of names:

  1. Convert each name to uppercase 2. Removed names that start with ‘A' 3. Reverse the order of the names

Now, this is going to be the section where Swift comes out the clear winner. The reason will become clear shortly.

The final code will look like this:

id names = @[ @"Anne", @"Bob", @"Frank" ];

id names1 = toUpper(names);
id names2 = filterNames(names1);
id names3 = doreverse(names2);
NSLog(@"names3: %@", names3);

id result = doapply(@[toUpper, filterNames, doreverse], names);
NSLog(@"result: %@", result);

id nested = doreverse(filterNames(toUpper(names)));
NSLog(@"nested: %@", nested);

objc

let names = [ "Bob", "Frank", "Anne" ]

let names1 = toUpper(names)
let names2 = filterNames(names1)
let names3 = reverse(names2)
println("names3; \(names3)")

let filtered = apply([toUpper, filterNames, reverse], names)
println("filtered: \(filtered)")

let nested = reverse(filterNames(toUpper(names)))
println("nested: \(nested)")

swift

From this perspective, things are looking pretty much the same. Swift, of course, has the advantage of many of the high level functions that we'll need to use already being defined, namely: map, filter, and reverse.

Note that I had to prefix the ObjC implementations of mentioned high level functions with ‘do' as some of the names were already taken in the global space.

So let's take a look at implementing toUpper and filterNames:

typedef NSArray*(^filter_type)(NSArray *);

filter_type toUpper = ^NSArray*(NSArray *array) {
    return domap(^(id e) { return [e uppercaseString]; }, array);
};

filter_type filterNames = ^NSArray*(NSArray *array) {
    return dofilter(^BOOL(NSString *e) {
        return ![e hasPrefix:@"a"] && ![e hasPrefix:@"A"]; }, array);
};

objc

Well that's looking pretty ugly… So why blocks instead of straight C functions? A few reasons:

  1. Blocks are functional objects for ObjC. 2. You cannot put C functions into an NSArray5. 3. Parity with semantic meaning between the function declarations between our Swift and ObjC implementations; there's not context in which our blocks cannot be used but the Swift functions can. This would not be true if C functions were used.

The Swift version looks a lot cleaner:

func toUpper(array: [String]) -> [String] {
    return map(array) { return $0.uppercaseString }
}

func filterNames(array: [String]) -> [String] {
    return filter(array) { return !$0.hasPrefix("a") && !$0.hasPrefix("A") }
}

swift

The biggest problem with the ObjC version thus far has been the horrible block syntax that Swift has unquestionably made significantly better.

If we had the mystical new ObjC language, the ObjC code might have looked like this instead:

func toUpper(array: NSArray) -> NSArray
    return map(x => x.uppercaseString, array)
end

func filterNames(array: NSArray) -> NSArray
    return filter(x => !x.hasPrefix("a") && !x.hasPrefix("A"), array)
end

objc3

toUpper :: (array: [String]) -> [String]
toUpper array := map (x => uppercase x) array

filterNames :: (array: [String]) -> [String]
filterNames names := filter (x => not contains (prefix x) ["a", "A"]) names

fobjc

Swift does have a clear advantage over ObjC today: we could rewrite the apply function as a new operator. However, this is a very powerful feature that can lead to much ambiguity in your code; it should always be used with great care and consideration.

infix operator >> {
    associativity left
}

func >> <T>(lhs: [T], rhs: ([T]) -> [T]) -> [T] {
    return rhs(lhs)
}

let names = [ "Bob", "Frank", "Anne" ]
let result = names >> toUpper >> filterNames >> reverse
println("result: \(result)")

swift

Even with all of these niceties, Swift is no more functional than C# is, and really, ObjC can be. The fact that you can curry certain functions and create special operators makes it appear more functional than it really is. While those things lend itself to better functional approaches with less syntax, they do not make Swift a functional language.

That's ok, but we would simply stop calling Swift a functional language.

The Road to Hell is Paved with Generics

If you've made it with me this far, then I'll probably lose many of you here…

I hate generics. I don't just dislike the concept of generics, I hate the extremism that generics forces onto your code. Once you move to generics in your code, you, by definition, give up an extreme amount of flexibility in your code base. In exchange, you are supposed to get back improvements in type safety, code reduction, and performance. What no one talks about though, is the cost to write that code, to debug that code, and to understand that code, especially as generic systems get more and more "feature rich".

The canonical example that I also see with generics is typed collections. This is absurd on many counts. First, this is often touted as the way to solve this problem. It's not, it's one of several ways to fix this problem. The issue here is that every solution has benefits and drawbacks to them. However, for some odd reason, few seem to think generics brings anything bad to the table.

func reverse<C : CollectionType where C.Index : BidirectionalIndexType>(source: C) -> [C.Generator.Element]

WTF? Really, nothing bad to the table? And mind you, that's a very simple generic declaration. Compare that to this:

func reverse(source: CollectionType) -> CollectionType

I'm not a compiler author. I'm not one that gets excited about enforcing that every item in your collection is of type String. Why? Because it does not actually matter. If an Int gets in my array, it's because I screwed up and likely had very poor testing around the scenario to begin with.

var names = [String]()
names.append("David")
names.append("Frank")
names.append("Sally")

swift

var names := NSArray()
names.append("David")
names.append("Frank")
names.append(90120)

objc3

Uh… oops. Whatever… let's say I really, really wanted to ensure that all of my containers contained on a single type of element. How else might I do that without generics?

var names := MYTypedArray(x => return x.isKindOfClass(NSString.class))
names.append("David")
names.append("Frank")
names.append(90120)   // runtime exception

objc3

Yes, in this example, I've moved the validation from compile-time to runtime. But you know what, that's likely where many of these types of errors are going to be coming from to begin with because the content of the array is being filled in with dynamic content getting coerced into your given type at runtime from dynamic input sources, not from a set of code statements appending items to your arrays.

To me, the cost of generics is extremely high and brings little to the table that cannot be solved via other means, namely code generation and protocol conformance. Using those two patterns, you can achieve nearly everything that generics brings to the table without enforcing the extreme type system that it requires to function. These come at a cost too, I just find that cost significantly smaller.

Of all of the changes to Swift, generics is the #1 reason why I'm very hesitant to move forward with it. Yes, I miss many of the other features of ObjC, but this one tops the cake because it drastically decreases my ability to quickly write code.

A Lot More to Say

There's a lot more to say about my experience with Swift, and maybe I'll cover it more in the future. Some of the topics that I did not cover are:

  1. The baggage that the Swift language has because of ObjC. 2. The compile-time performance of Swift in anything but small projects. 3. The sheer number compiler bugs that need to be worked around just to make your code work as intended. 4. The lack of reflection. 5. The inability to create Cocoa, arguably one of the best platform frameworks, in Swift because design choices. 6. The terrible debugging support.

I'm not trying to claim that Swift is a terrible language, it's not. There are parts of it that I really enjoy, such as many of the improved syntax features. But at the end of the day, when I ask myself the question of whether it's going to make me more productive as both an app developer and a framework author, the answer for me is clear: "not yet".

Do I think that will ever change?

I honestly do not know.

  1. This is strange and must be a regression or limitation of built-in operators. At one point in time, I was able to go CMD+CLICK on operators and see which one was being invoked.
  2. There's an interesting bug in Xcode 6.1 Beta 2 as well. Without the lhs: String annotations in the closure definition, Xcode cannot actually provide you the proper completions for either $0 or lhs. A point-in-time deficiency in the tooling most likely, but still an issue we need to suffer through now.
  3. It's also possible to use switch and pull out the optional values as well, but that is more complicated, especially in the single optional case.
  4. Yes, we can use the switch-statement as well. However, I find it offers no distinct advantage in these single optional cases. The pattern is more helpful when we have multiple optionals though.
  5. Ok… you can, but you have to box the function pointer and it's a complete mess…
My Swift Dilemma