Delay variable creation with closure

aunnnn
3 min readApr 13, 2018

--

Useless variable problem

You want to fetchObject with id, if failed, use the default object you pass in:

Well, problem is that if it’s a success, you don’t need defaultObject .

You might try to put makeDefaultObject() where it’s needed, but that is so inflexible as you might want to use different default objects:

Enter the Builder

Note: This is totally unrelated to the Builder design pattern.

Whatever code you write, you can wrap it inside a closure (or function) and execute them later.

For example, instead of making a UIImage:

let image = UIImage(named: "img.png")!

You can step back, and make () -> Image instead:

let imageBuilder = { UIImage(named: "img.png")! }

Unlike image, imageBuilder doesn't occupy any memory for an image yet (only a little for closure declaration). It’s not a thing yet, just a piece of code, something “on the paper”, a recipe.

You can do something similar with URLRequest:

let requestBuilder: () -> URLRequest = {
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.httpBody = ...
request.httpMethod = ...
request.allHTTPHeaderFields = ...
return request
}

Because function (or closure) is a first-class citizen in Swift (treated as a normal variable, can be passed as parameters, etc.), you can pass the builder around.

Whoever receives this builder, has the power for executing it whenever it needs, and not executing it:

func callRequestIfNeeded(_ builder: () -> URLRequest) { 
...
if needed {
let request = builder()
// do something with request
}
...
}

Builder is like a recipe instead of actual food, you give that to someone and they get that food whenever they want.

Concise inline builder with autoclosure

Swift supports @autoclosure, which automatically wraps the variable you pass in with closure. It means even if you pass in a variable to a function, you get a builder inside.

func work(on number: @autoclosure () -> Int) {
let _number = number()
...
}
work(on: 100) // number is { 100 }

As you can see, unlike normal closure, you can’t pass any parameters to the closure though (() -> Int).

Also, it doesn’t help much with multi-line builder like our requestBuilder, you still have to wrap it in some function or closure:

// @autoclosure here doesn't help much...
func callRequestIfNeeded(_ builder: @autoclosure () -> URLRequest) {
...
}
// Still need to create a closure/function
let requestBuilder: () -> URLRequest = { … }
callRequestIfNeeded(requestBuilder())

A little unintuitive, requestBuilder() is not called here yet. Imagine:

builder = { requestBuilder() }

It is executed when we call builder(), as we intended.

But don’t do this:

let request = requestBuilder() // This is executed already!
callRequestIfNeeded(request)

In this case, request is built already. @autoclosure just capture that result in a closure for later use.

XCTest already uses this technique

Look at the function declaration of assert, you can see that it heavily uses autoclosure:

public func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default ...)

What this means is that 1 == 1 in assert(1 == 1, “1 must equal 1”) is not executed right away, and the string "1 must equal 1" is not even really created yet! Be frugal like XCTest

Recap situations that we could use Builder

  • Default variable when it’s needed (e.g., on failure)
  • Lazy UITableViewCell’s view models (e.g., when displaying a lot of cells, just create those that are needed)
  • Lazy dependency injection from outside:
  • (Maybe that example is a bit too much, but you get the idea)

Conclusion

Builder can be used to delay variable creation until the time it’s used. It’s not necessary to use Builder in your app at all. Experiment first and use it as you see fit.

If this is useful, 👏 🙏 Feel free to comment!

--

--