Add userInfo protocols to standard library

As I’m sure everyone is well-aware, many types across various frameworks (and a few parts of the standard library) have an instance property called userInfo. This property is almost always a Dictionary, and almost always completely unused within the defining module. It exists purely as a customization point for consumers to use.

userInfo dictionaries are useful, but they currently exist more or less as an informal name-based convention. And if there is one thing I think we should all learn from Objective-C, it is that informal conventions are dangerously messy.

I propose the addition of two protocols to the standard library:

protocol UserInfoProviding {
  associatedtype UserInfoKey: Hashable
  associatedtype UserInfoValue
  var userInfo: [UserInfoKey: UserInfoValue] { get }
}

protocol HasUserInfo: UserInfoProviding {
  var userInfo: [UserInfoKey: UserInfoValue] { get set }
}

By adopting one of these protocols, a type formally agrees not to add, read, or delete any key-value pairs in the userInfo dictionary (within the defining module, anyway). In other words, downstream consumers are free to use it to pass whatever they want.

I welcome debate on the specific type constraints and names.

1 Like

Various Apple-owned types do have this property, but none [edit: only Encoder and Decoder] in the standard library.

Such a protocol does indicate that the conforming type has certain semantics, which is nice.

However, what generic algorithms are enabled by these protocols? You would have to be able to provide compelling answers to that question in order to justify the addition of these protocols. I don't work much with userInfo-containing types, so perhaps you could provide some real-world examples where this protocol would have enabled a useful generic solution.

7 Likes

As an example, Decoder currently looks like this:

public protocol Decoder {
  var codingPath: [CodingKey] { get }
  var userInfo: [CodingUserInfoKey: Any] { get }
  func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key>
  func unkeyedContainer() throws -> UnkeyedDecodingContainer
  func singleValueContainer() throws -> SingleValueDecodingContainer
}

One of those requirements doesn’t seem particularly relevant, does it? Adopting UserInfoProvider allows Decoder to simply define the types, making it much easier to understand.

This is particularly powerful with language features like conditional conformance and opaque types, not to mention proposed improvements like parameterized extensions and generalized existentials.

1 Like

I'm not sure I understand the motivation here for adding this.

Protocols in the standard library exist to unify algorithmic implementations. What algorithms will this be enabling and/or unifying?

2 Likes

Like Identifiable, this is mainly for formally adopting certain behavior. It can also be constrained further by inheriting protocols to allow for stronger typing.

1 Like

I'm not sure I understand. Please give some examples of how this protocol is particularly powerful. As @davedelong has reiterated, we need concrete examples of what algorithms are enabled.

For example, Identifiable supports diff algorithms and "other generic code to correlate snapshots of the state of an entity in order to identify changes to that state from one snapshot to another." Moreover, that proposal justifies why sinking the protocol into the standard library specifically is justifiable.

You are right that it's a good idea to look at the criteria by which Identifiable was judged, and to work on developing your pitch along those lines.

1 Like

It would be more useful with generalized existentials, but you could already use it for constraints like the following:

func example<T: UserInfoProviding>(_ input: T) where T.UserInfoKey == CodingUserInfoKey {
  let value = input.userInfo[.staticKey] as? Int
}

That would work with any conforming type with the right key. Add other protocol requirements, and that can become quite useful.

Alternatively:

let predefinedKey = "Example"
func example<T: UserInfoProviding>(_ input: T) {
  if let predefinedKey = predefinedKey as? T.UserInfoKey {
    let test = input.userInfo[predefinedKey] as? Int
  }
}

Another example:

func example<T: UserInfoProviding>(_ input: T) where T.UserInfoKey: RawRepresentable, T.UserInfoKey.RawValue: ExpressibleByStringLiteral {
  if let testKey = T.UserInfoKey(rawValue: "test") {
    let test = input.userInfo[testKey] as? Int
  }
}

How many userInfo dictionaries would that work with? Not all of them, but certainly most of them. Even if they each have their own type (like CodingUserInfoKey) for some reason.

Types I can think of that have userInfo dictionaries are NSError and Notification. Are there others? Are you proposing that all the existing types that have userInfo dictionaries be made to adopt these protocols?

The thing about userInfo is that it's really [String: Any] or even [Any: Any]. I guess narrowing down the types, like your Decoder example could be useful. You're going to have to add UserInfoValue to all the basic types I guess.

1 Like

No, type inference would take care of that. All types need to do is conform, which is why it is important to have in the standard library à la Identifiable.

At any rate, if your key cannot be represented in a type’s UserInfoKey, you can be pretty damn sure there’s no value associated with it. That in itself is useful.

With generalized existentials, this becomes substantially more useful:

func example<T>(_ input: T) {
  /// syntax from Generics Manifesto
  let test = (input as? Any<UserInfoProviding where .UserInfoKey: ExpressibleByStringLiteral>)?.userInfo[testKey] as? Int
}

That could be done without constraining the input at all.

I don't understand what your example is trying to illustrate here. What useful task is accomplished by extracting an arbitrary user info key from values of arbitrary type?

2 Likes

Examples 1 and 2 are just examples of how to use a protocol in Swift. I’m not seeing any actual functionality that is enabled by this.

Moreover, there is a heavy of use of generic constraints, without any explanation as to how they can be satisfied. For example:

When I construct my object (say, an NSNotification or NSError or something of that sort), how am I supposed to know what UserInfoKey type your processing object expects?

The name “user info” is a hint that the dictionary contains additional library or application-specific data. Your design would force every type with a user-info dictionary to also take library or application-specific generic parameters (and thus to make that additional data part of its own type - for example, NSNotification<UIKeyboardEventUserInfoKeys>). That defeats the point of the user info dictionary, and Is better modelled as a stores property.

This is shown again in the next example:

You can’t just go and instantiate some unknown type with a raw value you pulled out of a hat.

What should become clear from this is that user-info dictionaries are intentionally loosely-typed. A String->Any dictionary really is the best representation for them, and this protocol doesn’t really serve much purpose.

5 Likes

If you think the protocol should require [String: Any], fine.

Also, what “processing object”? This for putting something in a type and getting it out later. Potentially one of multiple types, especially if you are using different frameworks depending on availability.

The specific impetus for pitching these protocols is the proposal to move Combine’s TopLevelDecoder and TopLevelEncoder protocols to the standard library.

The protocols introduced by Combine don’t require userInfo dictionaries, even though they are required by Decoder and Encoder. Why? Because having a userInfo dictionary isn’t actually relevant to serialization. Any type might arguably benefit from having such a property, but it would be ridiculous to add that requirement to all of them.

It is widely agreed that the most “Swifty” design patterns make heavy use of composition, especially protocol composition: good protocols require only what is directly relevant to their intended meaning, and inherit from other protocols as necessary.

If Decoder requires userInfo to be a concrete type, then it becomes mutually exclusive with other protocols that require a different type. While that is now set in stone, it would be better for future protocols to avoid such requirements.

That’s the point of my examples: keeping protocols separate allows for more flexibility, especially when those writing the protocols are not the same people implementing them. And in the case of userInfo, that’s more or less the entire point.

Actually it is. See the original Codable proposal for the rationale:

CodingUserInfoKey is a bit of a useless type, though. It just wraps a string - I guess we probably should have just used String directly. It has already been acknowledged as a mistake: Why is CodingUserInfoKey's rawValue initializer failable?

1 Like

Because Combine’s TopLevelDecoder doesn’t require userInfo, and no protocol exists that only requires userInfo, it is more or less impossible to write a generic method that interacts with the userInfo dictionary of a type conforming to TopLevelDecoder. This is in spite of the fact that Decoder requires it, so every TopLevelDecoder must at some point supply such a dictionary.

I ran into this problem while trying to write a publisher that operated on an NSManagedObjectContext stored in a TopLevelDecoder.

import Combine

import class CoreData.NSManagedObjectContext

extension Publisher {
  /// Decodes the output from upstream using a specified decoder.
  ///
  /// If `decoder` has an `NSManagedObjectContext` inside a `userInfo` dictionary with a `CodingUserInfoKey` of `context`, decoding will be performed on that context's private queue.
  func performDecode<Item: Decodable, Coder: TopLevelDecoder>(type: Item.Type, decoder: Coder)
    -> Publishers.FlatMap<Future<Item, Error>, Publishers.MapError<Self, Error>>
  where Self.Output == Coder.Input {
    mapError { $0 as Error }.flatMap { (element: Coder.Input) in
      Future { promise in
        let decode = { promise(.init { try decoder.decode(type, from: element) }) }
        guard
          let context = Mirror(reflecting: decoder).children.first(where: { $0.label == "userInfo" }
          ).map({ $0.value as? [CodingUserInfoKey: Any] })?.map({ userInfo in
            CodingUserInfoKey(rawValue: "context").flatMap { userInfo[$0] }
          }) as? NSManagedObjectContext
        else { return decode() }
        return context.perform(decode)
      }
    }
  }
}

Needless to say, this is not ideal. My initial solution was to ask that userInfo be required by TopLevelDecoder, since it is required by Decoder. However, I’ve since concluded that the Combine team had the right idea when they excluded it: why require something you don’t need?

Let’s see how I could write this if the protocols I am proposing existed:

import Combine

import class CoreData.NSManagedObjectContext

extension Publisher {
  /// Decodes the output from upstream using a specified decoder.
  ///
  /// If `decoder` has an `NSManagedObjectContext` inside its `userInfo` dictionary with a key of `context`, decoding will be performed on that context's private queue.
  func performDecode<Item: Decodable, Coder: TopLevelDecoder & UserInfoProviding>(
    type: Item.Type, decoder: Coder
  ) -> Publishers.FlatMap<Future<Item, Error>, Publishers.MapError<Self, Error>>
  where
    Self.Output == Coder.Input, Coder.UserInfoKey: RawRepresentable,
    Coder.UserInfoKey.RawValue: ExpressibleByStringLiteral
  {
    mapError { $0 as Error }.flatMap { (element: Coder.Input) in
      Future { promise in
        let decode = { promise(.init { try decoder.decode(type, from: element) }) }
        if let context = Coder.UserInfoKey(rawValue: "context").flatMap({
          decoder.userInfo[$0] as? NSManagedObjectContext
        }) {
          return context.perform(decode)
        } else {
          return decode()
        }
      }
    }
  }
}

That is much more resilient, and allows callees as much flexibility as possible. I don’t think I can provide a more concrete example than that.

The original proposal explains that exposing a userInfo dictionary may be useful, but by including it in the protocol itself they are requiring it.

In my opinion, it would have been better to move that requirement into a different protocol, so implementations could conditionally access userInfo dictionaries if they existed. There’s no point in guaranteeing that every Decoder and Encoder has a userInfo property of a certain type: there’s no guarantee that it would have what you are trying to access anyway.

Of course, that wouldn’t be possible right now: such conditional access would require generalized existentials, unless the userInfo protocols only used concrete types.

Sure - because some decoding algorithms require it, and if it wasn’t required, we’d need more protocols for model types to declare things like DecodableButRequiresUserInfo; meaning you couldn’t decode it with a regular Decoder but would instead require a DecoderThatProvidesUserInfo. That’s just horrible - regardless of whether it’s a protocol composition or a refinement.

Yeah I would agree that it should be on TopLevelDecoder for the same reason that it’s on the standard Decoder.

Terms of Service

Privacy Policy

Cookie Policy