Providing an explicit type vs type inference - why not both?

Type inference is a really nice feature to have but sometimes we have to help the compiler out when what we want to write creates ambiguity.

This post uses a toy helper function that fetches remote JSON to show how we can design its api so that explicitly providing the type isn’t required when the compiler can infer types from context.


Let’s start by defining a pair of functions with a bit of duplication

func loadPost(id: Int) async throws -> Post {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Post.self, from: data)
}

func loadPhoto(id: Int) async throws -> Photo {
    let url = URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Photo.self, from: data)
}

In the functions above the main changes are the URL to fetch and the type to attempt to JSON decode to.

We could create a helper function to remove the duplication that would look like this:

private func load<Output: Decodable>(url: URL) async throws -> Output {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Output.self, from: data)
}

This function is generic over an Output that must be decodable and takes care of the networking and decoding tasks. With this in place our original functions can now become one liners that call through to this helper:

func loadPost(id: Int) async throws -> Post {
    try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!)
}

func loadPhoto(id: Int) async throws -> Photo {
    try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
}

With the above the compiler is happy to infer the type of Output in both cases because it can see that it needs to match the return type of the loadPost or loadPhoto functions.


This is all nice but quickly shows its inflexibility and breaks down if we change our usage slightly. If I decide that loadPost should really be changed to loadPostTitle instead as callers don’t need the full post object I would try to update my function like this

- func loadPost(id: Int) async throws -> Post {
+ func loadPostTitle(id: Int) async throws -> String {
-     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!)
+     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!).title
  }

With this change the compiler is no long happy and emits this error:

Generic parameter ‘Output’ could not be inferred

We can look at how JSONDecode.decode is defined to see how its api is designed. Clicking through the header we see

open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

We could replicate this by providing the type explicitly, the updated helper function becomes:

- private func load<Output: Decodable>(url: URL) async throws -> Output {
+ private func load<Output: Decodable>(url: URL, as type: Output.Type) async throws -> Output {
      let (data, _) = try await URLSession.shared.data(from: url)
      return try JSONDecoder().decode(Output.self, from: data)
  }

With this change the compiler now prompts us to update the call sites to explicitly provide the type to decode to

  func loadPostTitle(id: Int) async throws -> String {
-     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!).title
+     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!, as: Post.self).title
  }
  
  func loadPhoto(id: Int) async throws -> Photo {
-     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
+     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!, as: Photo.self)
  }

With the latest change we have more flexibility but if feels like we’ve lost some brevity in cases where the compiler can infer things. To bring this type inference back we can use a default argument (I think this was first shown to me by my friend Ollie Atkinson many years ago):

- private func load<Output: Decodable>(url: URL, as type: Output.Type) async throws -> Output {
+ private func load<Output: Decodable>(url: URL, as type: Output.Type = Output.self) async throws -> Output {
      let (data, _) = try await URLSession.shared.data(from: url)
      return try JSONDecoder().decode(Output.self, from: data)
  }

With this final change we get a good balance between full flexibility when we need it and type inference when the compiler can figure things out.

  func loadPostTitle(id: Int) async throws -> String {
      try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!, as: Post.self).title
  }
  
  func loadPhoto(id: Int) async throws -> Photo {
-     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!, as: Photo.self)
+     try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
  }

Conclusion

Making apis intuitive and nice to use can help keep you on track solving problems whilst the code gets out of the way. We’ve probably all used apis that require all our mental energy to remember how to use them, which means we can’t focus on the problem we are trying to solve.


Sample code

Here’s a code listing with scaffolding that you can slap into a playground to explore yourself

import Foundation

struct Post: Decodable {
    let id: Int
    let title: String
}

struct Photo: Decodable {
    let albumId: Int
    let id: Int
    let title: String
}

enum Original {
    static func loadPost(id: Int) async throws -> Post {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Post.self, from: data)
    }

    static func loadPhoto(id: Int) async throws -> Photo {
        let url = URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Photo.self, from: data)
    }
}

print(try await Original.loadPost(id: 1))
print(try await Original.loadPhoto(id: 1))

enum HelperFunction1 {
    static func loadPost(id: Int) async throws -> Post {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!)
    }

    static func loadPhoto(id: Int) async throws -> Photo {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
    }
    
    private static func load<Output: Decodable>(url: URL) async throws -> Output {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Output.self, from: data)
    }
}

print(try await HelperFunction1.loadPost(id: 1))
print(try await HelperFunction1.loadPhoto(id: 1))

enum HelperFunctionWithExplicitType {
    static func loadPostTitle(id: Int) async throws -> String {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!, as: Post.self).title
    }

    static func loadPhoto(id: Int) async throws -> Photo {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!, as: Photo.self)
    }
    
    private static func load<Output: Decodable>(url: URL, as type: Output.Type) async throws -> Output {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Output.self, from: data)
    }
}

print(try await HelperFunctionWithExplicitType.loadPostTitle(id: 1))
print(try await HelperFunctionWithExplicitType.loadPhoto(id: 1))

enum HelperFunctionWithOptionalInference {
    static func loadPostTitle(id: Int) async throws -> String {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!, as: Post.self).title
    }

    static func loadPhoto(id: Int) async throws -> Photo {
        try await load(url: URL(string: "https://jsonplaceholder.typicode.com/photos/\(id)")!)
    }
    
    private static func load<Output: Decodable>(url: URL, as type: Output.Type = Output.self) async throws -> Output {
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(Output.self, from: data)
    }
}

print(try await HelperFunctionWithOptionalInference.loadPostTitle(id: 1))
print(try await HelperFunctionWithOptionalInference.loadPhoto(id: 1))

Easy simulator data access

When developing for iOS it’s often useful to navigate to the files you create in the simulator so you can inspect everything is how you expect it to be. The location of where the files live on disk has changed throughout the years but one thing has remained constant - it’s awkward to locate where the files are. Now days simctl and excellent wrappers around it like Control Room help make it simpler to locate your files but there is still too much friction. The friction of locating files becomes even more evident when working in a team where everyone has different tooling and levels of comfort with the various options.

Here’s a hack solution that avoids any third party tooling and keeps things consistent for all members on the team. The general idea is to detect we are running in a simulator and then drop a symlink on your desktop. Checking out the environment variables available when running in the simulator reveals there is all the information we need to make this happen.

#if targetEnvironment(simulator)
    let environment = ProcessInfo.processInfo.environment
    if
        let rootFolder = environment["SIMULATOR_HOST_HOME"].map(URL.init(fileURLWithPath:))?.appendingPathComponent("Desktop/SimulatorData"),
        let simulatorHome = environment["HOME"].map(URL.init(fileURLWithPath:)),
        let simulatorVersion = environment["SIMULATOR_RUNTIME_VERSION"],
        let simulatorName = environment["SIMULATOR_DEVICE_NAME"],
        let productName = Bundle.main.infoDictionary?["CFBundleName"]
    {
        let symlink = rootFolder.appendingPathComponent("\(productName) \(simulatorName) (\(simulatorVersion))")
    
        let fileManager = FileManager.default
        try? fileManager.createDirectory(at: rootFolder, withIntermediateDirectories: true)
        try? fileManager.removeItem(at: symlink)
        try? fileManager.createSymbolicLink(at: symlink, withDestinationURL: simulatorHome)
    }
#endif

Now whenever you run your app in the simulator a fresh new symlink will be created on your mac’s desktop making it really quick to go from thinking “I need to look in my apps data folder” to being there in Finder.

Wrap up

This problem has annoyed me for a long time (I wrote a Ruby gem 10 years ago to help with locating simulator directories 1). I’ve used multiple tools over the years from my gem, to various third party apps and now I mostly use simctl directly. This is my new favourite solution that requires no third parties or searching through my zsh history - it’s only taken 10 years of pushing this particular stone up a hill to come up with this idea 🤦🏼‍♂️.

  1. https://rubygems.org/gems/sidir/versions/0.0.5 don’t use 

Add name for debugging

A really useful trick when debugging is to subclass the thing you are interested in just to make it easier to search for inside the various debugging tools.


Worked example

I was hunting down a retain cycle using Xcode’s memory graph tool but although the tool is excellent the code I was debugging was not really set up to be useful. In my example I had a CustomView (not its real name) that is used many times but I was only interested in one particular usage. When spinning up the Debug memory graph tool I get presented with something like the below:

Debug memory graph with lots of CustomView instances

As you can see in the above for this run of the app I have 218 instances of CustomView, which means it will be a mighty task to try and locate the correct one before I can conduct my analysis. Keep in mind that debugging could take multiple runs so I’d have to repeat the process of finding my view each time before I can doing any real investigation.

To make things easier on myself if I know roughly the area of code that might be causing the issue I can create a subclass to help make this search easier e.g.

class SentinelView: CustomView {}

Now at the call site in question I instantiate my SentinelView instead of CustomView, everything behaves the same except when I open the memory graph tool this time my job is much simpler

Debug memory graph with lots of CustomView instances and one SentinelView instance


Other uses

Here’s an example of printing the view hierarchies recursiveDescription

Recursive description of a view hierarchy

Here’s adding a symbolic breakpoint to this one type

Adding a symbolic breakpoint

Here’s locating the view in the view hierarchy debugger

Adding a symbolic breakpoint

There are plenty more places like instruments, logging etc to make use of this technique.


Conclusion

This technique has been in my back pocket for many years and it’s always been really useful. For as long as we have tools that show information and log class names it’s always helpful to be able to help narrow the search.