www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - polymorphism: overloading vs. overriding

reply Markus Kranz <Markus_member pathlink.com> writes:
Following the rule of last surprise: wouldn't it be desirable if the output of
the following program would be:

Bar.doIt()
Bar.doIt(Bar)

The actual output

Bar.doIt()
Foo.doIt(Foo)

at least for me is a little bit surprising since the best matching function
seems to be Bar.doIt(Bar).
As overriding with covariant return types is supported it seems a bit unnatural
to me that overriding with covariant arguments does not work.

Instead of doing overload resolution before virtual function resolution (what
seems to be done now) an implementation of 'covariant arguments' theoretically
could be as simple as reversing the order of resolution.

What do you think?

Regards,
Markus

--
import std.stdio;

class Foo {
void doIt() { writefln("Foo.doIt()"); }
void doIt(Foo that) { writefln("Foo.doIt(Foo)"); }
}

class Bar : Foo {
//overrides Foo.doIt()
void doIt() { writefln("Bar.doIt()"); }

//overloads (inherited) Foo.doIt(Foo)
void doIt(Bar that) { writefln("Bar.doIt(Bar)"); }
}

int main() {
Foo foo = new Bar();

foo.doIt();
foo.doIt(foo);
}
May 07 2006
next sibling parent reply Mike Parker <aldacron71 yahoo.com> writes:
Markus Kranz wrote:
 Following the rule of last surprise: wouldn't it be desirable if the output of
 the following program would be:
 
 Bar.doIt()
 Bar.doIt(Bar)
 
 The actual output
 
 Bar.doIt()
 Foo.doIt(Foo)
 
 at least for me is a little bit surprising since the best matching function
 seems to be Bar.doIt(Bar).
 As overriding with covariant return types is supported it seems a bit unnatural
 to me that overriding with covariant arguments does not work.
 
 Instead of doing overload resolution before virtual function resolution (what
 seems to be done now) an implementation of 'covariant arguments' theoretically
 could be as simple as reversing the order of resolution.
 
 What do you think?

This line is the key: Foo foo = new Bar(); foo is an instance of Foo, not an instance of Bar, so the appropriate method (Foo.doIt(Foo)) is being called. Java and C++ both have the same behavior. What you are suggesting is unintuitive. Not all Foos are Bars, but all Bars are Foos. If you want Bar.doIt to be called when you have a an instance of Foo, you have to downcast.
May 07 2006
next sibling parent markus_kranz gmx.net writes:
In article <e3ki2e$2o7a$1 digitaldaemon.com>, Mike Parker says...
Markus Kranz wrote:
 [The desired output]
 
 Bar.doIt()
 Bar.doIt(Bar)
 
 The actual output
 
 Bar.doIt()
 Foo.doIt(Foo)
 
 [snip]

This line is the key: Foo foo = new Bar(); foo is an instance of Foo, not an instance of Bar, so the appropriate method (Foo.doIt(Foo)) is being called.

Sorry, but what I read is that foo is not just a Foo but even a Bar?!
Java and C++ both have the same behavior.

I can confirm this at least for Java. But someone told me D was not Java... ;-)
What you are suggesting is unintuitive. Not all Foos are Bars, 
but all Bars are Foos. If you want Bar.doIt to be called when you have a 
an instance of Foo, you have to downcast.

To my knowledge in D as in Java all member functions are virtual. That is why foo.doIt() actually calls the version in Bar - without any explicit cast. Why do you think the actual behaviour is intuitive while my proposal isn't? I would have understood if Foo.doIt() Foo.doIt(Foo) got your vote. Regards Markus
May 07 2006
prev sibling parent reply Bruno Medeiros <brunodomedeirosATgmail SPAM.com> writes:
Mike Parker wrote:
 Markus Kranz wrote:
 Following the rule of last surprise: wouldn't it be desirable if the 
 output of
 the following program would be:

 Bar.doIt()
 Bar.doIt(Bar)

 The actual output

 Bar.doIt()
 Foo.doIt(Foo)

 at least for me is a little bit surprising since the best matching 
 function
 seems to be Bar.doIt(Bar).
 As overriding with covariant return types is supported it seems a bit 
 unnatural
 to me that overriding with covariant arguments does not work.

 Instead of doing overload resolution before virtual function 
 resolution (what
 seems to be done now) an implementation of 'covariant arguments' 
 theoretically
 could be as simple as reversing the order of resolution.

 What do you think?

This line is the key: Foo foo = new Bar(); foo is an instance of Foo, not an instance of Bar,

No, that is wrong, foo *is* an instance of Bar. (yet, it's type contract is only that it is a Foo) -- Bruno Medeiros - CS/E student http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
May 07 2006
parent Mike Parker <aldacron71 yahoo.com> writes:
Bruno Medeiros wrote:
 Mike Parker wrote:
 This line is the key:

 Foo foo = new Bar();

 foo is an instance of Foo, not an instance of Bar, 

No, that is wrong, foo *is* an instance of Bar. (yet, it's type contract is only that it is a Foo)

Yes, yes. Underneath the foo is an instance of Bar. But because, as you say, the type contract is that of Foo then you are effectively dealing with a Foo and not a Bar. It should only be treated as a Bar when you downcast.
May 07 2006
prev sibling next sibling parent reply Bruno Medeiros <brunodomedeirosATgmail SPAM.com> writes:
Markus Kranz wrote:
 Following the rule of last surprise: wouldn't it be desirable if the output of
 the following program would be:
 
 Bar.doIt()
 Bar.doIt(Bar)
 
 The actual output
 
 Bar.doIt()
 Foo.doIt(Foo)
 
 at least for me is a little bit surprising since the best matching function
 seems to be Bar.doIt(Bar).
 As overriding with covariant return types is supported it seems a bit unnatural
 to me that overriding with covariant arguments does not work.
 
 Instead of doing overload resolution before virtual function resolution (what
 seems to be done now) an implementation of 'covariant arguments' theoretically
 could be as simple as reversing the order of resolution.
 
 What do you think?
 
 Regards,
 Markus
 
 --
 import std.stdio;
 
 class Foo {
 void doIt() { writefln("Foo.doIt()"); }
 void doIt(Foo that) { writefln("Foo.doIt(Foo)"); }
 }
 
 class Bar : Foo {
 //overrides Foo.doIt()
 void doIt() { writefln("Bar.doIt()"); }
 
 //overloads (inherited) Foo.doIt(Foo)
 void doIt(Bar that) { writefln("Bar.doIt(Bar)"); }
 }
 
 int main() {
 Foo foo = new Bar();
 
 foo.doIt();
 foo.doIt(foo);
 }
 
 

No, it's not unnatural. For a function to override another, the return type must be covariant, but the parameters must be contravariant. It's quite natural to be this way, in fact, it's the only correct way (other than invariant parameters). The reason why it is so, is because for a function to override another, it must handle all (or more) inputs than the previous function. Consider, in your example, the following addition class Baz : Foo { } Foo.doIt(Foo that) can handle an argument of type Baz, but Bar.doIt(Bar that) cannot handle such an argument. Thus Bar.doIt doesn't override Foo.doIt -- Bruno Medeiros - CS/E student http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
May 07 2006
next sibling parent reply markus_kranz gmx.net writes:
In article <e3kjf0$2q09$3 digitaldaemon.com>, Bruno Medeiros says...
Markus Kranz wrote:
 [snip]
 --
 import std.stdio;
 
 class Foo {
 void doIt() { writefln("Foo.doIt()"); }
 void doIt(Foo that) { writefln("Foo.doIt(Foo)"); }
 }
 
 class Bar : Foo {
 //overrides Foo.doIt()
 void doIt() { writefln("Bar.doIt()"); }
 
 //overloads (inherited) Foo.doIt(Foo)
 void doIt(Bar that) { writefln("Bar.doIt(Bar)"); }
 }
 
 int main() {
 Foo foo = new Bar();
 
 foo.doIt();
 foo.doIt(foo);
 }

No, it's not unnatural. For a function to override another, the return type must be covariant, but the parameters must be contravariant. It's quite natural to be this way, in fact, it's the only correct way (other than invariant parameters). The reason why it is so, is because for a function to override another, it must handle all (or more) inputs than the previous function. Consider, in your example, the following addition class Baz : Foo { } Foo.doIt(Foo that) can handle an argument of type Baz, but Bar.doIt(Bar that) cannot handle such an argument. Thus Bar.doIt doesn't override Foo.doIt

I see Bar.doIt(Bar) does not override Foo.doIt(Foo) given your definition of overriding. But given your szenario: Bar bar = new Bar(); Baz baz = new Baz(); bar.doIt(baz); AFAIS should work perfectly (cause baz is a Baz and so a Foo and Bar inherits from Foo the function doIt(Foo))... ..and does so in Java (but that's no argument <g>) ..but fails in D <hmpf> test.d(23): function test.Bar.doIt () does not match argument types (Baz) test.d(23): cannot implicitly convert expression (baz) of type test.Baz to test.Bar Of course what dmd says is true, but why can't it implicitly convert expression (baz) of type test.Baz to test.Foo? Does 'overriding' in D does not mean the same as in Java? Regards, Markus
May 07 2006
parent reply Bruno Medeiros <brunodomedeirosATgmail SPAM.com> writes:
markus_kranz gmx.net wrote:
 In article <e3kjf0$2q09$3 digitaldaemon.com>, Bruno Medeiros says...
 Markus Kranz wrote:
 [snip]
 --
 import std.stdio;

 class Foo {
 void doIt() { writefln("Foo.doIt()"); }
 void doIt(Foo that) { writefln("Foo.doIt(Foo)"); }
 }

 class Bar : Foo {
 //overrides Foo.doIt()
 void doIt() { writefln("Bar.doIt()"); }

 //overloads (inherited) Foo.doIt(Foo)
 void doIt(Bar that) { writefln("Bar.doIt(Bar)"); }
 }

 int main() {
 Foo foo = new Bar();

 foo.doIt();
 foo.doIt(foo);
 }

type must be covariant, but the parameters must be contravariant. It's quite natural to be this way, in fact, it's the only correct way (other than invariant parameters). The reason why it is so, is because for a function to override another, it must handle all (or more) inputs than the previous function. Consider, in your example, the following addition class Baz : Foo { } Foo.doIt(Foo that) can handle an argument of type Baz, but Bar.doIt(Bar that) cannot handle such an argument. Thus Bar.doIt doesn't override Foo.doIt

I see Bar.doIt(Bar) does not override Foo.doIt(Foo) given your definition of overriding. But given your szenario: Bar bar = new Bar(); Baz baz = new Baz(); bar.doIt(baz); AFAIS should work perfectly (cause baz is a Baz and so a Foo and Bar inherits from Foo the function doIt(Foo))... ...and does so in Java (but that's no argument <g>) ...but fails in D <hmpf> test.d(23): function test.Bar.doIt () does not match argument types (Baz) test.d(23): cannot implicitly convert expression (baz) of type test.Baz to test.Bar

In http://www.digitalmars.com/d/function.html , "Function Inheritance and Overriding" it is said: "However, when doing overload resolution, the functions in the base class are not considered:" If that is the ideal behavior, well, that I'm not sure...
 Of course what dmd says is true, but why can't it implicitly convert expression
 (baz) of type test.Baz to test.Foo?
 

Even if you put a cast(Foo) on the argument, it won't compile, because it only considers the overloads (Bar.doIt) of the child class (Bar) Once again, if that is the ideal behavior, I'm not sure...
 Does 'overriding' in D does not mean the same as in Java?
 

-- Bruno Medeiros - CS/E student http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
May 07 2006
parent Markus Kranz <markus_kranz gmx.net> writes:
In article <e3knre$2vp1$1 digitaldaemon.com>, Bruno Medeiros says...
In http://www.digitalmars.com/d/function.html , "Function Inheritance 
and Overriding" it is said:
"However, when doing overload resolution, the functions in the base 
class are not considered:"
If that is the ideal behavior, well, that I'm not sure...

 Of course what dmd says is true, but why can't it implicitly convert expression
 (baz) of type test.Baz to test.Foo?

Even if you put a cast(Foo) on the argument, it won't compile, because it only considers the overloads (Bar.doIt) of the child class (Bar) Once again, if that is the ideal behavior, I'm not sure...

Okay. At least the considered alias construction works.
 Does 'overriding' in D does not mean the same as in Java?
 


That's soothing. Regards Markus
May 07 2006
prev sibling parent reply Bruno Medeiros <brunodomedeirosATgmail SPAM.com> writes:
Bruno Medeiros wrote:
 
 No, it's not unnatural. For a function to override another, the return 
 type must be covariant, but the parameters must be contravariant.
 It's quite natural to be this way, in fact, it's the only correct way 
 (other than invariant parameters).

Whoa, I forgot to mention: D doesn't support contravariant parameters, only covariant return types (in other words, a covariant parameter will create a new overload, and not an override). I don't know why I got the idea that D supported that. -- Bruno Medeiros - CS/E student http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
May 07 2006
parent Markus Kranz <markus_kranz gmx.net> writes:
In article <e3kp8d$31ag$1 digitaldaemon.com>, Bruno Medeiros says...
Bruno Medeiros wrote:
 No, it's not unnatural. For a function to override another, the return 
 type must be covariant, but the parameters must be contravariant.
 It's quite natural to be this way, in fact, it's the only correct way 
 (other than invariant parameters).

Whoa, I forgot to mention: D doesn't support contravariant parameters, only covariant return types (in other words, a covariant parameter will create a new overload, and not an override). I don't know why I got the idea that D supported that.

So the actual state is (1) the scope of overload resolution is the current class (and optionally superclasses using alias) (2) the scope of virtual function resolution is the current class and 'childwards' Overloading and overriding on their own seem to be simple concepts. It remains the question: as in D is it enough to implement both concepts separately although they are strongly related? (In both concepts we deal with multiple functions having a common name and search for the 'best' matching instance.) Regards, Markus
May 07 2006
prev sibling parent reply Markus Kranz <markus_kranz gmx.net> writes:
Uh, I didn't know the issue of "covariance versus contravariance" is a topic of
continuing debate:

http://c2.com/cgi/wiki?ContraVsCoVariance

and especially

http://www.cs.trinity.edu/~mlewis/CSCI3294-F01/Papers/p431-castagna.pdf

where I found on page 10:

"At this point, we are able to make precise the roles played
by covariance and contravariance in subtyping: contravariance 
is the correct rule when you want to substitute a function 
of a given type for another one of a different type; 
covariance is the correct condition when you want to specialize 
(in object-oriented jargon "override") a branch of an overloaded 
function by one with a smaller input type."

Regards,
Markus
May 07 2006
parent Bruno Medeiros <brunodomedeirosATgmail SPAM.com> writes:
Markus Kranz wrote:
 Uh, I didn't know the issue of "covariance versus contravariance" is a topic of
 continuing debate:
 
 http://c2.com/cgi/wiki?ContraVsCoVariance
 
 and especially
 
 http://www.cs.trinity.edu/~mlewis/CSCI3294-F01/Papers/p431-castagna.pdf
 
 where I found on page 10:
 
 "At this point, we are able to make precise the roles played
 by covariance and contravariance in subtyping: contravariance 
 is the correct rule when you want to substitute a function 
 of a given type for another one of a different type; 
 covariance is the correct condition when you want to specialize 
 (in object-oriented jargon "override") a branch of an overloaded 
 function by one with a smaller input type."
 
 Regards,
 Markus
 
 

I finally took the time to read it, it is indeed a good article. -- Bruno Medeiros - CS/E student http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
Jun 15 2006