Leaning on the Compiler · paul-samuels.com


A real strength of statically compiled languages is that you always have a really clever friend (the compiler) looking over your shoulder checking for various types of errors. Just like real friendships you’ll have times where you seem to disagree on really stupid things but at the end of the day you know they have your back. Here’s some tips on ways that you can lean on your friend to really take advantage of their expertise.

Disclaimer everything below is a suggestion and your milage may vary. As with anything in software there are no silver bullets.

Avoid force unwrapping (even when you know it’s safe)

I’ve seen code that will do a nil check to guard against having an optional but the code then continues to use force unwraps for all subsequent uses. Consider the following:

1 guard self.shape != nil else {
2 return
3 }
4 5 update(with: self.shape!)

For the sake of simplicity let’s assume that shape is defined as let so it can’t possibly go nil after we have checked it.

If I saw this code I would suggest that the author rephrase it like this:

1 guard let shape = self.shape else {
2 return
3 }
4 5 update(with: shape)

The change itself is trivial and it may be difficult to see all the advantages. The biggest win for me is that the compiler is now primed to have my back during future refactorings.

What do I mean by the compiler is now primed to have my back? Let’s first look at some of the problems from the above. With the first listing the biggest maintenance headache is that we have added a line of code that cannot be safely relocated. Relocating lines of code can come about for many different reasons:

1) Copy and Pasta

A developer may copy and paste the working line of code (line 5) without the guard statement and place it somewhere else. Because this line has a force unwrap the compiler won’t force us to explicitly handle a nil check. In a worst case scenario we may not exercise the use case where shape is nil very often, which would result in run times crashes that are rarely reached.

2) Refactoring

Refactoring is a dangerous game unless you have really good test coverage (quality coverage covering all branches, not just a high number from executing everything). Imagine someone removed lines 1-3 in an attempt to tidy things up - we’d be back to the point of crashes that we may or may not reproduce. This seems a little laughable with the example above but it would be really easily done if the guard statement was more complicated to begin and we was careless with the tidy up.

3) Bad Merge

There is always a chance in a multi-author system that people’s work may not merge as cleanly as we would like, which could result in the guard being taken out.

How is the second listing better?

I’m glad you asked. With the second listing all three scenarios above are just non existent. If I take the line update(with: shape) and place it anywhere that does not have an unwrapped shape around then the compiler will shout. This shouting will be at build time so I don’t need to spend potentially hours tracking down a crash, I get an immediate red flag that I need to handle the potential that this reference could be nil.

Avoid non-exhaustive switch statements (even when it’s painful)

Switch statements need to be exhaustive in order to compile but I often see code that does not exhaustively list out cases, instead opting to use default. Consider the following:

enum Shape { case circle case square case triangle
} func hasFourSides(_ shape: Shape) -> Bool { switch shape { case .square: return true default: return false }
}

I would argue that the function would be better phrased as:

func hasFourSides(_ shape: Shape) -> Bool { switch shape { case .square: return true case .circle, .triangle: return false }
}

Like the first example it may seem like this is a trivial change with no immediate advantage but it’s the future maintenance that I think is important here. With the second listing the compiler will be ready to shout at us if we expand our shapes to include a case rectangle - this will force us to consider the function and provide the right answer. In the first listing the compiler will not notice any issues and worse the code will now incorrectly report that a .rectangle does not have four sides. In this case I would argue that this is a trickier bug than a crash because it’s non fatal and relies on us checking logic correctly either in an automated test or via manual testing.

Create real types instead of leaning on primitives

Creating real types gives the compiler even more scope to help you. Consider the following:

struct User { let id: String let bestFriendID: String
} func bestFriendLookup(id: String) -> User { ...
}

With the above API it’s actually impossible to tell without looking at some documentation or viewing the source whether you should pass a User.id or a User.bestFriendID to the bestFriendLookup(id:) function.

If we was using more specific types instead of String for the id the function might look more like this:

func bestFriendLookup(id: BestFriendID) -> User { ...
}

Note that I mean a real type e.g. struct BestFriendID { ... } not just a typealias which would have no benefit

I’m not going to list a solution here because you may as well just check out https://github.com/pointfreeco/swift-tagged repo/README for how you can easily use real types to solve this problem.

Avoid Any

There are absolutely times that we have to use Any but I’m willing to bet that most of the times I encounter it in code there could have been a way to rephrase things to keep the type knowledge.

A common example, that I have definitely done myself, occurs when crossing a module boundary. If I write a new module that I call into from my main app I may want to to pass a type declared in my main app to the module as userData: Any. In this instance the new module has to take Any as I don’t want it to know anything about the main app.

This userData: Any is another potential bug just waiting to be encountered because the compiler can’t validate that I didn’t get my types mixed up. The fix for this is to provide a module type that uses generics. The best example of this is going to be the collection types in the standard library - they don’t have an interface that works with Any instead they are generic over a type.

Conclusion

I’ve outlined a few cases where leaning on the compiler can eliminate potential future maintenance issues. There is no one size fits all solution so I encourage people to keep sweating the details and maybe some of the above suggestions will be appropriate for your code bases. The compiler isn’t going to solve all of our coding problems but it can certainly help reduce pain if we code in a compiler aware way - I like to ask the question “can I rephrase this code so that it won’t compile if the surrounding context is removed”.