Being Lazy



Today weā€™ll see how we can be more efficient āš”ļø byā€¦ being lašŸ’¤y šŸ˜“.
In particular, weā€™ll talk about lazy var and LazySequence. And cats šŸ˜ø.

Lazy cat

The problem

Letā€™s say you are making a chat app and want to represent your users using an avatar. You might have different resolutions for each avatar, so letā€™s represent them this way:

extension UIImage {
  func resizedTo(size: CGSize) -> UIImage {
    /* Some computational-intensive image resizing algorithm here */
  }
}

class Avatar {
  static let defaultSmallSize = CGSize(width: 64, height: 64)

  var smallImage: UIImage
  var largeImage: UIImage

  init(largeImage: UIImage) {
    self.largeImage = largeImage
    self.smallImage = largeImage.resizedTo(Avatar.defaultSmallSize)
  }
}

The problem with this code is that we compute the smallImage during init, because the compiler enforces us to initialize every property of Avatar in init.

But maybe we wonā€™t even use this default value, because weā€™ll provide the small version of the userā€™s Avatar ourselves. So weā€™d have computed this default value, using a computational-intensive image-scaling algorithm, all for nothing!

Possible solution

In Objective-C for similar cases we were used to use an intermediate private variable, in a technique which could be translated like this in Swift:

class Avatar {
  static let defaultSmallSize = CGSize(width: 64, height: 64)

  private var _smallImage: UIImage?
  var smallImage: UIImage {
    get {
      if _smallImage == nil {
        _smallImage = largeImage.resizedTo(Avatar.defaultSmallSize)
      }
      return _smallImage! // šŸ“
    }
    set {
      _smallImage = newValue
    }
  }
  var largeImage: UIImage

  init(largeImage: UIImage) {
    self.largeImage = largeImage
  }
}

This way we can set a new smallImage if we want to, but if we access the smallImage property without assigning it a value before, it will compute one from the largeImage instead of returning nil.

That is exactly what we want. But thatā€™s also a lot of code to write. And imagine if we had more than two resolutions and wanted this behavior for all the alternate resolutions!

Swift lazy initialization

Lazy cat on keyboard

But thanks to Swift, we can now avoid all of this glue code above and do some lazy codingā€¦ by just declare our smallImage variable to be a lazy stored property!

class Avatar {
  static let defaultSmallSize = CGSize(width: 64, height: 64)

  lazy var smallImage: UIImage = self.largeImage.resizedTo(Avatar.defaultSmallSize)
  var largeImage: UIImage

  init(largeImage: UIImage) {
    self.largeImage = largeImage
  }
}

And just like that, using this lazy keyword, we achieve the exact same behavior with way less code to write!

  • If we access the smallImage lazy var without assigning a specific value to it beforehand, then and only then will the default value be computed then returned. Then if we access the property later again, the value will already have been computed once so it will just return that stored value.
  • If we gave smallImage an explicit value before accessing it, then the computational-intensive default value will never be computed, and the explicit value we gave will be returned instead.
  • If we never access the smallImage property ever, its default value wonā€™t be computed either!

So thatā€™s a great and easy way to avoid useless initialization while still providing a default value and without the use of intermediate private variables! šŸŽ‰

Initialization with a closure

As with any other properties, you can provide the default value for lazy vars using an in-place-evaluated closure too ā€” using = { /* some code */ }() instead of just = some code. This is useful if you need multiple lines of code to compute that default value.

class Avatar {
  static let defaultSmallSize = CGSize(width: 64, height: 64)

  lazy var smallImage: UIImage = {
    let size = CGSize(
      width: min(Avatar.defaultSmallSize.width, self.largeImage.size.width),
      height: min(Avatar.defaultSmallSize.height, self.largeImage.size.height)
    )
    return self.largeImage.resizedTo(size)
  }()
  var largeImage: UIImage

  init(largeImage: UIImage) {
    self.largeImage = largeImage
  }
}

But because this is a lazy property, you can reference self in there! (note that this is true even if you donā€™t use a closure, like in the previous example).

The fact that the property is lazy means that the default value will only be computed later, at a time when self will already be fully initialized, thatā€™s why itā€™s ok to access self there ā€” contrary to when you give default values to non-lazy properties which gets evaluated during the init phase.

ā„¹ļø Immediately-applied closures, like the one used for the default values of lazy variables above, are automatically @noescape. This means that there is no need to use [unowned self] in that closure: there wonā€™t even be a reference cycle here.

lazy let?

You canā€™t create lazy let instance properties in Swift to provide constants that would only be computed if accessed šŸ˜¢. Thatā€™s due to the implementation details of lazy which requires the property to be modifiable because itā€™s somehow initialized without a value and then later changed to the value when itā€™s accessed1.

But as weā€™re talking about let, one interesting feature about it is that let constants declared at global scope or declared as a type property (using static let, not as instance properties) are automatically lazy (and thread-safe)2:

// Global variable. Will be created lazily (and in a thread-safe way)
let foo: Int = {
  print("Global constant initialized")
  return 42
}()

class Cat {
  static let defaultName: String = {
    print("Type constant initialized")
    return "Felix"
  }()
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    print("Hello")
    print(foo)
    print(Cat.defaultName)
    print("Bye")
    return true
  }
}

This code will print Hello first, then Global constant initialized and 42, then Type constant initialized and Felix, then Bye; demonstrating that the foo and Cat.defaultName constants are only created when accessed, not before.3.

Lazy cat on leash

āš ļø donā€™t confuse that case with instance properties inside a class or struct. If you declare a struct Foo { let bar = Bar() } then the bar instance property will still be computed as soon as the Foo instance is created (as part of its initialization), not lazily.

Another example: Sequences

Letā€™s take another example, this time using a sequence / Array and some high-order function4 like map:

func increment(x: Int) -> Int {
  print("Computing next value of \(x)")
  return x+1
}

let array = Array(0..<1000)
let incArray = array.map(increment)
print("Result:")
print(incArray[0], incArray[4])

With this code, even before we access the incArray values, all output values are computed. So youā€™ll see 1,000 of those Computing next value of ā€¦ lines even before the print("Result:") gets executed! Even if we only read values for [0] and [4] entries, and never care about the othersā€¦ imagine if we used a more computationally intensive function than this simple increment one!

Lazy sequences

Lazy Cat in sequence

Ok so letā€™s fix the above code with another kind of lazy.

In the Swift standard library, the SequenceType and CollectionType protocols have a computed property named lazy which returns a special LazySequence or LazyCollection. Those types are dedicated to only apply high-order functions like map, flatMap, filter and such, in a lazy way.5

Letā€™s see how this work in practice:

let array = Array(0..<1000)
let incArray = array.lazy.map(increment)
print("Result:")
print(incArray[0], incArray[4])

Now this code only print thisā€¦

Result:
Computing next value of 0ā€¦
Computing next value of 4ā€¦
1 5

ā€¦demonstrating that it only apply the increment function when the values are accessed, and not when the call to map appears, and only apply them to the values being accessed, not all the one thousand values of the entire array! šŸŽ‰

Thatā€™s way more efficient! This can change everything especially with big sequences (like this one with 1,000 items) and for computationally-intensive closures.6

Chaining lazy sequences

One last nice trick with lazy sequences is that you can of course combine the calls to high-order functions like youā€™d do with a monad. For example you can call map (or flatMap) on a lazy sequences, like this:

func double(x: Int) -> Int {
  print("Computing double value of \(x)ā€¦")
  return 2*x
}
let doubleArray = array.lazy.map(increment).map(double)
print(doubleArray[3])

And this will only compute double(increment(array[3])) when this entry is accessed, not before, and only for this one!

On the contrary using array.map(increment).map(double)[3] instead (without lazy) would have computed all the output values of the whole array sequence first, and only once all values have been computed, extract the 4th one. But worse than that, it would have iterated on the array twice, one for each application of map! What a waste of computational time it would have been!

Conclusion

Be lazy7.

Lazy cat on sofa

  1. Some discussion are still ongoing in the Swift mailing lists about how to fix that and allow lazy let to be possible, but for now in Swift 2 thatā€™s how it is.Ā 

  2. Note that in a playground or in the REPL, as the code is evaluated like big main() function, declaring let foo: Int at top-level will not be considered a global constant and thus you wonā€™t observe this behavior. Donā€™t let the special case of playgrounds or REPL fool you, in a real project those let global constants are really lazy.Ā 

  3. By the way, the use of a static let inside a class is the recommended way to create singletons in Swift (even if you should avoid them šŸ˜‰), as static let is both lazy, thread-safe, and only created once.Ā 

  4. ā€œhigh-order functionsā€ are functions that either take another function as a parameter, or return a function (or both). Example of high-order functions are map, flatMap, filter, etc.Ā 

  5. In practice, those types just keep a reference to the original sequence and a reference to the closure to apply, and only do the actual computation of applying the closure on one element when that element is accessed.Ā 

  6. But beware then ā€” that according to my experimentations at least ā€” the computed value isnā€™t cached (no memoization); so if you request incArray[0] again it will compute the result again. We canā€™t have it allā€¦ (yet?)Ā 

  7. Yes I was too lazy to write a conclusion. But as this article just demonstrated, being lazy makes you a good programmer, right? šŸ˜œĀ