Swift code reviews: Lazy variables

Continuing with my Code Reviews series, I write these posts as more in-depth self-study regarding comments, recommendations, or alternatives I’ve been pointed out during code reviews. Today I’m taking a look at Swift’s Lazy keyword. For this post, I used different resources, like Sean Allen’s video, the Swift Docs, and Paul Hudson’s Hacking With Swift to measure the actual loading speed of object init.

We use the Lazy keyword when we do not want to initialize an object or some of its properties right away, but we put off the work until we need it (The so-called JIT, or Just-In-Time mechanism). There could be different reasons for this, for example:

  • The object does not have all the information on init, so we delay its implementation details for later on.
  • We need to do heavy processing, and may not be needed just at object initialization.

Let’s say we have a Pokemon class, but in order to create an introduction for this Pokemon, we need to wait until it has been already initialized, as otherwise we still don’t have all the necessary values to set this up. In this case, we can use lazy in order to set this up later when we actually need it:

struct Pokemon {
    var name: String
    var team: String
    var type: String

    lazy var introduction = {
        return "Team \(team): \(name) [\(type)] entering the game."
    }()
}
var pikachu = Pokemon(name: "Pikachu", team: "Blue", type: "Electric")
print(pikachu.introduction)

> Team Blue: Pikachu [Electric] entering the game.

Can’t we use a computed property here, instead? We can, but there are 2 caveats:

  • Each time you access the object it will get recomputed. While using a lazy variable it will be calculated just once, then saved in memory.
  • For some cases like the string above, it is not a big deal, however, what happens when we have to do heavier calculations? Let’s check:

We modify the object to perform some calculations as part of its initialization, in this case, how many games have the Pokemon played via gamesPlayed. We’re just creating a large array, iterating through it 4.000 times, appending each value, and then retrieving the last one. In order to see how much time it takes to compute, we also use CFAbsoluteTimeGetCurrent()

struct Pokemon {
    var name: String
    var team: String
    var type: String
    var gamesPlayed = Calculator.calculateGamesPlayed()

    lazy var introduction = {
        return "Team \(team): \(name) [\(type)] entering the game."
    }()
}

struct Calculator {
    static func calculateGamesPlayed() -> Int {
        let start = CFAbsoluteTimeGetCurrent()
        
        var games : [Int] = []
        for i in 1...4_000 { games.append(i) }
        
        let diff = CFAbsoluteTimeGetCurrent() - start
        print("\(diff)s loading time")
        
        return games.last!
    }
}
var pikachu = Pokemon(name: "Pikachu", team: "Blue", type: "Electric")
print(pikachu.introduction)

> 2.395401954650879s loading time
> Team Blue: Pikachu [Electric] entering the game.

There hasn’t been a moment yet where we actually needed to get the played games data anywhere, so why not set that property as lazy and just load it if and when we actually need it? We get the Pokemon immediately initialized, and we will only get the gamesPlayed property when we actually need it. We’re leaving these for later:

struct Pokemon {
    var name: String
    var team: String
    var type: String

    lazy var gamesPlayed = {
        return Calculator.calculateGamesPlayed()
    }()

    lazy var introduction = {
        return "Team \(team): \(name) [\(type)] entering the game."
    }()
}

struct Calculator {
    static func calculateGamesPlayed() -> Int {
        let start = CFAbsoluteTimeGetCurrent()
        
        var games : [Int] = []
        for i in 1...4_000 { games.append(i) }
        
        let diff = CFAbsoluteTimeGetCurrent() - start
        print("\(diff)s loading time")
        
        return games.last!
    }
}
var pikachu = Pokemon(name: "Pikachu", team: "Blue", type: "Electric")
print(pikachu.introduction)

> 0.009340047836303711s loading time
> Team Blue: Pikachu [Electric] entering the game.

We just saved 2.4 seconds loading the object. This is only for 1 object, what happens when each team has 10 players that must be loaded per team? And other elements in the app? We’re saving hundreds or thousands of seconds of computing power just to initialize our app, and reallocating them to the right time, when it actually needs to load.

Leave a Reply

%d bloggers like this: