The original situation where I am finding this is an exercise in exercism.io (I am just learning Swift), but let me show you the root of the issue.
I have a structure that initializes some constant numeric properties (a, b, and c) in the constructor, and a constant String property (kind) that is a function of the other three. I'd like to compute kind only if needed.
Writing kind as a computed property doesn't make sense, there is no point in computing kind on each call, since it won't change between getter invocations. So, I thought it should be a lazy property:
lazy var kind: String = {
if a, b, c such and such {
return "foo"
} else {
return "bar"
}
}()
which can't be declared to be a constant.
That code compiles, but the test suite errs because of
error: cannot use mutating getter on immutable value: function call returns immutable value
XCTAssertEqual(expected, MyStruct(3, 4, -5).kind)
What does that error mean? Is there a way to accompish what I want?
When you do something like MyStruct(...) inline then that value is immutable (you can think of it as writing let myStruct = MyStruct(...)). The lazy property's getter will assign the value to its storage when you access the kind property (because it's the first time you're accessing it), but because you have an immutable value, that storage cannot be modified, and hence the error.
So, what you need to do is:
var myStruct = MyStruct(...)
XCTAssertEqual(expected, myStruct.kind)
Ahhh, I thought the problem was related to the property!
Awesome, did not know that tidbit about inline instances. The test suite is given with the exercise, so I'll make it pass rewriting that (making kind lazy is not a requirement of the problem), but exploring this has helped me learn that detail anyway.
One thing to keep in mind is that your computed property result is only stored in the value you invoked it on.
If you have a freshly constructed value of MyStruct and pass it to three separate functions which each invoked kind, then kind would be constructed and cached in three separate places. If you had invoked kind before passing the value to three different functions, then the cached value of kind would have been cheaply copied along with the rest of the properties.
Just wanted to point this out in case you didn't intend for this behavior. I can imagine scenarios where it would actually be harder to reason about the cost of computing kind using this mechanism vs pre-computing it for every value constructed.
I am seeing lazy properties have their gotchas, they are by no means lazy computations that are transparent to client code.
Following the logic of your example, I have seeing the == synthetized by Equatable seems to ignore lazy properties too, I tried comparing a couple of instances with a mix of "materialized" states. On a second reading, I realize the docs of Equatable mention specifically stored properties, which was an unknow concept to me. So, definitely stored and lazy properties act differently.
I find this very surprising. The proposal for synthesizing Equatable (SE-0185) doesn't say anything about lazy properties, but I would have expected them to be treated like normal stored properties. Lazy properties are basically stored properties (in fact, The Swift Programming Language calls them "lazy stored properties").
But apparently they're not, at least as far as the synthesized conformance is concerned. Two structs with different values in a lazy property compare equal:
struct S: Equatable {
var a: Int = 0
lazy var b: Int = 0
}
var x = S()
var y = S()
x.b = 1
y.b = 2
x == y // true
I found a rationale by @itaiferber in the pitch discussion for synthesizing Equatable (this mainly talks about Codable but also refers to the planned synthesis for Equatable/Hashable):
Derived conformance for Codable ignores all computed properties (including lazy properties and their associated storage). … [The thought process here is that accessing computed vars (and more so lazy vars) will generally have side effects. We don’t want to trigger side effects on encoding/checking for equality/hashing, and in general, those types of properties will not affect equality/hash value/encoded representation.]
— Pitch: Automatically deriving Equatable/Hashable for more value types - #49 by itaiferber
If the rationale says "in general, those types of properties will not affect equality/hash value/encoded representation", then Swift designers do not put these properties in the same bag as regular ones. If we talk about "instance variables", it seems like regular stored properties map to the concept of instance variables, and lazy/computed act more like methods with syntax sugar.
That surprises to me a bit, because since the notation is the same as the one for non-lazy stored properties, client code needs to know what is what. It is not transparent. Of course, this is deliberate and there must be a reason for that, so that means I need to understand their role better.
I wonder about the Optional(1) notation, what is that?
When you declare a lazy property, the compiler creates a hidden stored property that acts as the backing storage (where the property's value is stored after the lazy evaluation). This property's type must be Optional because it doesn't yet have a value before the lazy evaluation. This backing storage property is what you see in the output.
When you write this:
lazy var y: Int = 1
The compiler essentially rewrites it into this:
// Hidden backing storage
// The actual name is something like $__lazy_storage_$_y
var _y: Int? = nil
// The visible property becomes a computed property that forwards to `_y`
var y: Int {
get {
if _y == nil {
_y = 1
}
return _y!
}
set { _y = newValue }
}