Preserve static type information in dictionary

Is it possible to preserve the static type information for the following API? Basically what I need is a key/value collection that provides a static type relationship between key and value but allows different key/value pair types.

extension View {
    
    func onIntent<D>(for data: D.Type, perform action: @escaping (D) -> Void) -> some View where D: Hashable {
        let intents = [
            ObjectIdentifier(data): { value in
                if let value = value as? D { // although the type information is statically known, it must be checked dynamically here
                    action(value)
                }
            } as (any Hashable) -> Void
        ]
        return environment(\.intents, intents)
    }
    
}

The API itself provides the type information for the call side, but the implementation does not preserve it statically. Do you have any ideas how to solve the problem without losing the static type?

Will this work?

extension View {
    func onIntent<D: Hashable>(for data: D.Type, perform action: @escaping (D) -> Void) -> some View {
        let intents = [
            ObjectIdentifier(data): { value in
                action(value)
            } as (D) -> Void
        ]
        return environment(\.intents, intents)
    }
}

The problem (and I wasn't clear enough in my question) is that intents need to be able to contain key/value pairs of different types, while preserving the type relationship between key and value. Your solution would force intents to have only values of type (D) -> Void, but what I need is a collection that allows key/value pairs that share a (generic) type, but allows different types for those key/value pairs.

At some level of the implementation, you'll need to use dynamic type information to store your heterogeneous values, but you can contain the problem by wrapping a dictionary (or some other keyed container) in a type that only publicly provides a generic subscript<T>(key: T) -> ValueType<T>. That way only that subscript needs to deal with the dynamic type assertion and the rest of the program can work with static types.

2 Likes

You can take some inspiration from https://github.com/apple/swift-service-context (which is used by swift-distributed-tracing) which is basically a type-safe dictionary, using types as keys.

1 Like

Great thanks! Seems to be exactly what I was looking for. :+1:

I have a question about the implementation of ServiceContext: what is the runtime cost of a force-cast? This is the only part that could be improved if full static knowledge of the dictionary were possible (which it is not).

You can benchmark it: there’s a great swift benchmark package plugin GitHub - ordo-one/package-benchmark: Swift benchmark runner with many performance metrics and great CI support

Force casting is relatively cheap though.

1 Like

I did a quick synthetic test and managed to get this line being executed in 7 (!) seconds:

any as! [[String: String]]

so it heavily depends on what any actually holds.

Great - thanks for the hint