Inline types and functions in Swift
Separating concerns and decoupling are both incredibly important things to do when it comes to writing code that’ll have a much higher chance of standing the test of time — but splitting code up too much can often have quite the opposite effect. Even though an app or system might technically have a rock-solid architecture that follows all the rules of good system design, our code can still end up feeling hard to navigate.
One way to mitigate that, while still maintaining a solid overall structure in our code base, is to inline functionality whenever two pieces of code are heavily related. This week, let’s take a look at how that can be done using inline types and functions, and how — when tactically deployed in the right situations — it can make our code a bit easier to follow.
Container types
Let’s start with an example. When working with some kind of web API — whether that’s something we control ourselves, or an external service that we rely on — it’s very common to end up with a mismatch between the format of the data returned by the API, and the structure of our Swift code.
One pattern that’s particularly common when working with lists of data — such as search results — is for a web API to reply with a JSON object instead of an array, looking something like this:
{
"results": [
...
]
}
Let’s say that we want to use Codable
to decode the above data, but we’re only interested in the actual array of results, not the root object. That’d require us to create some form of Container
type that we decode our web API response as, and then afterwards we can extract our results from that instance:
struct Container: Codable {
var results: [SearchResult]
}
But the question is — where to put the above kind of type? While we could make it a top level type and put it in its own Swift file, since it’s only ever used to facilitate JSON decoding of our search results — wouldn’t it be easier to simply put it right next to that code?
That’s exactly what inline types allow us to do — since Swift supports type definitions not only at the top level of our program, but also inside other types and even inside individual functions — like this:
private extension SearchResultsLoader {
func decodeResults(from data: Data) throws -> [SearchResult] {
struct Container: Decodable {
let results: [SearchResult]
}
let decoder = JSONDecoder()
let container = try decoder.decode(Container.self, from: data)
return container.results
}
}
With the above approach, whoever will end up maintaining our code (which might turn out to be a future version of ourselves), won’t have to go search through the project to find which container type that’s being used — it’s right there next to the code that’s using it! 👍
Simple mocks
Another situation in which inline types can come in handy is when writing unit tests. Especially when testing some of our more complex code, we usually end up having to do some form of mocking — and when doing so it’s usually a good idea to try to keep those mocks as simple as possible.
Let’s say that our app uses an Action
metaphor to enable multiple, independent actions to be updated at regular time intervals, through a protocol looking like this:
protocol Action: AnyObject {
func update(at timestamp: TimeInterval) -> UpdateOutcome
}
When the above update
method is called, each action returns an UpdateOutcome
— which is an enum containing cases like finished
, cancelled
, and so on. That outcome is then used by an ActionController
to determine whether an action should continue receiving updates, or if should be removed.
Now let’s say that we’d like to write a unit test that verifies that our ActionController
correctly removes an action that returned the cancelled
outcome when updated. To do that, we’ll mock our Action
protocol to create an action that’s hard-wired to cancel itself on its first update, like this:
class CancelingActionMock: Action {
func update(at timestamp: TimeInterval) -> UpdateOutcome {
return .cancelled
}
}
Similar to our search results container example from before, since the above mock is very specific to the unit test we’re about to write (given that it’s hard-wired to always cancel itself), perhaps the most appropriate place to put it is actually inline within our test. That way, we could simply call it Mock
, and have all of our test code contained within a single method:
class ActionControllerTests: XCTestCase {
func testCancelledActionRemoved() {
class Mock: Action {
func update(at timestamp: TimeInterval) -> UpdateOutcome {
return .cancelled
}
}
let controller = ActionController()
let action = Mock()
controller.enqueue(action)
XCTAssertTrue(controller.contains(action))
controller.update(at: 0)
XCTAssertFalse(controller.contains(action))
}
}
One alternative to the above would be to instead make our mock a bit more generic and easier to reuse — for example by naming it ActionMock
and to enable it to be initialized with any given UpdateOutcome
to return, which would let us make it a top level, shared type.
While that’s also a completely valid approach (and arguably more appropriate in cases where the same protocol needs to be mocked multiple times), it does come with a slight increase in complexity by introducing additional types that we need to be aware of when reading and writing our tests. Keeping things inline sort of “forces” us to keep our mocks hard-wired and simple, which is usually a good thing for maintainability and clarity.
Self-executing closures
It’s not only types that can sometimes benefit from being inlined into the context in which they’re being used — the same can also be true for logic that computes a program’s data or state.
For example, let’s say that we’re writing a Swift script to be able to quickly check how many public repositories a given GitHub user has. Our script will get the username that we want to look up through a command line argument, and will then construct a GitHub web API URL using that name, and finally perform the network request and parse the result.
Currently, we’ve structured our script using separate functions that each perform one of the above operations, like this:
let name = try extractNameFromArguments()
let url = try constructURL(for: name)
let user = try loadUser(from: url)
print("User named \(name) has \(user.repoCount) public repos")
The above looks really neat, but if we start looking into the underlying functions, it turns out that there’s not that much logic required for each step. So rather than having to constantly jump in and out of functions when navigating our code, we could instead transform those functions into self-executing closures.
Like we took a look at in “Using lazy properties in Swift”, a self-executing closure is simply a normal closure that immediately calls itself when defined — and can provide a great way to encapsulate the setup of an object or value — like this:
let redButton: UIButton = {
let button = UIButton()
button.backgroundColor = .red
return button
}()
Using the above technique for our GitHub script would allow us to achieve the same level of encapsulation as before, but with all of our logic now inlined according to the main flow of the script — giving us a very nice overview of all of our operations and their implementations:
let name: String = try {
let arguments = CommandLine.arguments
guard arguments.count > 1 else {
throw Error.noName
}
return arguments[1]
}()
let url: URL = try {
let urlString = "https://api.github.com/users/" + name
guard let url = URL(string: urlString) else {
throw Error.invalidName
}
return url
}()
let user: GitHubUser = try {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
return try decoder.decode(GitHubUser.self, from: data)
}()
print("User named \(name) has \(user.repoCount) public repos")
Apart from scripting and computing lazy properties, self-executing closures can also be a great way to encapsulate the computation of some of our more complex local variables — again without the need for additional functions or context switches when reading our code.
Inline function definitions
Finally, let’s take a look at how even proper function declarations can be inlined — and how that can be really useful, especially when dealing with recursive code.
Let’s say that we’re building a note-taking app, and that one of our most appreciated convenience features is that our app lets the user quickly open the first note that matches a given search query. That feature is implemented through a NoteFinder
class, which at the moment looks like this:
class NoteFinder {
private var files: [File]
private var findIndex = 0
func findFirstNote(matching query: String,
then handler: @escaping (Note?) -> Void) {
findIndex = 0
performFind(withQuery: query, handler: handler)
}
private func performFind(withQuery query: String,
handler: @escaping (Note?) -> Void) {
guard findIndex < files.count else {
return handler(nil)
}
let file = files[findIndex]
findIndex += 1
file.read { [weak self] content in
guard content.contains(query) else {
self?.performFind(withQuery: query, handler: handler)
return
}
handler(Note(content: content))
}
}
}
There are two main problems with the above implementation. First, we have to pass both the query and the completion handler through to a second function, which seems a bit unnecessary given that the initial findFirstNote
function doesn’t really do much. Second, and perhaps even more important, is that our note finding functionality is currently stateful.
Since we reset findIndex
to 0
each time we call findFirstNote
, we could quite easily end up with some pretty nasty bugs if we ever were to perform two find sessions at the same time. One way to solve that problem would be to also pass the current index to performFind
, but that’d just add to the first problem of having to pass a lot of data through to a second function.
Instead, let’s again use inlining — this time to actually place our entire performFind
function inline within findFirstNote
. That way we can both give it a simpler name and signature (we’ll now simply call it matchNext
), and since inline functions can both capture state — just like closures — and call themselves recursively, we’ll end up with a much simpler implementation without leaking any state:
class NoteFinder {
private var files: [File]
func findFirstNote(matching query: String,
then handler: @escaping (Note?) -> Void) {
// These local variables can be captured by our inline
// function, removing the need for the class itself to
// to manage any state local to this function.
var index = 0
let files = self.files
func matchNext() {
guard index < files.count else {
return handler(nil)
}
let file = files[index]
index += 1
file.read { content in
guard content.contains(query) else {
return matchNext()
}
handler(Note(content: content))
}
}
matchNext()
}
}
If the above technique looks familiar — it might be because it’s very similar to the way task sequences were implemented in “Task-based concurrency in Swift”.
While recursion can sometimes be a bit of a double-edged sword (especially since Swift doesn’t guarantee “Tail call optimization”, which can cause our call stack to become very deep for large datasets) — it can let us encapsulate all the state that an algorithm needs within the algorithm itself, which can both make its code easier to follow — and make it more robust as well.
Conclusion
Inlining can be a great tool for situations when we need an additional type or function, but we don’t want to expose it outside of the scope that it’s being used in. By placing that kind of simpler, more narrowly scoped, types and functions right next to the code that uses them — we can also make our code a bit easier to navigate and work with, by reducing context switching.
The fact that we can choose to define types and functions not only at the top level of a program, but also inline within other types or functions, is another example of just how flexible Swift is in terms of structure and syntax. However, just like with other features of the same nature, it’s important not to take things too far.
Just like how too much separation and decoupling can make a code base hard to navigate, too many inline types and functions can cause our code base to become more or less one big blob of logic. My recommendation — avoid the extremes and use each tool when it’s the most appropriate, and refactor things as they outgrow their original design.
Questions, comments or feedback? Contact me, or find me on Twitter @johnsundell.
Thanks for reading! 🚀