Lazy initialization for constants

I started writing a module for cross-platform complex number support in Swift.
For complex numbers, there are obviously two major representation formats:

  • Standard form a+ib
  • Polar form r*exp(ix)

I would like to support both formats for my complex number struct and therefore allow initialization for both formats. Conversion between the two however is quite expensive and therefore I only want conversion to happen when really needed. So it makes sense to use lazy initialization as follows:

private let _real: Double?
/** The real value of the complex number */
lazy var real: Double = { _real ?? (rad*cos(phi)) }()
private let _im: Double?
/** The imaginary value of the complex number */
lazy var im: Double = { _im ?? (rad*sin(phi)) }()
private let _rad: Double?
/** The radius of the complex number in polar format */
lazy var rad: Double = { _rad ?? sqrt(real*real + im*im)}()
private let _phi: Double?
/** The angle of the complex number in polar format */
lazy var phi: Double = { _phi ?? atan(im/real) }()

The problem I have now however is, that I need to use variables for the respective fields, when they actually should be constants. It gets even worse, because I can no longer access the lazy fields for constants of my complex number type, since access to a lazy field means modifying the value of the constant in Swift.

So the general problem is, that there are some occations, where it makes sense to delay initialization until usage for constants (whose values are set during initilaization, but not calculated then) for pure performance gain. However Swift does not allow lazy initialization for constants.

My question therefore is: What is the exact motivation behind this design decision (i.e. why is there no such feature) and is there a way to still achieve what I want without such an explicit feature?

2 Likes

Lazy initialization necessarily means that the field starts out uninitialized and is initialized later. Semantically, though, the value is never observed in its uninitialized state, so why is that important? The answer is when the lazy variable is used as a struct member. When a struct is used with let, the storage that backs it is considered completely immutable. For a global value, that means it can be put into a read-only section of memory—which, of course, means that no lazy initialization is possible. Even without that, the compiler will still assume that any data read from memory is still valid no matter what other operations have been invoked on the struct, which could lead to the lazy property getting initialized more than once.*

All of this is about optimization, though, and we could decide to forego that optimization if the compiler can't prove that a struct value has no lazy let members. In most cases where that can't be proven, the compiler already wouldn't be able to put the data in read-only memory anyway. I'm not sure if there are additional complications I'm missing, though.

* Note that it's not just possible but likely for a lazy struct property to be initialized more than once:

var wrapper = SomeStructWithLazyProperty()
var copy = wrapper // copied without resolving the lazy property
print(copy.lazyProp) // cannot modify 'wrapper'...
print(wrapper.lazyProp) // ...so the value gets computed again here
var anotherCopy = wrapper
print(anotherCopy.lazyProp) // but this one can use the cached value
3 Likes

There is no language feature targeting this (at least yet). So it does require boilerplate, but it is not that hard to do.

I posted an explanation on Stack Overflow several years ago. The question was a little different, but the nature of the underlying solution is the same.

More specifically, when it comes to implementing cached conversions between ways of defining a structure’s value, you can find a working example here. The linked code is the relevant section of a CalendarDate structure (analogous to your ComplexNumber), whose underlying value can be one of several internal definitions such as GregorianDate, HebrewDate, RelativeDate, etc. (analogous to your StandardComplexNumber and PolarComplexNumber). CalendarDate can be initialized in any of those ways, but can always generate a converted copy in the other formats in order to vend the properties related to those other definitions. All conversions are cached, and unlike lazy, they are even shared across copies (until something is mutated).

In your case, your list of definitions is closed (whereas CalendarDate supports arbitrary custom definitions), so you can probably reduce many aspects down to a private access level. For the same reason, you can skip having something like the DateDefinition protocol, and just have a cache with the two definitions as direct properties.

1 Like

Your best option is to fake immutability by hiding its setter in the public interface like so:

private(set) lazy var phi = { ... }()

Interesting. So it seems that lazy initialization is not allowed because it prevents some optimizations. I understand the performance problem for multiple computations of a lazy variable. But with lazy constants, couldn't there be a simple optimization, which makes a struct value use a pointer to the original struct for lazy constants which are initialized in a pure manner instead of the constant value itself, when copying the value?

let wrapper = SomeStructWithLazyConstant()
let copy = wrapper
print(copy.lazyPureConst) // resolve the location of the value by following its pointer, find out that it is not initialized yet, initialize it and write its value into wrapper (and possibly copy)
print(wrapper.lazyProp) // won't be computed again, since it was already computed in the previous instruction

If the struct value is stored into a global, you can't be sure that the original value is going to still be alive when you go to access the lazy property.

Thanks for the detailed reply. This seems to do the job, however the performance overhead with using an inner class is quite a bummer. But I will use this system until there is support for lazy constants (which I think would still be great, even with the optimization issues).

Right, missed that one.