Bug 3075 – Implement parameter contravariance

Status
REOPENED
Severity
enhancement
Priority
P4
Component
dmd
Product
D
Version
D2
Platform
Other
OS
All
Creation time
2009-06-17T10:20:15Z
Last change time
2024-12-13T17:50:23Z
Assigned to
No Owner
Creator
david
See also
https://issues.dlang.org/show_bug.cgi?id=16303, https://issues.dlang.org/show_bug.cgi?id=17349
Moved to GitHub: dmd#18032 →

Comments

Comment #0 by davidl — 2009-06-17T10:20:15Z
It's perfectly safe to call a const/invariant ensured version delegate or function when the caller only try to call a version without any const warranties.
Comment #1 by dfj1esp02 — 2009-07-02T06:47:55Z
Is it tango2 blocker? There is a tracker for tango2 blockers - bug 2267.
Comment #2 by bugzilla — 2009-07-02T19:27:02Z
A test case: void foo(void delegate(void[]) dg); void test() { void func(const(void)[] t) { } foo(&func); }
Comment #3 by bugzilla — 2009-07-03T00:51:57Z
const, yes, but not immutable as that would require that mutable be implicitly convertible to immutable, which cannot be.
Comment #4 by bugzilla — 2009-07-03T01:09:32Z
The actual rule for matching a delegate against a type is that the delegate is covariant with the type. The return type is checked for covariance, and things like a pure function is considered covariant with an impure one. This works exactly the same as overriding a virtual function with a covariant one. What you're asking for with the const parameters is contravariance. Contravariant parameters are a good idea until overloading is considered. If you have two functions, one with a const parameter and the other mutable, which one overrides the base virtual function? You could say overriding is based on a 'best match', but things are complex enough without throwing that into the mix. So, it is by design that the parameter lists must match exactly for covariant functions. I also think it is not a good idea to have one covariant matching rule for overriding, and another for implicit conversions. Better to have one covariant rule.
Comment #5 by davidl — 2009-07-03T07:56:21Z
(In reply to comment #3) > const, yes, but not immutable as that would require that mutable be implicitly > convertible to immutable, which cannot be. It's not implicit cast. But a special case for function/delegate parameters. Any qualifier for parameters or for the function is a restriction for the function. So you can still treat them as a special case of mutable version just they don't change anything even they are allowed to change the parameter. This is another testcase. void foo(void delegate(void[]) dg); void test() { void func(invariant(void)[] t) { } foo(&func); } test.func won't change paramter t. So this func as a delegate is perfectly safe to hold the prototype which has no restrictions on parameters. It's not casting invariant(void)[] to void[]. It's implicitly casting from "void delegate(invariant(void)[])" to "void delegate(void[])". Furthermore, it's pretty ugly to write: void foo(void delegate(void[]) dg); void test() { void func(invariant(void)[] t) { } foo(cast(void delegate(void[]))&func); } I don't think it is an invalid bug. But you can mark it as WONTFIX.
Comment #6 by bugzilla — 2009-07-03T10:55:57Z
It is an implicit cast. First, the invariant of course won't change the arguments. The problem is that the function that takes the invariant assumes that the data is invariant, i.e. never changes. Mutable data that is implicitly cast to invariant *can* change (because mutable aliases for the same data may exist). Therefore, a function taking an invariant is *not* compatible with a function taking a mutable. For the disposition, INVALID means that the compiler works as designed, which it does in this case. The WONTFIX still means the compiler is not working as designed, which is not the case here. Hence, the INVALID disposition is the correct one.
Comment #7 by davidl — 2009-07-03T16:47:11Z
(In reply to comment #3) > const, yes, but not immutable as that would require that mutable be implicitly > convertible to immutable, which cannot be. wait, but you are going to fix the const one?
Comment #8 by bugzilla — 2009-07-03T18:24:55Z
No, for the reasons already mentioned.
Comment #9 by schveiguy — 2009-07-08T10:36:32Z
I'll chime in a bit on this. What the original poster is looking for is not all-encompasing contravariance, it's a specific case. He is saying that a delegate that takes a const type is a subtype of the delegate that takes a mutable (or immutable) version of that type, similar to how mutable and immutable are "subtypes" of const. Similarly, it should be possible to implicitly convert a delegate that takes an object to a delegate that takes, for instance, a Stream, since Stream is implicitly convertable to object, and can viably be passed to that function. (In reply to comment #4) > The actual rule for matching a delegate against a type is that the delegate is > covariant with the type. The return type is checked for covariance, and things > like a pure function is considered covariant with an impure one. This works > exactly the same as overriding a virtual function with a covariant one. You're missing the issue here, a delegate today is not variant at all, you can't implicitly cast a delegate to another type in any case that I know of. > What you're asking for with the const parameters is contravariance. > Contravariant parameters are a good idea until overloading is considered. how do you overload delegates? As far as I know, a delegate is a pointer to a single overload, not an overload group (though the latter would sometimes be nice). > If > you have two functions, one with a const parameter and the other mutable, which > one overrides the base virtual function? You could say overriding is based on a > 'best match', but things are complex enough without throwing that into the mix. Yes, contravariance in other cases makes things difficult unless you specify the direction of the variance. For example in C#4, you can do contravariance and covariance with generics (check out bearophile's example: http://codepad.org/kQgbwAqJ) But in delegates, we are talking about casting a concretely defined type. Implicit casting to what should be valid should be allowed. In fact, I don't even think this works today: class C{ void foo(string s); void foo(const(char)[] s); } C c = new c; void delegate(const(char)[]) foo2 = &c.foo; // error, didn't select the right overload // or it's the other way around, can't remember Similarly, implicit covariant delegates should also be allowed. That is, class C { C foo(); } C c = new C; object delegate() foo2 = &c.foo; (In reply to comment #1) > Is it tango2 blocker? There is a tracker for tango2 blockers - bug 2267. Not that I'm aware of, I removed it as a blocker.
Comment #10 by bugzilla — 2009-07-09T02:57:14Z
Fixed dmd 2.031
Comment #11 by bugzilla — 2009-07-09T03:09:51Z
Oops, it's not fixed, I entered the wrong number. It's invalid. The request is for contravariance of parameter types, and this will not work in the general case because of function overloading and overriding (which rely on exact matching of parameter types). Making it work in a specific case is a kludge and will cause all kinds of problems in the future. It's an attractive idea, but it's been considered and rejected a couple of times now.
Comment #12 by schveiguy — 2009-07-09T08:23:21Z
you mean a kludge like COM interfaces? But I digress... Kludge is not the right word here, because what is implemented would fully satisfy what is desired. It may be worth while looking into how contravariance can work in the general case. At least in this case, it seems much less complex, and I'd say it's pretty well contained. That is, if this were implemented, I don't see how it would translate to the expectation that contravariance works in the general case (which has it's own problems). In fact, I'd say the current behavior is less consistent, since you can do implicit casting of parameters to match the function call, but the same call cannot be made by passing a delegate to a function that expects the base. For instance, imagine the following: struct S(T) { T[] data; applyAll(void delegate(T t) dg) { foreach(t; data) dg(t); } } class C {} class D : C {} class X { void foo(C c) {writefln(c);} void foo2(S!D s) { s.applyAll(&foo); // would just work. // current requirement (a kludge IMO) void wrapper(D d) { foo(d); } s.applyAll(&wrapper); } } I would petition to leave this open as an enhancement, maybe one of the newly found compiler gurus who can now modify dmd to add things can try making this work as a test.
Comment #13 by bugzilla — 2009-07-09T12:47:42Z
It's commonplace in language design to hack things up to work for the special cases, and then get stuck with an intractable legacy compatibility issue when it fails for the general case. C++ is full of that.
Comment #14 by dfj1esp02 — 2009-07-10T03:23:04Z
> The request is for contravariance of parameter types, and this > will not work in the general case because of function overloading and > overriding (which rely on exact matching of parameter types). Making it work in > a specific case is a kludge and will cause all kinds of problems in the future. Strange. I thought overloading and overriding work in different ways, e.g. foo(int) and foo(long) can be overloads, but not overrides.
Comment #15 by dfj1esp02 — 2009-07-10T03:25:51Z
And which step of overriding do you mean? When functions are checked if they can be overloads or when arguments types are matched to parameters types?
Comment #16 by dfj1esp02 — 2009-07-10T03:26:50Z
*fix step of overloading
Comment #17 by schveiguy — 2009-07-10T06:15:14Z
(In reply to comment #14) > Strange. I thought overloading and overriding work in different ways, e.g. > foo(int) and foo(long) can be overloads, but not overrides. That's exactly what Walter means. He doesn't want to have to support this kind of contravariance: class X {} class Y : X {} class C { void foo(Y y) {} } class D : C { override void foo(X x) {} } Should be technically valid, since calling the base function still works on the derived version (A Y is always an X). I think personally, this kind of contravariance provides little benefit, but being able to implicitly cast delegates (or function pointers) is much more useful because you use them as variables, which typically enjoy implicit conversion when passing as parameters. I don't share his opinion that doing this only for delegates is a hack.
Comment #18 by dfj1esp02 — 2009-07-13T01:42:00Z
Now I see how this can be connected with delegate casting, but what this has to do with overloading? So now contravariance is not supported at all for overloading? Only exact match? I don't see how this can conflict with function casting. Aren't the contextes different? In the context of overloading check for exact match, in the context of overload resolution check for implicit cast as for any other type. They hardly can clash, only if the compiler is written so that they clash. I suppose no algorithm for implicit function casting was written. It seems it only needs to be written, though I'm not familiar with compiler intrinsics, so I can lose some important details :)
Comment #19 by dfj1esp02 — 2009-07-13T01:45:06Z
*fix context of overriding v_V
Comment #20 by dfj1esp02 — 2009-07-13T01:49:49Z
What on earth am I writing?.. Now I see how function casting can be connected with overriding, but what this has to do with overloading? So now contravariance is not supported at all for overriding? Only exact match? I don't see how this can conflict with function casting. Aren't the contextes different? In the context of overriding check for exact match, in the context of overload resolution check for implicit cast as for any other type (this adds consistency imho). They hardly can clash, only if the compiler is written so that they clash. I suppose no algorithm for implicit function casting was written. It seems it only needs to be written, though I'm not familiar with compiler intrinsics, so I can lose some important details :)
Comment #21 by schveiguy — 2009-12-28T08:41:46Z
Just submitted bug 3656, which might require contravariance for delegates to make it work properly... Consider that a function foo: foo(void delegate() dg) { dg(); } should compile with the following: class A { void f1() const {} void f2() {} } void main() { A a = new A; foo(&a.f1); foo(&a.f2); } But if bug 3656 is resolved (and it must be to preserve const), then there is no current way to mark foo as not caring whether the delegate is const or not.
Comment #22 by yebblies — 2011-06-15T23:36:12Z
*** Issue 5471 has been marked as a duplicate of this issue. ***
Comment #23 by yebblies — 2013-11-09T07:05:52Z
*** Issue 7725 has been marked as a duplicate of this issue. ***
Comment #24 by yebblies — 2013-11-26T20:33:25Z
*** Issue 7708 has been marked as a duplicate of this issue. ***
Comment #25 by yebblies — 2013-11-26T22:59:35Z
*** Issue 7221 has been marked as a duplicate of this issue. ***
Comment #26 by yebblies — 2013-11-26T23:00:41Z
*** Issue 7219 has been marked as a duplicate of this issue. ***
Comment #27 by verylonglogin.reg — 2013-12-19T10:26:21Z
*** Issue 11773 has been marked as a duplicate of this issue. ***
Comment #28 by bugzilla — 2017-05-09T17:21:33Z
Reopened because contravariance really should work.
Comment #29 by razvan.nitu1305 — 2019-09-27T10:04:33Z
(In reply to Walter Bright from comment #2) > A test case: > > void foo(void delegate(void[]) dg); > > void test() > { > void func(const(void)[] t) > { > } > > foo(&func); > } This test case compiles today. Is this issue fixed?
Comment #30 by qs.il.paperinik — 2021-01-09T14:29:25Z
Overriding with contravariant parameter types can work. Because D has overloading and contravariant overriding is factually indistinguishable from overloading, an annotation must be used to clarify: It could be as simple as giving override an optional type list that signifies the types of the parameters of the method to be overridden: Attribute: ... override + override Parameters class X {} class Y : X {} class C { void foo(Y y) {} } class D : C { override(Y) void foo(X x) {} // overrides foo(Y y) } It's similar to the VisibilityAttribute package with a QualifiedIdentifier to signify the exact package it should be visible from. It's very specific and one could argue that overloading and forwarding solves the issue in almost all cases. class D : C { override void foo(Y y) { foo(cast(X) y); } void foo(X x) { ... } } There might be very specific circumstances where this is not a viable option.
Comment #31 by qs.il.paperinik — 2021-01-09T14:57:34Z
In the context of Argument-dependent Attributes (ADA) or Attributes for Higher-Order Functions (AfHOF) (pending DIPs), contravariant overloading is necessary. Imagine a proper sink-toString interface. The whole reason to have a sink in the first place instead of returning an allocated string is not doing the allocation. So, considering interface SinkToString { void toString(scope void delegate(char) @nogc) @nogc; } the following override should work in some way: class C : SinkToString { override(scope void delegate(char) @nogc) void toString(in void delegate(char) sink) @nogc const { ... } } The intention of the interface is: toString is @nogc and may require a @nogc delegate for this. An implementation of it may not require a @nogc delegate in its internal logic, but by the reasoning explained in AfHOF, calling C.toString with a @nogc argument will result in a @nogc execution. ADA is similar (as far as I can tell): one would replace the @nogc of C.toString by @nogc(*).
Comment #32 by qs.il.paperinik — 2024-07-03T11:36:00Z
Good: ```d void main() { void function( const(int)*) fp = null; void function( int *) gp = fp; void function(immutable(int)*) hp = fp; } ``` Error: ```d void main() { void function(ref const int ) fp = null; void function(ref int ) gp = fp; // error void function(ref immutable(int)) hp = fp; // error } ``` Same with s/function/delegate/g.
Comment #33 by robert.schadek — 2024-12-13T17:50:23Z
THIS ISSUE HAS BEEN MOVED TO GITHUB https://github.com/dlang/dmd/issues/18032 DO NOT COMMENT HERE ANYMORE, NOBODY WILL SEE IT, THIS ISSUE HAS BEEN MOVED TO GITHUB