Property initialization using a KeyPath within an init method

Given:

struct Foo {
    let bar: Int
}

In an init method, you can do:

    init() {
        self.bar = 42
    }

However, unfortunately, you cannot do:

    init() {
        self[keyPath: \.bar] = 42
    }

This error is thrown: "'self' used before all stored properties are initialized."

This seems like a compiler bug, or at least, a counter-intuitive aspect of keypaths. If a property setter works on self before its properties are initialized, then a keyPath subscript should also work.

So I would like to pitch the idea of updating keypaths so they can be used to init properties, perhaps using a new type of keypath if that seems best.

I feel like this would greatly enhance the power of KeyPaths for purposes of dependency injection.

We may need to require that any init which relies on passed-in keypaths must be a failable init, and/or provide a convenience check to determine whether some set of keypaths is sufficient to initialize an instance of a given type.

One approach could be to conditionally cast passed-in keypaths as? WritableKeyPath within the init method; if a variable has no value yet then it would be writable at that point in time, and within the init method's scope.

Another approach might be to cast to a new InitializableKeyPath type.

I'm sure the geniuses here can think of something better, or more likely, explain why this is a horrible idea, but at any rate I wanted to open this up for discussion. It's an idea stemming from a prior pitch I made, but I realized this feature would be needed first in any event, so would like to know whether it's feasible or desirable.

(Apologies if it's been pitched before, I did a search but did not come up with an exact match. I think this could work nicely though with some of the other keypath-related proposals, though!)

1 Like

The most sound approach I can think of would be to have an initializer that takes a mapping of key paths to initial values and creates a value of the type if the set of key paths is complete, or returns nil or throws an error otherwise:

struct Foo {
  init?(fields: [PartialKeyPath<Foo>: Any])
}

As part of a key path based reflection API, this could perhaps be a global function that works on any type we have reflection info for:

func createInstance<T>(from: [PartialKeyPath<T>: Any]) -> T?
5 Likes

Joe's answer makes sense to me, but just to clarify the original point: the problem with using the keyPath subscript is that either the compiler knows what the key path is (in which case you could have used a setter), or it does not (in which case it can't prove that you haven't initialized this property already).

Fortunately, there's very little reason to use let in structs anyway, since modifying a var property is equivalent to making a new struct with one property changed. So for the purposes of dependency injection, you could make a default-initialized struct and then modify the fields afterwards. (The one downside here is that properties that might be expensive to compute will have to become optional.)

This is essentially what I proposed in the "Compositional Initialization" pitch, however I think that pitch was not focused enough on this core aspect, which seems to me like a foundation upon which some very powerful and nice patterns could be built.

One aspect of that prior pitch that we would likely need, is a new type that provides a typesafe, type-eraseable wrapper around a KeyPath-Value pair. In the prior pitch, I gave an example Playground where this new type was called a "Property," (however I'm not sure that's a great name for it—I'm sure we can think of something better). There is also a working implementation of a failable init method that takes in a variadic argument of "Property" type, and tries to create the object, using Mirror to determine which non-optional children must be initialized.

However the current limitations of Swift still require a default value for any variable that would be initialized this way, which is not really ideal since you are wasting processor cycles to initialize those variables only to never actually use those values. Some types you may want to initialize only once because their initialization incurs a performance hit, or because you want the ivar immutable.

More important to me is the case of using keypaths for dependency injection. Current Swift DI frameworks rely on a bunch of gymnastics like code-generation to make sure we're setting the right values on the right types and to "validate the DI graph".

I might suggest however that if we could use keypaths in the manner described above, then it might alleviate the need for these extra curricular activities, since keypaths already provide the same abilities and guarantees—with the sole drawback that they cannot be used to initialize properties, and so any DI scheme based on them would require default values for everything.

While that may not be a bad compromise, still, I wonder if we might be able to improve upon this.

1 Like