Compile time Guaranteed

Reduce crashes and inconsistent behavior at run time

nikitalutsenko

Transcript:

Hi everyone. I'm Nikita. I'm gonna talk about something that is implicit, or something that we don't really focus on that much in our day-to-day work, which is; compile time guarantees. Things that you can do before running your application that can guarantee more things at run time. So, to begin with, let's take a look at our usual build and run flow. So first, we want to compile our application or piece of code to transform it into code, that the CPU or an actual device can interpret and run. This is a compilation step. Next one is; linking. It uses the code that we just compiled and links it with all the frameworks and packages it all with the resources into one single app bundle, or in just one binary. Next piece is, run. Great. Users can run our application, or we can run our application on the phone or a Mac, so everything is great. Though what usually happens is this- we get a crash, and a lot of times, we cannot really reason about it.

01:17: Our app crashes and generally provides some crazy, inconsistent behavior. That's the general reason why it crashes. Or we were expecting something, but we got something else, and we get a stack trace, and we go through crash reporting, and that's not the helpful flow. So why does it happen? It happens actually for a simple reason; because we are human. I found this quote online by Zed Shaw, which is really nice, it’s: "My brain tells me the truth, and it can't find any errors, therefore I have written perfect software."

Yeah, we all think that way, which is great, but that's not really what happens. The solution to the problem is to test everything. We usually test, or we should test. We should write tests, and there's all different ways and types of testing, like unit testing, which is simple, or dogfooding- which is giving your app to users; beta testing. If we take a look at Pyramid of Testing, which I'm sure you guys are familiar with this graph there, turns out there is one piece here that is actually missing. Or rather, it's super implicit. It's the compiler. The compiler is the first step in build and run whenever you build and run an application, and it's the best step out there. Or you might think it's the best step, because it provides way more guarantees than any other part of testing. It provides more guarantees than unit testing or integration testing. It provides probably more guarantees about how your application runs on the client than end-to-end testing. And the most important part about compilers is: they're not human.

03:08: Compilers have all these awesome features that people don't usually have, like syntax checking or type checking, advanced features like access control and semantic analysis, and also, they can provide guarantees. And generally, they provide two ways to guarantee how your application runs. The first one is compiler errors. It checks the syntax of your application, say in Swift or Objective-C, that actually you wrote something that makes sense in that language, which is great. The other piece that it checks is your intent. If you're writing a function and you're calling that function, there is one way of doing that, or there might be multiple ways of doing that, but it's defined, and it produces a compiler error, otherwise. The other piece here is, compiler warnings. These are probably not the errors, so that your app will still compile. But they let the compiler tell you about potential problems, or say, misuse of an API from system framework, or you're trying to do something, but that might not be the best intention there. So to accommodate all of these and use these in our application, we can do something like compiler-driven development.

That's not really a thing; I just came up with it yesterday. But… It's basically a set of rules that we can use writing Swift code to provide more guarantees about how our application runs at compile-time. So the first piece there is; fail early and fail often. Throws and Optionals are actually your best friends. If there is a slight possibility that this thing might do something else, or if there is a possibility that there might be an error in this case, use throws to provide more context, or use optional to return NIL. Another piece, which actually expends more than just compiler driven development, but this is; never trust input. If we attach it to Swift, we have implicit unwrapped optional and just optional. The first one is really bad, because you try to reason and make sure, and you're sure, that your code will never return NIL at that point. But since we are human, we cannot reason about that, so we should use actually an optional, not implicitly unwrapped optional, because just using that, running that line of code, if it's NIL, it will crash.

05:58: The other piece, which we usually love is- protecting everything. If we protect everything with guard, if, switch and where, we can provide more guarantees. We can structure our code in a way that gives us more guarantees, just by structuring it. There's actually more. There's things like constants and not variables. If you can make use of constant instead of a variable, please do that. Var is very, very, very bad for your code, because if you change the code later, if you come back to the same project in half a year and you try to mutate something, it's probably not the best intention there, which gets us to the next point about being explicit. The most important piece to it is usage of private and public as well as final.

If you don't want your class to ever be subclassed or you wrote it in a way that is not extensible, this is not your intention of the time of writing, use final. If you're unsure about a property, whether it should be public or private, use private since you don't really need to use it otherwise. And the last one here in this huge list is; prevention over documentation. We always want to prevent unsupported behavior of our code, instead of documenting it. For the first reason that I told you, which is because we are human and we don't usually read.

So this is all great. How does it apply in practice? What I have here is a small example. I'm trying to make a class, which is a request, and I want it to initialize with a string, which represents my URL as well as I'm gonna use some network library. Therefore, I need to use an NSURLRequest, or in this case, an NSMutableURLRequest, then I'm gonna add some authorization to it, and then I'm gonna perform my request.

08:10: Since I really need my request in the perform function, I'm gonna extract it into a variable right on top. And then some place else, I'm gonna actually use this class. So this all looks great and lovely, but what actually happens if I just change the URL from the proper canonical URL to something like this, with a TM on it? The TM is actually important. I will get a crash and usually what happens is, "Hey I just wrote this awesome piece of code and it works, I can reason about it", "It works with a generic URL, I have no idea what happened", so you go into that mode. What actually happened here is, you might have noticed, I'm using an implicitly unwrapped optional right over there, which is a single character. And that optional makes my code crash, so now I know the problem. Therefore, I can fix it. To usually fix things like implicitly unwrapped optionals, as well as to guard my code, to protect my code from crashing, I can use a throws annotation on an init function. It actually forces me also, to restructure my code so I benefit from error catching.

You can also use optionals here if you don't really have a precise error to provide back and provide some context of what actually happened, but in this example we can see how by simply structuring and adding a few more lines of code, the compiler makes us write code in a way that we can reason more about it. Another example from the same piece of code. What happens if we mutate this request? We are not gonna get a crash, but we cannot reason at run time about what happens there. The reason it happened is that due to the fact that we start by writing a var and not a let, we made our code express not the intention that we had. So var changed to let, the same code will actually produce a compiler error which will tell you, "Hey this was not your intention. You were intending to do something else, so you cannot really mutate that request over there."

10:47: Another one and another way to break this, simply mutate the URL request inside of it. Just use a different URL for it. We have no idea what's gonna happen at run time. We want to provide a way to guarantee that this never happens at compile-time, so instead of doing this, we're gonna actually split our class and usage of this class into separate swift files, and annotate our request variable on request class with private. It's an important catch there to separate these into separate files, because private annotation is private to the same source file, not to the same class. So now that we split our code, we provided more intentions there. And we actually listed that, we are using private, so this will produce a compiler error and we have a more guaranteed run time at compile-time. If we go back to the list of these things, you can see if you remove the swift annotation from all of them, they can mean really vague things.

The awesome part about all of these is that they can mean whatever you want. The main focus here is you should provide more guarantees and you should structure your code in a way that you can provide more guarantees at compile-time. Because otherwise, you're gonna run into production issues. You're gonna run into crashes on the App Store which lead to one star reviews, which lead to poor sales, which lead to all bad things. But if we use a set of these things, if we think before we write our code or after just we wrote our code and run it for the first time, we go back and clean everything up- Then we can provide more compile-time guarantees. There is actually more to these guarantees than just this. There are analyzer warnings. Clang Analyzer is amazing. It can check your code in advanced manner and can give you the flows that you might not even have thought about, and it's all automated and it's super easy to not just xcodebuild test, but also xcodebuild analyze. There are also compiler warnings and the important catch there is do not ignore warnings. If you have a warning in your project, you have less guarantees. I've seen projects with thousands of warnings and they’re really hard to reason about. But if you have zero warnings, you have used the best out of your compiler. You used the best out of your analyzer.

13:37: There is actually a lot of warnings in Clang that are not documented and they're all available at fuckingclangwarnings.com. Yes, I swore on-stage and I cannot swear in slides. That website has 26 + 21 + 234 warnings listed and they're all different kinds of things and this is the unique number. There's actually more warnings just different types with the same flag. But to use all of them, say in Objective-C, all we need to do is go into our target, change the build settings, find the other warning flags, and add Weverything. This will give any warning that Clang knows about, to us. If you're a pedantic person as many of us are, you can use Wpedantic, that actually is there. And if you use both of these, you can also disable the warnings, if you don't think they are necessary, simply by adding #pragma clang diagnostic push and pop, and ignore warnings case by case. The crazy part about this, this is not available in Swift. This is purely in Objective-C because there's no pre-processor in Swift and Apple doesn't want to make one. But if you use them just in Objective-C or you just enable all the warnings, it will actually give you more guarantees right at that point. And that's all I have.

Q&A:

1: Hello, I enjoyed your talk. I just wanna add on to what you were just talking about and that Xcode does have a setting that changes warnings to errors, “Treat Warnings as Errors”, so that way you actually can't compile if you have warnings in your project.

Nikita: It's hard to use that because usually you write code in a way that it's compiled and then you clean it up, then you eliminate warnings. But you don't want warnings to prevent you from compiling when you're just in the development process. But flag is useful.

2: Two things, one to add to him is you can turn it on just for release builds, which is actually pretty nice and then you can debug. Anyways, question about the exclamation-point vs question-mark guy. So when I first started Swift, I was in the same boat; Question marks are terrible, never use them. But I've actually started changing my thinking to be like, "Well, sometimes actually maybe a crash is better than a no-op to a user." Right? If you're a user and you're just hitting something and nothing's happening, that might actually be more confusing than something crashing. And like, modern bug reporting too, like with say, Crashlytics or something, might catch that, it might let you find that error easier than if it was just a no-op. So I've actually, when I develop now, I go back and forth like, "Would a crash actually be better in this situation than a no-op?" I dunno, just thinking if you have any insight on that or what your thoughts are.

Nikita: You're making a very good point and before, that was not the case, but starting with Swift 2, we have throws which gives us a way to structure our code. If something is NIL we can guard that statement and then throw. So we can provide the error to the end user. Because the amount of time the user spends to relaunching your app or if they even want to relaunch your app after the crash is actually a lot. If you can give them an error that, "Hey I just hit that URL and there was an unexpected error, and I have no idea what's going on," that might be a better choice, because they might take some different action. Not the same action as they intended first. So if you use the combination of optionals, not implicitly unwrapped optionals but just optionals, and guard every optional statement that you expect to be non-NIL, but you want it to throw, there you have it, a somewhat better code, than just crashing your app.

3: Great talk and this is really a great discussion, one follow on to piggyback on what Tony just said which is; you could also use assertions to guarantee invariance about your code. It's not compile-time, but at least it's something. And you get quick feedback, fail early-fail often, and be explicit over implicit. And you can also do things in release mode where you can have a customer assertion handler, that instead of crashing your app, throws an alert, that allows the user to report that error. It's actually something that the Spotify app does in debug modes, and I also think in release mode for developers.

But I wanna ask Nikita a question which is; when you have more of these compile-time contracts and guarantees, in your experience, how does that affect the way you write unit tests, and the things that you look at when you find candidates for unit testing and doing risk analysis?

Nikita: You actually end up writing less tests. That's so very true. Even in Objective-C by simply adding nullability annotations to your headers, you don't need to write a test to break your code or make it throw or make it do something, it could assert on something and you want to validate those assertions. You want to make sure that your code doesn't do some crazy thing, when you are passed with a NIL. So if your code throws and if it actually guards the proper things, you can test less things at that point.

4: Hi. Thanks for the talk. One thing that a team I was on was dealing with, was how to handle NIL propagation and optional propagation. In the examples you showed, do you always try to guard it early? Because, otherwise, you end up doing an Objective-C style thing where you're constantly checking NIL all the way up the stack, for example. So, are you guarding very tightly to prevent optional propagation, is the question.

Nikita: It actually depends. So in the example with the request, if you return optional NIL from it, there're a lot of reasons that that can be NIL or we fail to allocate- can that be NIL? Probably not in Swift, but generally, yes. If we add more attributes to the same initializer, we can return NIL in both scenarios. The benefit of throws over NIL propagation further is that it gives you an ability to explicitly tell what happened. So, in this example, you can actually tell that, "Hey, my URL string was malformed and I cannot construct a request with that string."