I wanted to make a pitch (my first) for the ability to memoize computed properties. What follows is a summary, but I also have a much more detailed version here.
The memoization of computed properties would help to speed up some Swift programs relying on computed values (especially expensive ones) by only re-calculating the results when one of the properties they depend on has changed.
Anyone familiar with React and React hooks will know one of the most useful hooks is useMemo
, which provides this exact functionality for rendering UIs. While memoization would be broadly applicable to any Swift program, it may be especially helpful for use in SwiftUI, where preventing unnecessary re-renders can help optimize app performance.
In Swift, we can already achieve similar results, but it takes quite a bit of boilerplate.
Note: For these examples, imagine that getting the area is actually a much more expensive calculation. Getting the area is trivial, but for sake of demonstration, consider it as a proxy for something more complex.
Non-lazy Memoizing
First, let's look at a box with memoized area. It stores a private area property that's first calculated on initialization (not lazy). The didSet
observers for height
and width
will actively recompute the area after they are set.
struct Box {
var height: Double {
didSet { // On setting height, the updated area is actively recalculated
setArea()
}
}
var width: Double {
didSet { // On setting width, the updated area is actively recalculated
setArea()
}
}
private(set) var area: Double = 0
private mutating func setArea() {
area = width * height
}
init(height: Double, width: Double) {
self.height = height
self.width = width
setArea()
}
}
Lazy Memoizing
Next, let's look at a box with memoized and lazy area. It stores a private area property that's only calculated on first use. The didSet
observers for height
and width
don't actively recompute area. Rather, they invalidate the stored area by setting it to nil
. When the getter for area
finds a value, it uses it. When it finds nil
, it recomputes the area and stores it in the private property for later use.
struct Box {
var height: Double {
didSet { // On setting height, the memoized area is invalidated
_area = nil
}
}
var width: Double {
didSet { // On setting width, the memoized area is invalidated
_area = nil
}
}
// Private var to store memoized value
private var _area: Double? = nil
var area: Double {
mutating get { // Area is calculated lazily, only as needed (though this implementation means it can't be used on `let` constants)
guard let area = _area else {
let newArea = width * height
_area = newArea
return newArea
}
return area
}
}
init(height: Double, width: Double) {
self.height = height
self.width = width
}
}
This option is ideal for types that will always be var
variables, but unsuitable for use with types that could be declared as let
constants.
Proposed solution
The proposal is to create a simplified syntax to tell the compiler to synthesize the boilerplate outlined above. Let's take a look at what the structs above might look like with memo
and lazy memo
keywords (this syntax is just my initial proposal, but there are other options discussed here):
struct Box {
var height: Double
var width: Double
memo var area: Double { |width, height| in width * height }
}
... and the lazy version:
struct Box {
var height: Double
var width: Double
lazy memo var area: Double = { |width, height| in width * height }()
}
You can also read a much more detailed version of the above here.