Swift, Uncle Bob, and The Dark Path
This week Robert C. Martin, aka, Uncle Bob published a blog post that made a lot of people mad.
Chris Eidhof wrote a nice response with his point of view on the subject, in which I believe they agree more than they disagree on the matter.
Some other responses weren’t that nice. I understand they felt hurt in a personal level and that’s why I tend not to get into those discussions however I feel like I could be part of this one since I see that both sides have very good points and I’m sure there’s much to learn there.
Uncle Bob believes the creators of Swift, Kotlin, and, as a trend, probably more new programming languages, crossed the line in making it too much static typed, which would require perfect upfront knowledge of the system, make code hard to change due to hard constraints, fairly complex languages with too many keywords, and hard to read manuals to name some. He also believes that the problems those languages are trying to solve are programmer problems and not language problems, and writing good tests would solve them.
I want to break down those thoughts with my point of view.
I don’t know Kotlin but I’ve been using Swift quite a lot lately and I’ve seen that most responses came from its community so I’ll focus on that.
Tests and too much static typing
Uncle Bob was very clear about not being against static typing, but against too much of it. I agree with that and I’m pretty sure most people do.
I read comments from people contrary to this idea and a lot of them have an idea of types as something that solves all the problems. “If it compiles, I’m sure it works” is often said which is a very naive thought in my opinion, and Swift creators never made that promise.
“Swift is designed to make writing and maintaining correct programs easier for the developer”
https://swift.org/about/
That doesn’t take the burden of us having to write it right. It just promises to help us as much as possible to write expressive and safe code from its maintainers' points of view.
With that said, my view is that it will never eliminate the need for you to write good tests for your program. It might reduce the number of tests you have to write but by a small percentage.
I’m more than happy to have the compiler checking things for me, although there’s a price to pay and we need to understand it. It does kind of forces us to make too much up front decisions, which we are almost always not ready to make, and it will cost us and make code hard to change later on.
How much is too much?
Types and code hard to change
In his article, Chris Eidhof gives us an example of his understanding of Uncle Bob’s complaints by saying that if you wrote a function that returns an Int and later on found out it actually should have been an optional Int? it would not be hard to change since the compiler would help you.
He’s right. The compiler would help you by making it impossible to compile and that’s a good thing since the signature (or contract) of that API has changed and bugs might happen if you don’t check the null case. The same goes for exceptions.
Why wouldn’t you want that? Well…
On iOS and Mac App Store development, we have a fairly strict release/deployment process in which we have almost no control over so breaking APIs is ok since there’s one thing we seem not to have compared to some other environments. That thing is called independent deployment.
Independent deployability gives you the freedom to develop, change, compile, test and deploy parts of your application without affecting other parts.
Let’s go back to Chris’ example. By changing the Int return type to optional Int? you have affected and broke all clients of your component. It broke the contract. It is now impossible to deploy that module independently. You’ll have to recompile and redeploy all dependent parts of your system because the language forces you to make a decision you were not ready to make and, as we often do, guessed wrong.
Again, that might seem fine on iOS/Mac App Store deployment, and yes, it is fine. It’s fine because you already cannot independently deploy parts of your app through normal ways. You can only submit the whole binary and wait for Apple to approve/reject it.
There’s also another related thing called independent development that we could and can have but we often don’t benefit from because our current tools make it very hard to do.
You can architecture your app in modules that can be developed, changed, compiled and tested independently. As long as you don’t break the public APIs all the time, you can pretty much break down the app development and go faster without getting in other developers ways and share those modules across applications.
Those two concepts go hand in hand and in my opinion, they are very important concepts, especially on large projects with many developers involved. So important that even when I’m writing a program alone, I follow.
The other problem is that Swift is a general-purpose language that has a commitment to also run on other environments like the cloud, an environment in which many developers make a fair amount of deployments. Daily. No need for Apple’s approval.
Nullability and Optionals
One could say that, as a convention, you could always return optionals and force the clients to unwrap it to keep the ability to keep your module safe from future changes although I don’t think that’s a good advice.
In fact, in most languages that don’t force null-checks, that’s pretty much what it is but without the compiler help. Almost everything can be null so we are forced to remember to check and test the null case if we want to be safe from NPEs. It let us decide what to do and forces us to have to think this through which we could forget and we’d pay the price for forgetting it.
There are conventions that says “never return null” and others that praises the Null Object Pattern. Those are good advices but in practice, if you’re not pragmatic, it can sometimes generate awkward APIs.
Most times you should not return a null and understanding when to comes with experience.
Changing a function that always returned an Int to maybe return a null in a language that doesn’t force null-checks could be even worse since it could break the system and without tests there would be no way to know until it crashes at runtime. The compiler would just not help you or the clients of your API. Actually you could probably never find this problem without a crash reporting system in place.
Maybe you should just not do that. We shouldn't be breaking client modules all the time. Find a better way. That's our job as I see it.
I believe that “always return optionals” in Swift wouldn’t be overriding safety since the compiler would force you to check them, but would create rather weird APIs. Really, don’t do it.
I can give you a simple example. Imagine a function that finds a set of employees with a certain name. I find it quite bizarre that that function should have any reason to return an optional but I often see APIs like that.
func employees(named: String) -> Set<Employee>?
What does the null mean? No employees with that name were found? Error?
If no employees were found, I’d expect an empty set. That’s it. It returned a set. With no employees. There are no employees with that name.
I don’t want to have to unwrap it.
If that API really needs to produce an error for any reason, you could potentially return a Result type with an error case that nicely describes the error so the clients of the API can make a decision on how to proceed. You could also throw exceptions or many other approaches that are more expressive than null.
For me, null should never imply error and I'd never return a null in that case. Ever.
I’d probably start writing the function that returns a non-optional set of employees with all the needed tests, but not for the null case since the compiler guarantees it won’t ever return null.
func employees(named: String) -> Set<Employee>
If at some point I have to return errors because of some new requirements, I’d create a new function that returns the Result type, not breaking the API but rather adding a new function that deals with the error, with all needed tests for the new requirement. Internally the function that returns a non-optional set could use that new function. That refactoring would work just fine without breaking any tests or clients.
However I don’t find it bizarre for a function that finds an employee by ID to return an optional Employee.
func employee(id: String) -> Employee?
I see that null as “There’s no employee with that ID”. I don’t feel the need to create an error type EmployeeNotFoundError. That would be annoying. I don’t need that.
I like expressive code and I think Swift helps with that there.
Some languages like Objective-C allows you to send messages to nil but without safety checks (that you can also forget to write), it often introduces other problems.
Those are just simple examples so you can imagine how that could scale to more challenging rigidity for other complex problems we've to face every day as developers.
The programmer job vs The language job
As I understand Uncle Bob’s point of view, he believes that if you forget to write a test and to check the null case, it’s your fault the program is broken, not the language's.
That “forget to write” part is a big problem. I believe it is our fault when we do forget. I quite like moving that responsibility to the compiler but I understand his complaints.
I really find the combination of tests and type check powerful if used with care and at the right places so understanding the trade-offs is key.
I also find that I can write much more flexible code with a powerful combination of dynamic typing and TDD without loosing safety. It would require more discipline and I’d gladly do it when that kind of flexibility is a requirement. Different than popular opinion, I don’t believe that TDD would require more time to finish building the system.
In most projects I’ve worked, if not all, that kind of flexibility is required in at least one module and Swift makes it very difficult to achieve it with its type system. For some other modules, Swift’s expressiveness is just perfect.
Simple vs Complex languages
I learned how to code with C. It was my preferred language for many years. I used to find it very simple and I couldn’t understand why so many people hate it. Pointers? What’s wrong with that? Manual memory management? What’s wrong with that? Mutability? Threading? And so on. It really helped me understand how a computer works. It’s super powerful and you can make anything with it.
Then something changed. I started to realise that programming is not always about computers. It’s most often about computation and abstractions are very important for simplicity.
“[Computer science] is not really about computers — and it’s not about computers in the same sense that physics is not really about particle accelerators, and biology is not about microscopes and Petri dishes…and geometry isn’t really about using surveying instruments.” — Hal Abelson
(taken from a discussion here)
From then on I focused on creating simple and elegant abstractions that better define intent and maintainability of the systems I build.
It was really key to combine my knowledge about computers and the power of abstractions to create more elegant solutions and I realised that some programming languages don’t give you any incentive to do so. They focus on the machine and that’s pretty much fine for some problems but as systems get more and more complex it’s just unmaintainable in my experience.
Then I found about LISP and its derivative languages. Its “simple” syntax almost looked silly. How could anyone write anything in it? Well, I was wrong. You not only can write anything but you can do it in an elegant manner with simple data structures and data transformation.
That opened my mind for a new way of programming. Which for many were the “only” way. Some people would look at C and say how could anyone write anything in it? And they would be wrong as I was.
What I'm trying to say is that simplicity is sometimes a point of view too. If you’re used to something it may look simpler than something drastically different. Maybe that changes once you make an effort to understand the other approach.
I’m studying clojure which is also a fairly new language and I’m very impressed with its simplicity and power to create good abstractions that simplifies the code by a huge factor. You end up writing less code to achieve the same results.
Swift also has functional programming aspects that makes it nice to write functional systems but due to its type constraints and many keywords you end up with fairly verbose and rigid code as Uncle Bob explains in his post.
The Dark Path
In my opinion, Swift does lead me to the wrong path and makes my life pretty miserable when I’m building highly flexible and composable systems which I enjoy and often have to build for myself and my clients.
It does go the right path when I’m building some less flexible, small systems within small teams. Something like most iOS apps. Again, mostly because of Apple's release process.
I’m pretty sure Swift is the right path for a lot of people that felt hurt by Uncle Bob’s words. But we have to face that it’s the wrong path for a lot of people too.
I've seen many companies hurt by that lack of flexibility. You can definitely achieve it with Swift, but it's just too hard to and too costly when compared to other environments and most developers don't have the knowledge, the time or the means to do so.
I’ve written systems in Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Javascript, COBOL, and the list goes on. Sometimes I didn’t have a choice. But when I do, I do think about the right language for the job.
Right now I’m building a game in Swift.
Final thoughts
As I see it, Chris has his thoughts on Swift more as an iOS/Mac language and Uncle Bob as a general-purpose language and I think they’re both right in their ideas when seeing it like this.
I believe the software industry need is for highly flexible, reusable and composable systems so I have to tend to Uncle Bob’s side but when I’m writing iOS/Mac apps I’m most probably going to choose Swift.
Do you understand the needs of the system you are building right now?
Can you make and explain that decision or are you just going with the flow?