Recursive protocol conformance requirement

I have a use case where having a feature similar to Equatable, Hashable, Codable (perhaps a few others?) would help: transitive protocol conformance requirement.

protocol P  /* : TransitiveConformanceRequired */  {}

extension Int: P {}

// This should work seamlessly
struct Foo: P { // ✅
    var x: Int
}

// This should fail because 'Double' does not conform to P
struct Bar: P { // ❌ Error: All fields must conform to P
    var x: Int
    var y: Double 
}

I assume this is not possible in current Swift. Is this something worth having in future Swift, or is it too niche of a use case?

Can you provide an example of the intended use case? What sort of generic code would you be able to write with such a conformance?

I suppose there might be some utility for macros (they can iterate over the properties and know that they can use certain requirements) or (shudder) reflection via Mirror, but it makes me wonder if there are better ways to express what you want here.

Nit: This is probably better called a "distributed" conformance—as in distributed over its members—not "transitive". A "transitive" conformance reads to me as A: B and B: C then A: C, which is already true in Swift.

2 Likes

Good catch on the name )

I want to enforce this restriction on view models, so they do not contain things like NSColor, NSImage, NSFont, etc, only simple "vetted" types like strings, integers, enums and collections of simple things.

Basically autosynthprotocol P?

This is interesting because it sort of straddles the line between a protocol and a "layout constraint", BitwiseCopyable being an example of the latter. (Yes, it's officially a protocol in the Swift sense, but there are things you can't do with it that you can do with regular protocols, like runtime dynamic checks.)

But the question still remains, is this just a business-logic-specific validation? What sort of generic algorithms would you implement with such a constraint on a type and its members? That's what we usually want to see to rationalize the existence of a protocol (or in this case, a new language feature that would provide a way to express distribution of a protocol).

Even in the case of a layout constraint like BitwiseCopyable, there are generic algorithms you can write where you know that you can load/store the raw bytes of a value safely. It's less clear what you would do generically with an arbitrary recursively applied protocol.

This feels like something that might better fit in the macro space, but for the lack of type information we have in those today. If we had static_asserts à la C++, I could envision a feature where you could use an extension macro to define the assertions you want on types that you don't own, and then other macro invocations could look at the members of your types and chain through to the assertions of their members' types, emitting a diagnostic if any of them don't meet the requirements. I think you might even be able to pull something like that off in the language today, except that the diagnostics would be really really bad, because you'd have to just emit something that generates members of a type and then attempts to poke at those members, so you'd just get something like "such and such has no member named "__this_is_the_thing_im_checking".

2 Likes

Yep, that's business-logic-specific validation, no generic algorithms involved. This is to enforce value resolution to be "as late as possible" to rule out a whole class of bugs.

I use a system of "tags" for images, fonts, colours, and so on. These tags are 1 or 2 byte enumeration constants (backed by String raw values for debuggability and codability convenience), and the corresponding values are obtained by resolving those tags against the current "theme".

If that resolution (lookup) is done at the very last step of the "food chain" (the view level), it makes theme switching very easy to implement. On the other hand, if the view model objects contain already resolved values, those models need to be re-resolved upon theme switching. This makes the system more complex and serves as a source of bugs, as the view models have to be reconstructed.

Another reason to use this indirection is that it makes it very easy for view models to conform to Equatable and Codable, which is useful for things like serialization, persistence, and unit testing. Last but not least, view models become smaller.

I realize Sendable does this, but every other example protocol's use of something like this is about whether or not the default implementation is appropriate; it is not about whether the protocol itself is appropriate. Sendable is a bit of an exception, in that in having no requirement members there's no way to provide a "manual" implementation. We chose to spell that @unchecked Sendable, but I suppose we could have required a static var sendableIPromise: Void {} which would normally be synthesized and only in the unchecked case would you provide your own. Then it'd be the same as the others.

As such, I'd expect a constraint on the semantic requirements of a type to be implemented using a conformance macro, or just a linter. I realize that macros have problems and that a customizable type-sensitive linter doesn't even exactly exist yet, but regardless I don't think attaching a new language feature to protocols would be the right way to go here.

4 Likes

This old discussion seems related.

1 Like

I could do these checks at runtime (Mirror API + recursively checking the fields), but for now it's a path of least resistance - requiring view models to be be Codable as a nudge to avoid using NSFont & co in those.