Introspection of KeyPaths

The trouble is that not every subscript takes just an Int. Subscripts can take multiple parameters, they can have argument labels, and the parameter types can be anything, possibly not conforming to CustomStringConvertible.

4 Likes

The keyPath to string conversion in core isn't extremely helpful, as you either have to provide the mapping yourself (which defeats the purpose), or depend on the object in question implementing Decodable as well. It's kind of a hack

This is a use case for which I have been searching around for a way to convert KeyPaths to their path string components.

There are two main features described below:
1: Piecemeal Construction:
With a typical initializer (T.init), all parameters must be known at the call site. In a situation where the parameters are determined through a series of steps, the set of parameters must be passed through these steps in some user defined structure (P) that is specific to the type being initialized. At the end, the parameter set is unpacked and supplied, again via some user specified mapping (M), to the initializer.

P | foo | bar | baz | M | T.init

In my case, I want to generalize that parameter-set structure such that it can be used for any type.
Furthermore, I want to rely on the existing Codable implementation (especially the synthesized implementations) to facilitate the mapping (M) between the parameter set (P) and the object.

2: Transformation between similar types
Given two distinct types A and B, transform A into B provided that the members of B are a subset of the members of A.

Required Infrastructure

import AnyCodable // https://github.com/Flight-School/AnyCodable.git

struct ConstructionModel<Model> {
    var data: [String: AnyCodable] = [:]

    subscript<Value>(_ keyPath: KeyPath<Model, Value>) -> Value? {
        get { data[String(describing: keyPath)]?.value as? Value }
        set { data[String(describing: keyPath)] = AnyCodable(newValue) }
    }

    func get<Value>(_ key: String, as: Value.Type) -> Value? {
        data[key]?.value as? Value
    }
}

Example: Piecemeal Construction

struct BigThing: Codable {
    var name: String
    var age: Int
    var quest: String
}

var model = ConstructionModel<BigThing>()
// Elsewhere...
model[\.name] = "Arthur"
// Elsewhere...
model[\.age] = 42
// Elsewhere...
model[\.quest] = "The Holy Grail"

Assuming the ability to recover string path names from a KeyPath, the following would then be possible:

try BigThing(from: CustomDecoder(model: model))

The CustomDecoder used would simply leverage the String + Type provided for each field during decoding and use those parameters to look up the appropriate value with ConstructionModel.get(_:as:).

Example: Transform between similar types

struct ThingOne: Codable {
    var name: String
}

struct ThingTwo: Codable {
    var name: String
}

let transformer = Transformer()
ThingOne(name: "One").encode(to: transformer)
try ThingTwo(from: transformer)

Here the Transformer conforms to both Encoder and Decoder such that it consumes the input object into an internal ConstructionModel (as shown above) and then references that ConstructionModel to create the specified output (if possible).

Compound Use Case
Firstly, I realize that the type transformation feature is possible using only Codable. However, I mention it here as context for a bit more detail on another part of my use case:

  • Given two types A and B, map A.x to B.y (generically) provided that A.x and B.y are of the same type.

In practice the described function signature looks something like this:

func map<A, B>(_ lhs: KeyPath<A, V>, to rhs: KeyPath<B, V>)

The important part for me here is the compile time guarantees of the KeyPaths. I know that I am mapping two fields that could plausibly be mapped to one another.

Final Notes
Anyway, there's a bunch of other code I have built up around what I've described here. The purpose for that code being validation and construction using a defined "shape".
That is to say...

  • Given a definition of a "shape" S, attempt to transform an instance of type T into S provided that T passes a set of validation constraints defined by S. Also, mapping any important pieces of T onto S.

And the opposite direction:

  • Given an instance of a "shape" S, attempt to construct an instance of type T such that T can be again transformed into S using the aforementioned "shape" definition.
1 Like

I actually was able to fully parse the underlying binary data structure of a KeyPath once using the ABI reference, and discern all the components.

This data is available in an optimised binary, so I don't see why there isn't a feature making its structure available at runtime.

3 Likes

Linking these two discussions as they seem relevant:

1 Like

I might make the code I mentioned above available if I can find it.
To be clear, you can fully introspect key paths at runtime by parsing the ABI using unsafe memory methods. Since all the data seems to be there at runtime, I'm not entirely sure why this hasn't become a feature yet, given how useful it is.

1 Like

Do you know how to check if the two key paths are related?