Optional Protocol Members

There's an idiom I've been playing with to encode conditional capabilities directly into protocol conformances, using uninhabited types. You can have a base protocol capture the type's conformance to a derived protocol in an associated type, which is either constrained to be equal to Self when the type conforms to the derived protocol, or defaults to an uninhabited type such as Never:


protocol Base {
  associatedtype AsDerived: Derived = Never

  func baseRequirement()
}

protocol Derived: Base where AsDerived == Self {
  func derivedRequirement()
}

extension Never: Derived {
  func baseRequirement() {}
  func derivedRequirement() {}
}

This allows for types that don't have an implementation of derivedRequirement not to provide one. Generic code that wants to require an implementation can do so by requiring T: Derived, but with only T: Base, we can check whether a type also conforms to Derived without paying for a full dynamic cast:

extension Base {
  var asDerived: Self.AsDerived? {
    if Self.self == AsDerived.self {
      return unsafeBitCast(self, to: AsDerived.self)
    } else {
      return nil
    }
  }
}

If you have many independently optional requirements, then I can see why you don't want to have a protocol for each. Within one protocol, you could still use a possibly-uninhabited associated type as a token to indicate whether each requirement is present:

protocol Base {
  func baseRequirement()

  associatedtype OptionToken = Never
  // If `OptionToken` is uninhabited, then this function can't be called
  func optionalRequirement(_: OptionToken)
}

// default implementation for the unreachable case
extension Base where OptionToken == Never { func optionalRequirement(_: Never) {} }

struct FulfillsOnlyBaseRequirement: Base {
  func baseRequirement() {}

  // doesn't need to implement optionalRequirement since it defaults to unreachable
}

struct FulfillsBothRequirements: Base {
  func baseRequirement() {}

  // implements the requirement for real, with a real inhabited type
  func optionalRequirement(_: Void) {}
}

And you could require or dynamically check where OptionToken == Void in code that could go either way to get access to the derived requirement. The associated type requirement also helps somewhat mitigate the "near miss" issue taylorswift raised above when using value-level Optional requirements, since the types won't line up if you expect the associated type to be inhabited but try to use the uninhabited default implementation. These approaches still require a lot of boilerplate in the language as it stands today, but could be looked at as a desugaring for a more fully-cooked optional requirement mechanism (or could possibly be used to implement a macro for an optional-like requirement).

10 Likes