Leaning on the Compiler

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
2
3
4
5
guard self.shape != nil else {
  return
}

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
2
3
4
5
guard let shape = self.shape else {
  return
}

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”.

Hands on Generics in Swift

Generics are a powerful language feature that we use daily when using the Swift standard library without really thinking about it. Things get tricky when people first start to write their own APIs with generics, there is often a confusion about what/why/when/how they should be used. This post runs through a worked example of writing a generic function and explaining the benefits as we go.


Problem Outline

Let’s imagine that we want a function that will take an array of shapes and remove any where the area is less than 100. The function should be able to handle multiple different shape types - here are two example types:

struct Rectangle: Equatable {
    let width: Double
    let height: Double

    func area() -> Double {
        return width * height
    }
}

struct Square: Equatable {
    let length: Double

    func area() -> Double {
        return length * length
    }
}

First Attempt

There are plenty of ways to tackle this problem so let’s just pick one to begin. Without generics we might try writing a function that ignores types by working with Any.

func filterSmallShapes(_ shapes: [Any]) -> [Any]

To write the implementation we need to cast to the correct type, call the area function and compare it against 100.

func filterSmallShapes(_ shapes: [Any]) -> [Any] {
    return shapes.filter {
        if let square = $0 as? Square {
            return square.area() > 100
        } else if let rectangle = $0 as? Rectangle {
            return rectangle.area() > 100
        } else {
            fatalError("Unhandled shape")
        }
    }
}

This implementation has some design floors:

1) It can crash at run time if we use it on any type that is not a Square or Rectangle.

filterSmallShapes([ Circle(radius: 10) ]) // This will crash as we have no implementation for `Circle`

2) The size predicate logic is duplicated twice.

This is not great because it means we’ll need to update multiple places in our code base if the core business rules change.

3) The function will keep getting bigger for every type we support.
4) We get an array of Any as the output, which means we’ll probably need to cast this output to a more useful type later.

1
2
3
4
5
6
7
8
func testFilterSmallShapes_removesSquaresWithAnAreaOfLessThan100() {
    let squares: [Square] = [ .init(length: 100), .init(length: 10) ]

    XCTAssertEqual(
      [ .init(length: 100) ],
      filterSmallShapes(squares).map { $0 as! Square }
    )
}

On line 6 above we have to cast back to a more specific type in order to do anything useful, in this case a simple equality check. This might not seem too bad but we must remember that this cast happens at runtime, which means that we put more pressure on our testing to ensure that we are exercising all possible scenarios in our code.


Second Attempt

Let’s introduce a protocol so that we don’t need to cast for each shape type. Doing this will resolve issues 1, 2 and 3.

protocol Sizeable {
    func area() -> Double
}

extension Rectangle: Sizeable {}
extension Square: Sizeable {}

func filterSmallShapes(_ shapes: [Sizeable]) -> [Sizeable] {
    return shapes.filter { $0.area() > 100 }
}

This implementation is a big improvement but we now return [Sizable] as the output, which is just as unhelpful as [Any] from the first attempt, which will still require a runtime cast.


Third Attempt

To resolve all the issues that we have encountered so far we might decide our previous attempts were ok but it might be easier to just duplicate the code and keep everything type safe:

func filterSmallShapes(_ shapes: [Rectangle]) -> [Rectangle] {
    return shapes.filter { $0.area() > 100 }
}

func filterSmallShapes(_ shapes: [Square]) -> [Square] {
    return shapes.filter { $0.area() > 100 }
}

Our test from earlier now becomes really simple without the type cast:

func testFilterSmallShapes_removesSquaresWithAnAreaOfLessThan100() {
    let squares: [Square] = [ .init(length: 100), .init(length: 10) ]

    XCTAssertEqual([ .init(length: 100) ], filterSmallShapes(squares))
}

This all works but we have reintroduced a couple of issues from our first attempt:

1) The size predicate logic is duplicated twice.
2) The function will keep being duplicated for every type we support.


The Generic Approach

This approach is a combination of the above attempts. The idea is that we’ll ask Swift to generate the various versions of our function (like in attempt 3) by providing a generic function that it can use as a blueprint. I’ll show the implementation and then explain the new bits:

func filterSmallShapes<Shape: Sizeable>(_ shapes: [Shape]) -> [Shape] {
    return shapes.filter { $0.area() > 100 }
}

The function body is identical to attempt two, the real change is in the function definition. We’ve introduced a “placeholder” type between <> that we have called Shape. This placeholder type has some constraints placed upon it where by we are saying it has to be a type that conforms to Sizable, this is indicated by writing Sizable after the :.

Our test is identical to the one written in attempt three - it’s as if we have just duplicated the function.


To understand how this all works I like to imagine the following mental model:

  • The compiler sees that I am calling the function with a type of Square.
  • The compiler will check that Square conforms to Sizable.
    • If it does not then it will cause a compiler error.
  • The compiler will generate a specialised copy of the function where mentions of Shape are replaced with Square.

I have no idea about the technical implementation of this from the compiler’s point of view but externally as a language user this model works well for me.


Conclusion

Writing your first functions/types that have generics can seem a little daunting but the steep learning curve is worth it when things start to click and you see the possible use cases as well as understand when it’s not appropriate to use generics. In the example above we end up in a position where we have no duplication of our core business logic (checking the area is < 100) and we have kept compile time type safety. I think analysing a few versions of the function can help with understanding the benefits/disadvantages of our decisions and make us more aware of the tradeoffs we are making when designing our APIs.

Speeding up your iOS Swift tests

Are your iOS Swift isolation tests running fast enough? My guess would be “no” unless you have taken deliberate actions to make them run faster. Here’s an approach to reduce the time we spend waiting for our tests to run.


Problem

If I head to Xcode and create a new iOS project with File > Project > Single View App. This gives me a bare bones project which hasn’t yet been sullied with my attempt at coding. I select the option to include unit tests so that everything is wired up and I get a blank test class.

From here I run the tests to get a sense of what my feedback cycle will be:

# Clean and Test
time xcodebuild -scheme SingleViewApp -destination 'platform=iOS Simulator,name=iPad Air 2,OS=latest' -quiet clean test

#=> 1) 38.470
#=> 2) 32.856
#=> 3) 34.413

# Test with warm caches
time xcodebuild -scheme SingleViewApp -destination 'platform=iOS Simulator,name=iPad Air 2,OS=latest' -quiet test

#=> 1) 31.669
#=> 2) 28.552
#=> 3) 28.462

I’m not going to dig into why these run times are soo slow but just know that this is way off the mark. This is a project with no production code and an empty test file but I have to wait ~30 seconds for feedback.


The End Result

Before going through the practical details of the Swift Package Manager based approach to speeding this up let’s look at the end results - this way I can avoid wasting people’s time if they decide the improvements don’t merit reading further.

Here I look at a single module extracted from a production code base. It’s a small module at 4633 lines of Swift, which is made from several smaller modules. It’s executing a modest 110 tests:

Xcode Clean SPM Clean Diff
46.873 22.717 2.06x faster
54.753 19.897 2.75x faster
58.447 19.883 2.93x faster

Clean builds are still painfully slow but they are 2-3x faster when using this technique so that’s still a win.

If you simply rerun the tests without changing any code you get a wild speed up of ~39% but that’s not a realistic test case. So the numbers for cached builds here will take into consideration changing some code to force some recompilation:

Xcode Cached SPM Cached Diff
31.840 7.389 4.30x faster
29.501 7.166 4.11x faster
25.396 6.306 4.02x faster

This is where the numbers get more exciting - ~7 seconds is still an awfully long time to wait for feedback when you are in a flow but it’s much more manageable than 30-60 seconds.


How?

So you made it past the results - let’s dig in. I’ve actually discussed this general idea already in Creating Swift Package Manager tools from your existing codebase (one of my catchier titles) but this is applying it to improving tests.

Prerequisites

  • If your code is well organised in folders then this will be easier
  • The code you want to test can’t import UIKit
    • Ideally you should avoid import UIKit as much as you can as this code will be more portable

Steps

  • In your project directory create a new Swift Package Manager project
  • Symlink your production code into the new project’s Sources folder
  • Symlink your test code into the new project’s Tests folder
  • Update the Package.swift file to reference your new module
  • Run swift test

Worked example of a simple project (not modularised)

Imagine I have a project called MySimpleApp, which has all the business logic separated from the UIKit related stuff. On disk it looks like:

├── MySimpleApp
│   ├── AppDelegate.swift
│   ├── Models                  <- business logic in this folder
│   │   ├── BlogPost.swift
│   │   └── BlogFilter.swift
│   └── ViewController.swift
├── MySimpleApp.xcodeproj
└── MySimpleAppTests
    └── Models
       ├── BlogPostTests.swift
       └── BlogFilterTests.swift

I would start by creating a new SPM project - I’m going to go with the arbitrary name of UIKitless. The swift package init command could be used but it generates a lot of stuff we don’t need so it’s simpler to build it manually here:

mkdir UIKitless
cd UIKitless
mkdir {Sources,Tests}
(cd Sources && ln -s ../../MySimpleApp/Models MySimpleApp)
(cd Tests && ln -s ../../MySimpleAppTests/Models MySimpleAppTests)

Next we need to create a Package.swift to tell SPM how to build all of this. The full file will look like this:

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "tests",
    products: [ ],
    targets: [
        .target(name: "MySimpleApp"),
        .testTarget(name: "MySimpleAppTests", dependencies: [ "MySimpleApp" ]),
    ]
)

With this we can run swift test and everything should work.


Worked example of a more complicated project

If you have modularised your project in any way then it will mean that you are using statements like import SomeOtherModule in your source files. This is not a problem as long as the module you want to import also follows the prerequisites above.

Here’s the additional steps that are required:

  • Symlink in the other module’s sources
  • Update the Package.swift file to tell SPM about this module

The trick is to use the same module name in SPM as is used in your code base e.g. if I import SomeOtherModule in my source files then the target name of my symlink should be SomeOtherModule

(cd Sources && ln -s ../../MySimpleApp/Modules/SomeOtherModule/Sources SomeOtherModule)

The last thing is to update Package.swift to tell it to build this new module and make it a dependency of MySimpleApp:

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "tests",
    products: [ ],
    targets: [
        .target(name: "MySimpleApp", dependencies: [ "SomeOtherModule" ]),
        .target(name: "SomeOtherModule"),
        .testTarget(name: "MySimpleAppTests", dependencies: [ "MySimpleApp" ]),
    ]
)

With this we can run swift test and everything should work.


Conclusion

Whilst this example is only using a small code base and possibly unrealistic tests it shows some promise. Hopefully I’ve shown that depending on how your codebase is structured this could be quite a cheap experiment to run and if it yields good results then you can keep using it. Having done a fair amount of Ruby where I would expect my tests to run in fractions of a second any time I save a file these tests are still mind numbingly slow - these things are improving all the time so I remain optimistic that testing won’t be this painful forever.