Experiment: Lazy Objects in Swift

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:

  • Properties are being evaluated upon instantiation of the object, except for lazy variables. lazy let doesn’t exist in Swift.
  • Lazy properties must declare their initializer, meaning you can’t pass a closure as the initializer to it.

With those constraints, our only option is to use existing features to make this work. Here’s what I expect from my implementation:

  • Pass a closure, which won’t be evaluated until it’s necessary, to something.
  • The closure should only be evaluated once, otherwise this would defeat the purpose of the whole concept as we could just use a normal closure.
  • This should be universally usable with anything.
  • A readable, easy-to-understand syntax.

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.

About the Author

Florian Schliep is a software engineer & entrepreneur based in Berlin, Germany. He is available for consulting in mobile engineering strategy, hiring and due diligence.

© 2024 Florian Schliep. All rights reserved. — Imprint