Clarity, cohesion, coupling, complexity
How can we write better code?
How can we write better code?
Alright, hello everyone. I have one question. One question, that I'd like to use to frame the 24 minutes that we have together and that is; how can we write better code? I think it's easy to write software that works, but that's just a jumble of super hacky code. The challenge for us is to write code that is elegant, well-crafted, easy to maintain, and readable. You'll hear a lot of people say that you want to write code that others can understand, and maybe more importantly, you want to write code that future you will be able to understand.
Computer programming might be a relatively new thing, like in the history of civilization, but writing in general is pretty ancient. People have been writing things down for people later on to understand, for a really long time. I think there are a lot of parallels between writing code and writing in general, prose, fiction, poetry, things like that. I studied language and literature in school, the code as poetry angle appeals to me especially. If we want to write code that people can read, maybe the question is how do we become better writers?
Read a lot and write a lot.
If you ask writers, that is writers of fiction and prose and things like that, they'll often tell you that the best way to become a better writer is to do a lot of reading. Here's a quote from Stephen King's book, ‘On Writing’, which you can guess what the book is about, and the quote goes,
"If you want to be a writer, you must do two things above all others: read a lot and write a lot.”
I would say the first step for us to become better writers of code is to become- better readers of code.
Maybe you already read a lot of code. Maybe you do code reviews at work or you just enjoy browsing open-source repositories, but I would say; let's try to read more code and let's be more thoughtful and focus ourselves a little bit more when we do that reading. To think about, what is it that we're looking for as we read code? Maybe if you're at work and you're doing code reviews, you're looking at all of the bugs your coworkers are putting in and nitpicking about spacing and how many tabs to use, but that's very different from when you're at home, sitting by the fireplace with a glass of whiskey and browsing GitHub.
What are the things that we want to look for when we read code? I was browsing an index of academic journals, as one does on the commute to work, and came across this paper, “Using complexity, coupling, and cohesion metrics as early indicators of vulnerabilities.” It's a really interesting paper where the author looked at security vulnerabilities in Mozilla Firefox and then went back to the source code and using these metrics to see whether he could've predicted where the vulnerabilities would have come based on some code analysis. I'll sound the spoiler horn and the TL;DR is that yes, indeed, there is a statistically significant correlation between cohesion, coupling, complexity metrics and where security vulnerabilities come up in the future.
I'm not going to talk about security vulnerabilities today, but I took those three ‘C’s from the paper and added ‘clarity’ as an overarching goal from the Swift 3 API Design Guidelines, and I decided to go forth into the wilderness of GitHub to see what I could find. Every year at WWDC, we get a whole bunch of cool stuff. Last year, the most exciting thing for me was the announcement that Swift was going to be open sourced, and at this very conference last year, I know a lot of us were eagerly anticipating when that would be. I don't know about you, but when the moment finally arrived, I was super excited about all of the code that I could read and looking at all of the commit history that was there, available for us.
The four C's - clarity, cohesion, coupling, complexity.
That's the story that I want to tell today, how I used the four ‘C’s as a lens to read more Swift code. Some of the things that reading that code has made me think about and maybe has taught me and that I hope have made me more thoughtful in general about both reading and writing code. And animation (*referring to animation in slide*).
Looking ahead, what are we going to talk about today? I'm going to look at some bits of source code from the open-source Swift project and hopefully something will pique your interest, and you'll also learn something new about this language and this ecosystem that we're all working and living in.
Let's start just briefly with the first C which is ‘Clarity’, and again, this comes from the Swift 3 API Design Guidelines, and I've heard from a lot of people saying, "Oh, I don't write APIs so I didn't read that thing," but I'm saying, I'm thinking, "Well, if you write apps, let's say, you're writing classes, you're writing methods, and then somewhere else in your app, you're calling those methods, so I'm thinking everything is an API." If you haven't read the document, then I do highly encourage you to check it out.
One of my favorite quotes from this is, "Clarity at the point of use is your most important goal. Entities such as methods and properties are declared only once but used repeatedly." I like this idea because it focuses on where the most time is going to be spent, like a performance optimization. You're going to write the code once, but then in theory, machines and people will spend much, much more time reading and executing the code.
Having that as a basis or a foundation saying, "We need to write clear code that's readable," I think, is my overall goal and one of those things that I'm going to look at as I'm reading code. As a bonus, if you're a grammar nerd like I am, the API Design Guidelines are filled with words like these; ‘imperative verb phrase’ and ‘past participle’ to tell you what tense to do your methods and your arguments and things. If you're a grammar nerd, then you will also enjoy reading this.
All right, let's move on to the next C, which is ‘Cohesion’, and that's how much the elements of a module belong together. You can think of a module as maybe a single class, maybe a struct, maybe a protocol, maybe one file, and the idea is you want to keep similar things together and maintain some sort of organization within your project.
Let's have a look at the min function.
Maybe you've used this before. Here's the function signature, it's called min. It has this generic type T which has to be comparable, and it takes two of these Ts, x and y, and it returns the lesser of the two. If y is less than x, we have this ternary. We're going to return y. If x is less or if they're equal, then we'll return x. Very simple. Maybe you've used it before and this is how it works.
One thing I didn't know was that there is another argument of min that takes more than two arguments. Let's have a look.
It's very similar, min, Comparable, there's our x, there's our y. We have an additional z parameter, or “zed” for the fellow Canadians and foreigners, and then we have this rest parameter at the end, which is the variadic argument at the end. The fourth, fifth, sixth, and subsequent parameters will get rolled into this rest as an array. Let's have a look at how this works.
We need to return the lesser of all of these things. That's going to be stored in r. Let's start with x. r is going to be x. That's our starting value.
Then we'll look at y. Is y less than it? If it is, let's set it.
What about z? If z is less than what we currently have in r, then set it.
Then we have a for-loop. Let's iterate. For every t in rest, if it's less than the current r, and then set it.
Then at the very end, we'll return whatever we found as the smallest thing in there, pretty simple implementation.
One bonus or one additional thing that I like in addition to reading code is reading diffs. This is actually the code from about a year ago. Let's look at the modern version of what it looks like today. This is it.
It’s much shorter. I took out the function signature, but it's the same thing. Let's see what's going on.
We're actually using the two-argument min and we're saying, "Give me the lesser of x and y." Take that answer and give me the lesser of that and z, and so we're doing those two if checks we've eliminated, and now we're just getting the least of x, y, and z in this one line.
Then we still have to iterate, so for each value in rest, let's see if it's smaller, but you'll see we've moved the condition into a where clause before we had an if statement inside the for-loop. Now we're doing it outside. If it’s smaller, we'll set it and then we’ll return it as usual.
In my head, I'm thinking, "Wait a minute, this doesn't seem like it's going to work." Let's say, for example, minValue is 10, and then in my head I'm thinking, "It's like substitution model." That 10 is going to go into minValue. It's going to replace it.
Now, every turn of the loop, we're going to see, is it less than 10?, is it less than 10?, is it less than 10? Even if we find nine or four or seven or zero or whatever, it's always going to be compared to 10. There's no way this could work. But of course it does work, so I'm thinking, "All right, I must be wrong about something."
It turns out that this thing after the where isn't substituted once, but it's actually re-evaluated every turn of the loop. It's obvious when I think about it now, because, of course, value is evaluated every time through the loop, and so minValue will be the same thing. Even though we're mutating minValue inside the for-loop, it's okay because the where clause is like an expression and it's going to be re-evaluated every turn of the loop.
That's min. You can imagine what max looks like, very similar. You'll find those in ‘Algorithm.swift’, so I'm thinking, "Ah, algorithms, maybe I'll get a nice sorting algorithm, a nice graph search, maybe it'll tell me how to reverse a linked list so I can pass a job interview.” You'll go into the open-source repository. You'll find this file. You'll open it up, and then you may be a little disappointed of what you find inside.
When you actually find this file and you open it up, you'll see these symbols defined.
There's min and max. Remember, there's two version’s of min and two versions of max, and then there's this EnumeratedIterator, EnumeratedSequence, and these last two are deprecated. They got caught up in the great Swift renaming and they point to these other things.
Why are these things in a single file? I would think maybe min and max should be in the file that has to do with maybe sorting. And maybe the iterator and sequence should be in a file having to do with like Collections or the SequenceType, that kind of thing. I'm thinking of this as sort of like a junk drawer file. Maybe you have a junk drawer at home with scissors and elastic bands and paperclips, things like that.
Similarly, maybe you have a junk drawer class, I'll bet, in your app. You have all of all of these functions like getting the font size and getting the screen size and picking a color, and you say, "Well, we love object-oriented programming, so let's put it all together into a class called ‘utility’ and make them all static methods," right? I'm sure many of us have a junk drawer class in our own app and in our own code, and so all of those things, I would say, are probably examples of low cohesion. It's just something that I'm trying to keep an eye out to say, "Let's keep like things together and separate things apart and try to group and organize my code a little bit better."
Alright, let's move on to the next C word which is ‘Coupling’. Coupling is like the direct knowledge that one thing has about another thing. In Swift 3, for example, access levels might be an example. We got fileprivate new in Swift 3, maybe it's going away, I don't know. And private, public, internal, open and so on. We have these ways to control who knows about what thing, from what place. You'll see the word ‘encapsulation’ a lot, right? “We have to maintain encapsulation”, or “no we have to break encapsulation”. All of that has to do in this…
Speaking of coupling, or speaking of encapsulation and things inside capsules that you have to open up, let's talk about optionals. We have a string, and we have an optional string.
They look so alike. You’re like; “There is just one little bit of punctuation difference. These are the same thing. I'm going to treat them the same.” When I'm talking about optionals or teaching about optionals, I often use the famous box metaphor and say, "Okay, this is a string, and this is an optional string.”
When you look at it like this, you'll say, "Ah, well, of course, those are very different things. One is this word, and one is a box." You look inside the box and you could get the string, you could get nil. If you force unwrap the box, you will get a runtime trap, or (my favorite slide ever), you might get a puppy. You never know what's going to be in the box. That's the benefit of optionals, that they give us a lot of flexibility when we're dealing with code, but then of course there is also a danger. You have to be careful when you're dealing with them.
How are optionals made? What are they made of? Optionals are, maybe you already know, an enumeration. There's a generic type to say what's in the optional, and then we have these two cases, some and none, whether there is a value there or whether there is nothing, or we might say ‘nil’ more commonly. There's also this extra little comment that says, "The compiler has special knowledge of Optional<Wrapped>, including the fact that it is an ‘enum’ with cases named ‘none’ and ‘some’."
It was a little surprising to me. I thought, "The Optional is such a standard ... It's such a core part of the language and the compiler is over here and it deals with optionals," but optionals are an enumeration defined over here in the Standard Library. How do those two talk to each other? Maybe it wasn't that surprising, because a string, for example, if you put a string constant in your source code where you have a number of 42. That's actually a string and an integer, which again is defined in the Standard Library. There has to be some kind of talk between the compiler side and the Standard Library side, and there has to be some level of coupling between the two, which I think is not a bad thing.
Let's look at a, maybe, slightly simpler example.
This is from the source code for booleans. There's this method called _getBool. It takes a Builtin.Int1, which is ... We're at Swift ... This is one level down. It's a single bit integer and it returns a Swift Bool. It has this comment saying, “COMPILER_INTRINSIC," so it's an intrinsic function known to the compiler. There's a helpful comment saying, "This is a magic entry point known to the compiler," so if you ever doubted whether there was magic in the Standard Library, I hope this convinces you otherwise.
I just want to highlight a few things before we move on. Keep in mind, the method is called _getBool. It takes a Builtin.Int1, which is one level down from where we are at Swift and it returns a Swift Bool.
Let's have a look at some compiler code and see how the compiler accesses this.
This is ASTContext. AST is the Abstract Syntax Tree. It's C++. I'm by no means a C++ expert, but don't worry, we'll go through it together. We'll look and we won't touch.
What do we have? Remember, the method is called _getBool. This is getGetBool. We want to get that method. Sounds good, we want to do a lot of checking. We want to be safe. We want to get the name of the method. We have this _getBool, that's a string. We have this findLibraryIntrinsic, and we want to find _getBool. Right here, you can see the compiler has direct knowledge of the name of the thing that we're looking for.
What's next? A common thing you'll find in the compiler is that it checks for an error condition and exits early. If we don't find the declaration or if it is not a NonGenericIntrinsic, I have no idea what that is, then we'll exit early. The important thing here to note is that this function, the input and output being passed in, those are passed in as references. You can think of it as like an in/out in Swift. This function, when it returns, it's going to set input and output to be the types. Input is the parameter passed in. Output is the return value.
All right, let's keep going.
We’ve got our function. What do we want to do next? We want to check the types. We want to make sure that the input type is a Builtin.Int1. If it's not, exit early, and then we need to check the output type. Remember, we're down one level now, and so this method returns a Swift boolean, which is one level up from us now. There are some more acrobatics. We want to get the nominalType. If we don't find the type or the type has a parent or the most important part, if the types, declarations, names, string is not bool, then we also exit early.
Then by that point we have the function declaration. I would say there's a high level of coupling here. The compiler code here has a whole lot of knowledge about this method; name, the parameter type, and the return type. I think that's okay, because no code exists in a vacuum. When I was thinking about it before, I thought, "Well, of course, the compiler has to know some things about things in the Standard Library.”
A lot of times, I think the engineering solution, as we heard about, is one more level of indirection or one more level of abstraction. I think that's okay, but I think it's also important to balance those two needs to say, "How much abstraction and indirection do I want," versus "When is it okay to just have component A have direct knowledge of component B."
That leads us to ‘Complexity’, something with many interacting parts. Ideally, we have a complex thing made up of smaller understandable pieces, and in the best case as well, that thing that we built is greater than the sum of its parts. Just like coupling, I think complexity is not necessarily a bad thing. We're building complex systems every day, and that's just something we need to deal with. How can we deal with it?
Let's have a look at fiendishly complex type which is String.
This is the base declaration of String. You can see it's a struct. There is an initializer. There's another initializer. There's a _StringCore, and that's it. You're thinking, "Wait a minute, strings are way more complex than this, all of the good stuff must be in _StringCore." We'll look at _StringCore, and there's some more stuff there.
There’s an address, there's an owner. There are some things to grow the buffer, maybe some low level storage stuff, but I'm thinking, "Where is string comparison? Where are substrings? Where is the all-important emoji handling?" Right?
That's the stuff I'm looking for. That stuff, you'll find here to find in all of these extensions.
String conforms to all of these extensions ExpressibleByStringLiteral, TextOutputStream and so on, and 40 more extension. A lot of them, these anonymous extensions, and they're just using them to group code together. They're saying, "Here are the bits of functionality in protocols that strings implement. In addition, strings do, of course, a lot of other things. We're going to do break those up into extensions as well.
All of these over 40 extensions spread out across something like 20 files. String’s are a very complex thing, but to manage that complexity, you do this kind of thing to break it up. We've heard about extensions and protocols earlier today and also yesterday, so I hope I don't have to convince you that they're also awesome.
Let's have a look at a slightly simpler type. We heard about Collections also yesterday and also a little bit today. Let's talk about my favorite collection type that's in the Standard Library; CollectionOfOne, and as the name suggests, this is a collection that holds not zero, not two, but one thing. You might think this isn't useful, but you should look it up. This is actually used quite a bit throughout the Standard Library.
I like this because maybe you've tried to implement your own collection, maybe you've tried to build a queue or a stack or ordered dictionary or something like that. If you've done it, you know that there's a lot of stuff you need to do. I think this is, aside from being actually useful, a really good example to see- how could I build this, one of the most simplest collections that there is. Let's have a quick look.
The storage here is pretty simple. It's, again, a generic type parameter there and we just need to store the element. It's not an optional because there's always going to be one, and it's not an array because there's always going to be on. Start index, easy, it's 0. End index, maybe you can guess, it's 1. What about count? Maybe you can guess the implementation of count, also returns one.
You'll notice that it's also RandomAccessCollection. It conforms to that protocol, so it has to support the subscript. How does this work?
Here’s the getter. There is a precondition. It says, "You have to ask for position 0," otherwise that's a runtime trap right there. If you do say 0, it returns the element. What about the setter?
Same thing, if you're not asking for 0, that's an error, otherwise, we just set the property. Again, I like it because it's a very simple example of a complex thing, a Collection. There's also an iterator of one and the code to iterate through a single element, is as delightful as this is, so you should totally look it up afterwards.
That's ‘Complexity’, building up a large structure from smaller, understandable pieces where the thing that you're building is greater than the sum of its parts. I think a really good technique we have again for that in Swift is the use of extensions and protocols to help keep those smaller pieces well-organized and understandable.
Okay, so we've reached the end. We've talked about many of these ‘C’ words here.
Starting with ‘Clarity’, that's in keeping code and APIs easy to understand.
‘Cohesion’, keeping similar things grouped together.
‘Coupling’, keeping things apart where needed, but also respecting encapsulation, knowing the limits of encapsulation, and knowing when it's okay for systems to have more direct knowledge and to talk to each other.
Then finally there is ‘Complexity’, building up complex systems with smaller pieces and also seeing how a complex concept like an iterable collection can have a really simple implementation to look at, such as CollectionOfOne, iterator of one, and how that can scale all the way up to the generic array that we have that can store billions of all kinds of elements.
We're here at the end then; ‘Conclusion’ and ‘Challenge’.
In the spirit and example of great authors throughout history, I say that; reading more code is the best way to write better code. You'll see some concrete examples of what works, what doesn't work, and maybe more importantly, how things work.
That's my challenge to you, to read a little bit more code every day. My own strategy is when I'm writing some Swift and I'm calling into the Standard Library, I'll say, "All right, I'm going to take two minutes, and this method or this property that I'm calling, I'm going to look up the source code for it and I'm going to read it." That leads me somewhere different every time, maybe string length, maybe map, maybe flatMap or whatever, and by doing this, I'm reading a little bit more code and I'm also learning more about the underpinnings of how Swift works.
I happen to enjoy reading Swift Standard Library code because I'm a Swift nerd, but that doesn't mean it's the only option. You have to think about- what are you interests, what kind of code are you interested in reading? Maybe you have an app and there are some third-party open-source library that you're using but you don't understand very well. That would make a really great thing to try reading every once in a while.
I also picked the four ‘C’s here as themes to help me focus on what to look for, but that doesn't mean they're the only things to look for. There is brevity, performance, architecture questions and things like that. Again, what's important to you? What kind of things do you want code and reading code to lead you towards that you need to improve on or that you want to think more about as you go forth and write your own code?
You have to find the code that interest you and then see what you can learn from reading it. You can browse some open-source projects. You can store those repositories, maybe even submit patches and pull requests, and just basically read a lot of code and learn from it. Then in the true spirit of open-source, you can then use those ideas and adapt shamelessly in your own projects. I think it's the only way to learn and get better and to reach that goal of writing better code.
I want to thank Ida, Beren, everyone behind the scenes and in front of the scenes at Swift Summit. If you have any feedback for me, please send it my way and thank you so much for your time. That's all I've got.