3. Immediately Invoked Closures vs Custom Initializers in Swift Development

Jun 27, 2020 | 22:53
Immediately invoked closures are often used to create, configure, and assign an instance of a type all at once. However this can also be accomplished via a custom initializer. Today we discuss the benefits and drawbacks of each approach.

Listen and/or subscribe via:

We will be discusing this topic in the context of the Gigasecond exercise found at exercism.io. More specifically we'll be discussing two common solutions, one using an immediately invoked closure and the other using a custom initializer. Let's get started.

Exercise

Calculate the moment when someone has lived for a gigasecond which is 10^9 (1,000,000,000) seconds.

Here's an example test:

class GigasecondTests: XCTestCase {
    func test1 () {
        let gs = Gigasecond(from: "2011-04-25T00:00:00")?.description
        XCTAssertEqual("2043-01-01T01:46:40", gs)
    }
    ...

So we need a type Gigasecond with a failable initializer which takes a from: String argument and who's instances have a description: String property.

Solution starting point

Let's start with all the logic in the initializer.

struct Gigasecond {
    let description: String
    
    init?(from birthDateString: String) {
        // Create and configure date formatter.
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        dateFormatter.timeZone = TimeZone(identifier: "UTC")
        
        // Actually solve problem.
        guard let birthDate = dateFormatter.date(from: birthDateString) else { return nil }
        let oneGigasecondOldAt = birthDate.addingTimeInterval(1_000_000_000)
        self.description = dateFormatter.string(from: oneGigasecondOldAt)
    }
}

Take note of how the exact same DateFormatter instance is created and configured for each Gigasecond instance. We really only need one date formatter instance so let's move it to a static stored property on Gigasecond.

Immediately invoked closures

It's common in Swift to use an immediately invoked closure when we want to:

  • Create an instance.
  • Configure it's properties.
  • Assign it to a stored property.
  • Keep all this code in one place.

This is exactly what we need for our dateFormatter property. Here's what it looks like:

struct Gigasecond {
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        formatter.timeZone = TimeZone(identifier: "UTC")
        return formatter
    }()

    let description: String

    init?(from birthDateString: String) {
        guard let birthDate = Self.dateFormatter.date(from: birthDateString) else { return nil }
        let oneGigasecondOldAt = birthDate.addingTimeInterval(1_000_000_000)
        self.description = Self.dateFormatter.string(from: oneGigasecondOldAt)
    }
}

Pros

This works. It compiles and the tests pass. More experienced Swift developers will recognize this pattern and know exactly what it's doing and why it's being used.

Cons

The syntax is cryptic and the code complex. I imagine less experienced Swift developers will have a hard time understanding exactly what is happening and why it's necessary. There are at least 5 concepts happening here:

  1. Declaration of static stored property.
  2. Assignment of value into a stored property.
  3. Closure expressions.
  4. Immediately invoking a closure.
  5. The actual logic inside the closure.

While declaration and assignment of static stored properties may be straight forward, immediately invoking closure expressions is more difficult to grasp and would be hard to search for on the internet without knowing what they're called.

Also, it's easy to get this wrong.

With a minor modification this code ends up being a computed property:

static var dateFormatter: DateFormatter {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
    formatter.timeZone = TimeZone(identifier: "UTC")
    return formatter
}

With an even smaller modification, and single fix-it, this ends up being a function stored in a property:

static let dateFormatter = { () -> DateFormatter in
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
    formatter.timeZone = TimeZone(identifier: "UTC")
    return formatter
}

Neither of these are what was intended or provide any benefit over our solution starting point. So what alternatives do we have?

Custom initializers

Let's look back to why we wanted an immediately invoked closure in the first place. We needed to:

  • Create an instance.
  • Configure it's properties.
  • Assign it to a stored property.
  • Keep all this code in one place.

Leaving out the assignment we have "create and configure an instance all in one place". That's a near-perfect description of initializers. In fact the only reason we considered an immediately invoked closure in the first place is because DateFormatter doesn't the required initializer. However in Swift we can simply add our own initializer via an extension.

extension DateFormatter {
    convenience init(
        dateFormat: String,
        timeZone: TimeZone)
    {
        self.init()
        self.dateFormat = dateFormat
        self.timeZone = timeZone
    }
}

Now our dateFormatter can simply be created and assigned like any other type.

struct Gigasecond {
    static let dateFormatter = DateFormatter(
        dateFormat: "yyyy-MM-dd'T'HH:mm:ss",
        timeZone: TimeZone(identifier: "UTC")!
    )
    
    let description: String

    init?(from birthDateString: String) {
        guard let birthDate = Self.dateFormatter.date(from: birthDateString) else { return nil }
        let oneGigasecondOldAt = birthDate.addingTimeInterval(1_000_000_000)
        self.description = Self.dateFormatter.string(from: oneGigasecondOldAt)
    }
}

Pros

There is less syntax, complexity, and code at the call site. There are no minor modifications that would change the meaning of the code. This initializer can be reused elsewhere in the project or in other projects if added to a common framework.

Cons

The assignment of the stored property and the configuration are no longer in the same place. They could be on different lines, in different files, or even in different projects. There is more code in total because the extension is defined separately.

Conclusion

It's probably obvious by now I am in favor of the custom initializer for this particular case. But what do you think? Is there some important point I missed? Am I giving too much weight to one thing or another? Let me know and we can discuss it on a future episode.

Next episode

Next episode we'll be looking at "Moving Logic to Standard Types via Extensions in Swift Programming". This discussion will be in the context of the Difference Of Squares exercise found at exercism.io. Feel free to complete that exercise before the next episode and we can compare our answers.

Take care everyone.