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 šø.
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
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.
ā ļø 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
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.
-
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.Ā ↩ -
Note that in a playground or in the REPL, as the code is evaluated like big
main()
function, declaringlet 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 thoselet
global constants are really lazy.Ā ↩ -
By the way, the use of a
static let
inside aclass
is the recommended way to create singletons in Swift (even if you should avoid them š), asstatic let
is both lazy, thread-safe, and only created once.Ā ↩ -
ā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.Ā ↩ -
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.Ā ↩
-
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?)Ā ↩ -
Yes I was too lazy to write a conclusion. But as this article just demonstrated, being lazy makes you a good programmer, right? šĀ ↩