contravariant argument types: wanna?

Jeremie Pelletier jeremiep at gmail.com
Tue Sep 22 19:02:59 PDT 2009


Steven Schveighoffer wrote:
> On Tue, 22 Sep 2009 21:25:44 -0400, Jeremie Pelletier 
> <jeremiep at gmail.com> wrote:
> 
>> Steven Schveighoffer wrote:
>>> On Tue, 22 Sep 2009 20:49:59 -0400, Jeremie Pelletier 
>>> <jeremiep at gmail.com> wrote:
>>>
>>>> Andrei Alexandrescu wrote:
>>>>> Hello,
>>>>>   Today, overriding functions have covariant return types:
>>>>>  class A {
>>>>>     A clone();
>>>>> }
>>>>>  class B : A {
>>>>>     B clone(); // fine, overrides A.clone
>>>>> }
>>>>>  That is entirely principled and cool. Now the entire story is that 
>>>>> overriding function may have not only covariant return types, but 
>>>>> also contravariant argument types:
>>>>>  class A {
>>>>>     A fun(B);
>>>>> }
>>>>>  class B : A {
>>>>>     B fun(A); // fine (in theory), overrides A.fun
>>>>> }
>>>>>  Today D does not support contravariant arguments, but Walter told 
>>>>> me once he'd be quite willing to implement them. It is definitely 
>>>>> the right thing to do, but Walter would want to see a compelling 
>>>>> example before getting to work.
>>>>>  Is there interest in contravariant argument types? If so, do you 
>>>>> know of a killer example?
>>>>>   Thanks,
>>>>>  Andrei
>>>>
>>>> I can't think of an use for contravariant parameters, since a B is 
>>>> guaranteed to always be a A, I don't see the point of being able to 
>>>> declare fun(A).
>>>>
>>>> However, I would love to hear about covariant parameters, it would 
>>>> be most useful for interface implementations:
>>>>
>>>> interface A {
>>>>     A fun(A);
>>>> }
>>>> class B : A {
>>>>     B fun(B);
>>>> }
>>>> class C : A {
>>>>     C fun(C);
>>>> }
>>>>
>>>> Currently you need some pretty boring boilerplate code, which isn't 
>>>> complicated but gets repetitive when you have hundreds of such cases:
>>>>
>>>> class B : A {
>>>>     B fun(A) {
>>>>         if(B b = cast(B)b) // do stuff
>>>>         else throw Error("Invalid object type");
>>>>     }
>>>> }
>>>  I don't know if this is possible:
>>>  A a = new C;
>>>  a.fun(new A); // oops, you just passed an A into a function which 
>>> requires a C!
>>>  Are you suggesting that the compiler insert dynamic cast checks 
>>> everywhere?  Cause that seems like a lot of overhead...
>>>  -Steve
>>
>> Not everywhere, only where it detects covariant/contravariant 
>> overrides or implementations. In these cases you would already use 
>> explicit dynamic casts so the compiler generated code would just lower 
>> the required boilerplate.
> 
> I don't think it's worth the trouble.  Dynamic casts are not as cheap as 
> implicit casts.  Contravariance on parameters can be statically proven 
> by the compiler.  I agree Andrei's example isn't that compelling (to be 
> fair, he did ask if anyone had a good example, indicating his wasn't), 
> but there are other examples that are more compelling (see the bug 
> report I referenced in a separate sub-thread).
> 
> For instance, if you only ever use class C, and never instantiate an A 
> or B instance, you still pay the dynamic cast penalty every time you 
> call fun!  It doesn't sound to me like a good design.
> 
> I suppose you probably have run into this before, perhaps a real example 
> would be more convincing.
> 
> -Steve

Yeah most of my display interfaces would make use of covariant 
arguments, I use main abstract factory for the entire package, and the 
objects it creates contain factory methods themselves. I plan to have 
implementations for all of win32, gdk, x11, quartz, cairo, pango, d2d, 
dwrite, gl, gl3 and finally d3d7 up to d3d11. Most of the client code 
will therefore see only the interfaces in order to maintain portability, 
and to allow different implementations to live in the same executable 
(for example win32/gl/cairo/pango for up to vista or 
win32/d3d/d2d/dwrite if on win7 and up).

Here is a watered down version of a few interfaces I use, which are used 
by client code:

interface IDrawable {}
interface IWindow : IDrawable {} // onscreen drawable
interface ISurface : IDrawable {} // offscreen drawable
interface IDisplayContext {} // base of 2d-3d contextes
interface IRenderContext {} // 3d context
interface IWindowRenderContext {} // specialized onscreen 3d context
interface IRenderer {
	IWindowRenderContext CreateRenderContext(IWindow);
	ISurfaceRenderContext CreateRenderContext(ISurface);
}

And some of their current implementation, which are all used within the 
package:

abstract class Win32Drawable : IDrawable {}
final class Win32Window : Win32Drawable, IWindow {}
final class Win32Surface : Win32Drawable, IWindow {}

final class GLRenderer : IRenderer {
	GLWindowRenderContext CreateRenderContext(IWindow window) {
		if(auto win32Window = cast(Win32Window)window)
			return new GLWindowRenderContext(win32Window);
		else throw new Error();
	}
	GLSurfaceRenderContext CreateRenderContext(ISurface surface) {
		if(auto win32Surface = cast(Win32Surface)surface)
			return new GLSurfaceRenderContext(win32Surface);
		else throw new Error();
	}
}

abstract class GLRenderContext : IRenderContext {}
final class GLWindowRenderContext : GLRenderContext, IWindowRenderContext {
	this(Win32Window) {}
}
final class GLSurfaceRenderContext : GLRenderContext, 
ISurfaceRenderContext {
	this(Win32Surface) {}
}

I have over a hundred of such methods doing dynamic casts across all the 
different implementations like these twos in this package alone, a 
display interface is quite a large beast.

Of course if you can suggest a better way of doing methods expecting a 
specific implementation of an object, while still allowing client code 
to call them with the interface pointer, I'd be glad to implement it :)

Jeremie



More information about the Digitalmars-d mailing list