KeyPath.Value Type Conversion

Currently, the KeyPath types do not support type casting or generic containers for the Value type. For example, if I have a key-path \Foo.int which is they type KeyPath<Foo, Int>, I cannot assign it to KeyPath<Foo, Any> or using a type that Int conforms to, such as KeyPath<Foo, Hashable>.

My question is; is this how key-paths where designed? If so why, and if not, why does it not work?

1 Like

Swift does not generally support covariance or contravariance in generic type parameters, and key paths would require both to support things like this in their current incarnation. Fully accurately capturing all the possible variance patterns in the type system today would furthermore be extremely complex due to the interactions with mutation; while a read-only key path could conceivably be handled as KeyPath<contravariant Root, covariant Value>, WritableKeyPaths would have to be invariant in both parameters, and ReferenceWritableKeyPath could only be contravariant in the Root parameter. Even if we could, this seems like it'd end up being a complex system hard for anyone to understand. If you want to work with a heterogeneous set of key paths, you can use PartialKeyPath<Foo> as a stand-in for read-only KeyPath<Foo, _>.

Possibly, in the fullness of time, I'd like to see us iterate on the design to make it based on protocols, which could let you express KeyPath where Root == Int, Value: Hashable as a set of constraints subsuming more specific constraints like Value == Int, Value == String, etc.

12 Likes

I actually attempted to create a lensing using some kind of abstraction over WritableKeyPath.

Neither some Numeric, nor any generic provide scope for this.

It would prove to be useful, though the benefit in my case would be just that we can infer the writable type from the return value, and not have to type cast it.

From this ->

// A use case requires it to have access to a global state controller...
protocol UseCase: AnyObject, CustomStringConvertible, DependencyEnvironment {
    associatedtype State: UseCaseState
    typealias StateAccess = WritableKeyPath<GlobalState, State>
    
    var stateController: GlobalStateController { get }
    var lens: StateAccess { get }
    var state: State { get set }
}

class UserAccountUseCase: UseCase {
    typealias State = UserAccountState
    var lens: StateAccess {
        \.userAccount
    }
}

To this ->

protocol UseCase: AnyObject, CustomStringConvertible, DependencyEnvironment {
    typealias StateAccess = WritableKeyPath<GlobalState, any UseCaseState>

    var stateController: GlobalStateController { get }
    var lens: StateAccess { get }
    var state: any UseCaseState { get set }
}

class UserAccountUseCase: UseCase {
    var lens: StateAccess {
        \.userAccount
    }
}

Probably diminishing returns on the complexity of the implementation... But it's one less line of boiler plate for type definition.