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.
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 String
s 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 example. 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:
- 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:
- 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 way 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
block.
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:
- 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:
- 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:
- Blocks are functional objects for ObjC. 2. You cannot put C functions into an NSArray. 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:
- 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.