Disclaimer: This post is purely experimental based on an idea I had recently, I’m personally not using this in production right now.
Laziness in Swift can be a very powerful tool, but it hasn’t reached its full potential yet. Recently, I came across a situation where I wanted to write something likes this:
class Foo {
lazy let bar: Bar
init(bar: () -> Bar) {
self.bar = bar
}
}
let foo = Foo(bar: { Bar() })
In other words: I wanted to pass the closure for creating a lazy property to the initializer of my class and make it a constant property. This doesn’t work because of two reasons:
lazy let
doesn’t exist in Swift.With those constraints, our only option is to use existing features to make this work. Here’s what I expect from my implementation:
No 3 will be easy to satisfy by using Generics. Due to No 1 & 2 it will be necessary to use a class
, as evaluating and storing the result of our closure implicitly mutates the object.
class LazyObject<T> {
var object: T {
if _object == nil {
_object = self.getter()
}
return _object!
}
private var _object: T?
private let getter: () -> T
init(getter: @escaping () -> T) {
self.getter = getter
}
}
Using our LazyObject
wrapper now looks like this:
let bar = LazyObject() { Bar() }
print(bar) // will be LazyObject<Bar>
print(bar.object) // will be Bar
Only on the last line the closure will actually be evaluated and our Bar
object created. Calling bar.object
again would just return the cached object and not evaluate the closure again.
So far so good. What about structs though? Right now, our wrapper doesn’t allow mutating the underlying object
. Let’s create a mutable subclass, MutableLazyObject
, that provides a scope allowing mutation:
class LazyObject<T> {
…
fileprivate var _object: T?
…
}
class MutableLazyObject<T>: LazyObject<T> {
func use(_ block: (inout T) -> Void) {
var object = self.object
block(&object)
_object = object
}
}
By moving the use(_:)
method to a subclass, we can explicitly decide whether we want to allow mutation or not.
let bar = MutableLazyObject() { Bar() }
bar.use { $0.message = "hello" }
Unfortunately though, using the LazyObject
class doesn’t look nice, violating expectation No 4. Let’s write a short helper function:
@inline(__always) func lazy<T>(_ getter: @autoclosure @escaping () -> T) -> LazyObject<T> {
return LazyObject(getter: getter)
}
This function allows us to use the shorthand syntax lazy(Bar())
. Much better!
The full code, including tests, is available on GitHub.