This is describing a value being used in both covariant and contravariant positions.
To provide a value x : T is covariant. To consume a value (x : T) -> R is contravariant. Covariant uses are unchanged then T becomes S, a looser, less informative type. Contravariant uses are unchanged when T becomes U, a more specific type.
x : T
(x : T) -> R
The reason this doesn’t happen with with functions definitions—which are interfaces themselves—is that every covariant use of that particular interface is controlled by you. Others always use the function contravariantly (they access your single instance and “consume” it via application). This means that you can always make the function more specific by either returning a more detailed output or asking for less of the inputs. The pain of covariant users of this interface is born entirely by you.
Really there’s no getting around this, but it’s worth thinking about the more general situation and learning the terminology for it.
I know the terms “covariant” and “contravariant”. Do…do you feel like the blog post would have been clearer or more useful if I dropped those in? Because I am pretty sure that for my target audience, explaining the general situation would have been a distraction.
Sorry, I think it’s totally fair to just state what you did. It calls to attention a common aspect of the problem.
I think why I want to invoke the more general terminology and concept is that there’s an implied recommendation—think super carefully about your protocols—with not as much of an offering of the mechanism to do it. The mechanism of “only use protocols you’re willing to support exactly as is forever” is very difficult to use.
The larger discussion of subsumption is super difficult and probably not something to add in to a post like this, but it’d be great to see it elsewhere.
It would be great to see a follow-up that discusses interface design. In Twisted which interfaces have worked out well, and why? (E.g., I think that IAgent is great while ISevice and IServiceCollection haven’t been so successful).
Wow do I feel personally called out by this! :)
(Maybe you don’t want a follow-up written by the person who authored IService and IServiceCollection)
I know you did. :) I thought you might have some perspective. I apologize for not making that context clear.
To clarify, I mean that I view IAgent as successful because there are many implementations, while folks tend to implement IService and IServiceCollection by subclassing Service or MultiService respectively. So why is that? Is it good/bad/whatever? What does that say about the design of the interface? Composition vs. inheritance etc.
IProtocol is another interesting case. It’s pretty tiny, but most protocols subclass twisted.internet.protocol.Protocol instead of implementing it directly, I guess for the default method implementations and ILoggingContext. IProtocol is also interesting because due to its complicated relationship with ITransport and the various extension interfaces it is difficult to wrap. Again, a contrast but IAgent, but could you do better? (Is tubes a better design?)