Re: [jcoplien/trygve] Liskov substitution principle (#112)

78 views
Skip to first unread message

James O Coplien

unread,
May 8, 2016, 7:21:08 AM5/8/16
to jcoplien/trygve, object-co...@googlegroups.com
Hej, Matt,

Thanks for the report. The compiler is correct: both calls are legal calls. There was a bug in the virtual machine that was causing the message dispatcher not to be able to find the script. It’s been fixed in the most recent release. And also in the newest release I’ve tightened up the rules for covariance and contravariance checking.

But since there seems to be some confusion about the LSP I thought I’d post this to the list along with a quick refresher course on some OO basics.

An old version of the LSP is: “If for each object o₁ of type S there is an object o₂ of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o₁ is substituted for o₂, them S is a subtype of T.” The "behavior of P is unchanged” is kind of squishy. The LSP is actually much more than substitutability, and is more properly stated as "if properties provable for objects of one type T1 imply that the same properties are provable for another type T2, then T2 is a subtype of T1.”

“Properties” include pre- and post-conditions for given functions that are applied to objects of the type. A function, as in the mathematical sense, is a mapping from a domain to a range. In the example of your question, there are two separate functions: their domains are distinct. The LSP doesn’t apply to the properties related to the functions of your example, so the LSP is irrelevant with respect to this bug.

Preconditions often tell us what we are allowed to assume about the properties of the method arguments (comparing methods of two objects pairwise) whereas postconditions tell us what we are allowed to assume about the properties of the object(s) returned by the methods. So if a base class method requires a short integer for its first arguments, it is perfectly valid for the derived class to override that method with a method whose first argument is a long integer. The invariants for the derived class arguments can be no less strong than those on the base class arguments: i.e., derived class arguments are more restrictive. One of the tacit arguments is of course self, which naturally follows this pattern. Other arguments follow the rule structure for self. This is called covariance.

Return values work the other way. A client using a derived class object through a base class identifier expects the returned objects to at least have the properties of the type returned by the base class method. It may have additional properties (e.g., it may have even more methods in its interface that the corresponding base class return value features). This goes the opposite way of the parameters and is called contravariance.

So a Liskov-compliant inheritance graph features methods that are covariant in their argument properties and contravariant in their return values.

Each of these classes gives rise to a different form of object, so many forms of objects can stand in where the code is originally written for one kind of object. “Many forms” =  “poly morphos” in Greek, from which we get the term polymorphism. The kind of polymorphism supported by inheritance and its related dynamic techniques is called inclusion polymorphism. However, it’s important to recognize that the ability to say both “1 + 2” and “1.0 + 2.0” makes “+” a polymorphic operator, though its polymorphism falls into a class called overloading. (There are two other common forms of polymorphism, as we find in trygve templates, and as some languages allow as casting. The trygve language does automatic casting and issues a warning if it does so.)

Your example is close to overloading: you are overloading the name foo to mean two different things, to represent two entirely separate and incompatible mappings. The selection is completely resolvable at compile-time. The reason that it’s not overloading in trygve (and in most other languages) is that both signatures do not appear in the same scope.  So they’re just two completely unrelated scripts. Java has some kind of weird treatment of such situations which has always baffled me and which leads me to believe that the Java people don’t understand even old-fashioned OO.

Most polymorphism in DCI takes place in the ability of many different (ordered) sets of objects to play a(n ordered) set of Roles. So the polymorphic mapping in DCI is not base class to derived classes, but rather a set of Roles onto a set of classes whose objects can play those Roles. This subtyping structure in trygve is a DAG rather than a tree, but because Roles and classes are different things, we never have to concern ourselves with the structure of the DAG, nor express the DAG in code. Of course there is ordinary class polymorphism, too, and class derivation is still useful, but it’s not the focus of DCI.

So here’s a puzzle for you. Which of these script declarations are illegal?

class Base {
}

class Derived extends Base {
}

class Base2 {
public Derived func(int i, Base j) { return null }
public Base func2(int i, Derived j) { return null }
public Base func3(int i, Base j) { return null }
public Derived func4(int i, Derived j) { return null }
}

class Derived2 extends Base2 {
public Derived func(int i, Base j) { return null }
public Base func2(int i, Derived j) { return null }
public Derived func3(int i, Derived j) { return null }
public Base func4(int i, Base j) { return null }
public void test() {
}
}

class Derived2b extends Base2 {
public Base func(int i, Base j) { return null }
public Derived func2(int i, Derived j) { return null }
public Base func3(int i, Derived j) { return null }
public Derived func4(int i, Base j) { return null } 
}

class Derived2c extends Base2 {
public Base func(int i, Derived j) { return null }
public Derived func2(int i, Base j) { return null }
public Base func3(int i, Base j) { return null }
public Derived func4(int i, Derived j) { return null }
}

class Derived2d extends Base2 {
public Derived func(int i, Derived j) { return null }
public Base func2(int i, Base j) { return null }
public Derived func3(int i, Base j) { return null }
public Base func4(int i, Derived j) { return null }
}

On 06 May 2016, at 23:56, Matt Browne <notifi...@github.com> wrote:

Should trygve be enforcing the Liskov substitution principle? I had heard this principle referenced before, but just today actually learned about it...so I may not understand it fully yet, but it seems that this code should not compile, but it does:

class A {
    public void foo(int x, int y) {}
}

class B extends A {
    public void foo(int x) {}
}

{
    //this should compile
    new A().foo(1, 2);
    //but this should not, right?
    new B().foo(1, 2);
}

Additionally, I get the following error when running the code:

Error: User process died because of internal trygve error: java.lang.AssertionError

Raoul Duke

unread,
May 8, 2016, 11:23:55 AM5/8/16
to object-co...@googlegroups.com, jcoplien/trygve

First time (in a good way) I've seen LSP converted to contra/covariance, thanks for that.

Raoul Duke

unread,
May 8, 2016, 11:27:10 AM5/8/16
to object-co...@googlegroups.com, jcoplien/trygve


> First time (in a good way) I've seen LSP converted to contra/covariance, thanks for that.

Reading the Wikipedia entry (have I not done that before? Probably. I have become addled in the memory. What was I talking about?) I see it is in there via mentioning DbC. I gotta start taking supplements, what stems dementia?

Matthew Browne

unread,
May 8, 2016, 12:20:56 PM5/8/16
to object-co...@googlegroups.com
Yes, great post Cope; I'm still digesting it. Andreas et al., let's just post only to the group for now to avoid the duplicate notifications...we can always move the discussion back to github if it becomes more technically focused on trygve internals.

On May 8, 2016 12:15:42 PM EDT, Andreas <notifi...@github.com> wrote:

Hmm, it seems that github fooled me with the quoting (again), now that I see in the DCI group that Raoul made those two last posts, not Cope. Cross-posting to github issues creates a little security/identity hole.


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub


--
Sent from my Android device with K-9 Mail.

Greg Young

unread,
May 8, 2016, 12:46:59 PM5/8/16
to object-co...@googlegroups.com
An old version of the LSP is: “If for each object o₁ of type S there
is an object o₂ of type T such that for all programs P defined in
terms of T, the behavior of P is unchanged when o₁ is substituted for
o₂, them S is a subtype of T.” The "behavior of P is unchanged” is
kind of squishy. The LSP is actually much more than substitutability,
and is more properly stated as "if properties provable for objects of
one type T1 imply that the same properties are provable for another
type T2, then T2 is a subtype of T1.”

“Properties” include pre- and post-conditions for given functions that
are applied to objects of the type. A function, as in the mathematical
sense, is a mapping from a domain to a range. In the example of your
question, there are two separate functions: their domains are
distinct. The LSP doesn’t apply to the properties related to the
functions of your example, so the LSP is irrelevant with respect to
this bug."

This is much more apparent in languages like eiffel.
> --
> You received this message because you are subscribed to the Google Groups
> "object-composition" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to object-composit...@googlegroups.com.
> To post to this group, send email to object-co...@googlegroups.com.
> Visit this group at https://groups.google.com/group/object-composition.
> For more options, visit https://groups.google.com/d/optout.



--
Studying for the Turing test

James O Coplien

unread,
May 8, 2016, 3:39:12 PM5/8/16
to object-co...@googlegroups.com

On 08 May 2016, at 18:46, Greg Young <gregor...@gmail.com> wrote:

This is much more apparent in languages like eiffel.

Yes, except, of course, Eiffel doesn’t quite get it right. Invocations on “self” can’t in general meet the class invariants. But Meyer has made many insighted contributions to the “design-by-contract” way of thinking. Most of my writing about assertions (as opposed to TDD) is inspired by his thinking. And so, in some large degree, was the requires section in trygve.

Greg Young

unread,
May 8, 2016, 3:46:15 PM5/8/16
to object-co...@googlegroups.com
Likely this should be a personal email but I think its reasonable to
put to the group.

We had Bertrand over to Lithuania a few years ago and one of the best
things he said was (and I may paraphrase) "Liskov substitution, its
also known as object orientation".

James O Coplien

unread,
May 8, 2016, 4:03:55 PM5/8/16
to object-co...@googlegroups.com
Bertrand and I have a pretty good relationship — I think that I and Stan Lippman are the only people with a C++ lineage who get along well with him. The main meeting of the minds that Trygve and I first had, about our shared vision related to the human connection, was at a TOOLS conference in France (TOOLS was run by Bertrand and his wife and, from time to time, his daughter). I have been a frequent speaker at TOOLS in Australia, Europe and the U.S., and I have in turn invited Bertrand to keynote at conferences I have chaired in the past. Rhetorically he is one of the most entertaining speakers out there, and I highly respect his intellectual integrity, which in my opinion is among the highest in our business.

Bertrand’s quote as you present it below is hardly news. The LSP was regarded as the definition of OO by the OOPSLA crowed, at least leading up to the Treaty of Orlando where the repercussions of the LSP were both ratified and broadened. So Bertrand was just stating the common opinion of the 1980s when he said this. Maybe he was still saying this in the 1990s… The problem is that the formalisation of the concepts in Eiffel was at some level broken, particularly with respect to self-encapsulation. The Eiffel contract realisation of LSP in fact had some holes.

Of course the whole OOPSLA crowd (and Bertrand) were out of line with Kay’s original vision, and that led to a rant by Kay in his keynote at, I think, OOPSLA 1995. Bertrand was never an object-oriented person but was fixated on the classes. Eiffel is all about class design. He probably took class-oriented programming to its apex.

By the way, the beat goes on. The U.S. company Bloomberg is trying to push through a proposal for contracts as part of the forthcoming C++ standard. I attended a talk about it at the ACCU conference a couple of weeks ago. They blow off the self-encapsulation problem and just mitigate the problem by requiring that all invocations on this be private.

Last I talked with Bertrand he was running ETH in Switzerland but I think he has since moved on. I don’t know how he makes a living these days.

Raoul Duke

unread,
May 8, 2016, 8:47:53 PM5/8/16
to object-co...@googlegroups.com

Hence Sather iirc.

Matthew Browne

unread,
May 8, 2016, 11:12:12 PM5/8/16
to object-co...@googlegroups.com
Hi Cope,
Thank you for the informative post. I worked on the puzzle have been re-reading your post as well as the Wikipedia article on covariance and contravariance but I am still confused.

On 5/8/16 7:21 AM, James O. Coplien wrote:
A client using a derived class object through a base class identifier expects the returned objects to at least have the properties of the type returned by the base class method. It may have additional properties (e.g., it may have even more methods in its interface that the corresponding base class return value features). This goes the opposite way of the parameters and is called contravariance.
That makes sense but it sounds like what the Wikipedia article described as covariance, not contravariance:

Covariant method return type[edit]

In a language which allows covariant return types, a derived class can override the getAnimalForAdoption method to return a more specific type:

    class CatShelter extends AnimalShelter {
        Cat getAnimalForAdoption() {
    	    return new Cat();
        }
    }

Among mainstream OO languages, Java and C++ support covariant return types, while C# does not. Adding the covariant return type was one of the first modifications of the C++ language approved by the standards committee in 1998.[4] Scala and D also support covariant return types.


Based on the Wikipedia definition, it would seem that trygve is now forbidding covariant return types but allowing contravariant return types. For example, Derived2.func4() compiles but Derived2.func3() does not. Consider the following code:

class Base2 {
    public Base func3(Base j) { return null }
}

class Derived2 extends Base2 {
    public Derived func3(Derived j) { return null }
}

class SomeClass {
    public void test(Base2 o, Derived2 p) {
        Base x = o.func3();
        Base y = p.func3();
    }
}


Variables x and y are both of type Base, which matches the return type of Base2.func3(), so what's the problem here in terms of substitutability? (Note: o could be an instance of either Base2 or Derived2; I just created separate parameters for a clearer example.)

Meanwhile, I do see what looks like a potential problem with the reverse:

class Base2 {
    public Derived func5() { return null }
}

class Derived2 extends Base2 {
    public Base func5() { return null }
}

class SomeClass {
    public void test(Base2 o) {
       
Derived x = o.func5();
    }
}


(Just in case this is starting to sound argumentative, that's not my intent - I'm asking these questions in an effort to understand.)

If o is an instance of Derived2, then x might not actually be an instance of Derived, so I don't understand how this is type-safe. And I got an internal trygve error (java.lang.ClassCastException) when I tried to test it:

new SomeClass().test(new Derived2());

Additionally, it seems that the trygve environment allows covariant argument types, but according to the Wikipedia article that's a violation of the Liskov substitution principle. Speaking of Eiffel (quoting Wikipedia again):

Covariant method argument type

Uniquely among mainstream languages, Eiffel allows the arguments of an overriding method to have a more specific type than the method in the superclass (argument type covariance). Thus, the Eiffel version of the following code would type check, withputAnimal overriding the method in the base class:

    class CatShelter extends AnimalShelter {
        void putAnimal(Cat animal) {
           ...
        }
    }

This is not type safe. By up-casting a CatShelter to an AnimalShelter, one can try to place a dog in a cat shelter. That does not meet CatShelter argument restrictions. The lack of type safety (known as the "catcall problem" in the Eiffel community) has been a long-standing issue.


The claim that this is unique to Eiffel is questionable since at least in the current version of Java, code like their example does compile. Since trygve doesn't currently allow the programmer to do explicit casting, maybe this is a non-issue. I think I'm now at least clear on the reverse situation (e.g. your long integer / short integer example) and why that's OK and allows for substitutability.

Matthew Browne

unread,
May 8, 2016, 11:15:35 PM5/8/16
to object-co...@googlegroups.com
Also, the following code produces an internal error (see below):

class Base {}
class Derived extends Base {}

class A {
public Base foo(Base x) {
System.out.println("A.foo()");
return null;
}
}

class B extends A {
public Base foo(Derived x) {
System.out.println("B.foo()");
return null;
}
}

{
new B().foo(new Base());
}

---

Exception: User process died because of internal trygve error:
java.util.EmptyStackException

java.util.EmptyStackException
at java.util.Stack.peek(Stack.java:102)
at java.util.Stack.pop(Stack.java:84)
at
info.fulloo.trygve.run_time.RunTimeEnvironment.popStack(RunTimeEnvironment.java:306)
at
info.fulloo.trygve.run_time.RTExpression$RTMessage$RTPostReturnProcessing.run(RTExpression.java:868)
at
info.fulloo.trygve.run_time.RunTimeEnvironment.runner(RunTimeEnvironment.java:445)
at
info.fulloo.trygve.run_time.RunTimeEnvironment.run(RunTimeEnvironment.java:227)
at
info.fulloo.trygve.editor.TextEditorGUI.simpleRun(TextEditorGUI.java:1001)
at
info.fulloo.trygve.editor.TextEditorGUI$32.doInBackground(TextEditorGUI.java:1067)
at
info.fulloo.trygve.editor.TextEditorGUI$32.doInBackground(TextEditorGUI.java:1060)
at javax.swing.SwingWorker$1.call(SwingWorker.java:296)
at java.util.concurrent.FutureTask.run(FutureTask.java:262)
at javax.swing.SwingWorker.run(SwingWorker.java:335)
at
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)

James O Coplien

unread,
May 9, 2016, 4:51:56 AM5/9/16
to object-co...@googlegroups.com

On 09 May 2016, at 05:12, Matthew Browne <mbro...@gmail.com> wrote:

Hi Cope,
Thank you for the informative post. I worked on the puzzle have been re-reading your post as well as the Wikipedia article on covariance and contravariance but I am still confused.

Maybe it would be good to use a concrete example to show why things are the way they are.

Consider this code:

class Base {
public void bf1() { System.out.println("Base.bf1") }
}

class Derived extends Base {
public void bf1() { System.out.println("Derived.bf1") }
public void df1() { System.out.println("Derived.df1") }
}

class TheBase {
public void f(Base b) { b.bf1() }
}

class TheDerived extends TheBase {
public void f(Derived d) { d.bf1() }
}

{
Base aBase = new Base();
Derived aDerived = new Derived();
TheBase b = new TheBase();
TheDerived d = new TheDerived();
b.f(aBase);
d.f(aDerived)
}

The parameter types for the method f are covariant in the direction of inheritance: that is, they are covariant with the relationships between the classes containing the respective methods. I can compile and run this code and it produces:

Base.bf1
Derived.bf1

Now let’s say I make the relationship of the parameter types (classes) contravariant to the relationship of the classes of the scripts’ classes:

class Base {
public void bf1() { System.out.println("Base.bf1") }
}

class Derived extends Base {
public void bf1() { System.out.println("Derived.bf1") }
public void df1() { System.out.println("Derived.df1") }
}

class TheBase {
public void f(Derived b) { b.bf1(); b.df1() }
}

class TheDerived extends TheBase {
public void f(Base d) { d.bf1() }
}

{
Base aBase = new Base();
Derived aDerived = new Derived();
TheBase b = new TheBase();
TheDerived d = new TheDerived();
b.f(aBase);
d.f(aDerived)
}

This gives the compilation error:

line 23: Script `f(Base)' not declared in class `TheBase’.

and this error is the result of the type system enforcing that parameters must be covariant with the type for which scripts are called. If the script TheBase.f were to accept this call, then the code in TheBase.f would end invoking the df1() script on the argument — but since the argument is of type Base, that method wouldn’t exist! So the parameter types must be covariant with the class of the object for which the script is invoked.

If I comment out line 23:

// b.f(aBase);

the program runs as it should, calling d.f, which selects the f  script from the d object, where  d has been bound to the same object  aDerived is bound to; i.e. the newly created Derived  object from line 20; and, indeed, we find that  Derived  indeed does contain an f  script so it is invoked and the program outputs:

Derived.bf1

It’s legal because it’s perfectly find to pass in a Derived where a Base is expected and to cal itsl script because, by the subtyping that trygve accords on objects related by inheritance, a  Derived IS-A  Base.

You can make the corresponding argument by example to show there are similar possible problems with the return types, except it goes the other way.

If it were the other way, IS-A simply wouldn’t work.

I’ll look at your most recent bug.

Matthew Browne

unread,
May 11, 2016, 12:52:31 PM5/11/16
to object-co...@googlegroups.com

Hi Cope,
Thanks for the example. I am trying to reconcile what you are saying, and what the trygve compiler is doing, with what I am seeing on Wikipedia and elsewhere on the web, and in my code experiments. Given your comments about Java, I wasn't surprised to see that Java's inheritance rules are quite different from trygve's, but I was a little surprised to see that C++ has pretty much the opposite behavior as well (at least with respect to return types).

Consider this trygve code:

class Base {}
class Derived extends Base {}

class Base2 {

    public Base func3() { return null }
    public Derived func4() { return null }
}

class Derived2 extends Base2 {
    public Derived func3() { return null }
    public Base func4() { return null }
}

The compiler complains about func3:

line 10: Return type `Base' of `func3()' in class `Base2' must be no less restrictive than `Derived' in the derived class.

I ported this code to C++, which was fine with func3 but complained about func4:

class Base {};
class Derived: public Base {};

class Base2 {
    public:
        virtual Base* func3() const {return NULL;}
        virtual Derived* func4() const {return NULL;}
};

class Derived2: public Base2 {
    public:
        virtual Derived* func3() const {return NULL;}
        virtual Base* func4() const {return NULL;}
};

Error:

return type of virtual function 'func4' is not covariant with the return type of the function
      it overrides ('Base *' is not derived from 'Derived *')

Do you disagree with the C++ behavior, and if so, why?

In your previous post, you said:

So if a base class method requires a short integer for its first arguments, it is perfectly valid for the derived class to override that method with a method whose first argument is a long integer. The invariants for the derived class arguments can be no less strong than those on the base class arguments: i.e., derived class arguments are more restrictive. One of the tacit arguments is of course self, which naturally follows this pattern. Other arguments follow the rule structure for self. This is called covariance.

If a long integer parameter (in a derived class) is overriding a short integer parameter (in a base class), wouldn't that be contravariance, not covariance?

I was also reading this article:
http://cpptruths.blogspot.com/2015/11/covariance-and-contravariance-in-c.html?m=1

It has an example of vehicles and cars, and it says:

The client of VehicleFactory (which has no knowledge of CarFactory) can use VehicleFactory safely even if the create function gets dispatched to CarFactory at run-time. After all, the create function return something that can be treated like a vehicle. No concrete details about Car are necessary for the client to work correctly.
...so why does the trygve compiler reject func3() in the example above? In the definitions I've seen of the Liskov substitution principle, it should be possible for derived types to be used in place of base types, but I haven't seen any definition asserting that it has to work the other way around.

Thanks,
Matt

James O Coplien

unread,
May 11, 2016, 1:35:47 PM5/11/16
to object-co...@googlegroups.com
Download trygve 2.19, try again, and let’s take the discussion from there.

James O Coplien

unread,
May 11, 2016, 1:40:59 PM5/11/16
to object-co...@googlegroups.com
On 11 May 2016, at 18:52, Matthew Browne <mbro...@gmail.com> wrote:

Do you disagree with the C++ behavior, and if so, why?

I did some exploring of C++ myself today and found that something has changed since I last looked at these issues in detail: namely, that there is absolutely no contravariance support! I notice that Bertrand Meyer said much the same thing in his book and that surprised me, so maybe it’s been so longer than I knew. There is apparently support for covariant return types.

I suspect C++ did this to avoid the overloading ambiguities that Java handles in an awkward (read: wrong) fashion. Peggy Ellis’ original paper on the rules for C++ overloading semantics still stands as one of the largest C++ specs ever written for a language feature…

Anyhow, I’ve finessed trygve’s lookup to use a much easier-to-explain approach, both for compile-time checking and run-time dispatching. We’ll see if it changes the behavior of your examples.

— Cope

Matthew Browne

unread,
May 11, 2016, 1:59:51 PM5/11/16
to object-co...@googlegroups.com

I ran the same code in trygve 2.19 and it now gives a different error message, this time about func4 which is what I would expect:

line 11: Return type `Base' of `func4()' in class `Base2' must be no less restrictive than `Derived' in the base class.
...but the error message is confusing. For context, here's the code example again:

class Base {}
class Derived extends Base {}

class Base2 {
    public Base func3() { return null }
    public Derived func4() { return null }
}

class Derived2 extends Base2 {
    public Derived func3() { return null }
    public Base func4() { return null }
}

Shouldn't the error say, "in class `Derived2'", not "in class `Base2'"?

I did some exploring of C++ myself today and found that something has changed since I last looked at these issues in detail: namely, that there is absolutely no contravariance support!
I assume you're talking about contravariant return types, not argument types? The article I mentioned describes a way to achieve contravariant argument types C++, but there seems to be no direct language support for it and perhaps it's a bad idea.

James O Coplien

unread,
May 11, 2016, 3:01:55 PM5/11/16
to object-co...@googlegroups.com
On 11 May 2016, at 19:59, Matthew Browne <mbro...@gmail.com> wrote:

Shouldn't the error say, "in class `Derived2'", not "in class `Base2’"?

You are exactly right; thanks. A fix is on the way.



I did some exploring of C++ myself today and found that something has changed since I last looked at these issues in detail: namely, that there is absolutely no contravariance support!
I assume you're talking about contravariant return types, not argument types?

Yup.


The article I mentioned describes a way to achieve contravariant argument types C++, but there seems to be no direct language support for it and perhaps it's a bad idea.

It’s just a bad idea — unless you want to revert to compile-time binding, in which case most of the concerns become irrelevant.

Matthew Browne

unread,
May 11, 2016, 3:50:11 PM5/11/16
to object-co...@googlegroups.com
On 5/11/16 3:01 PM, James O Coplien wrote:
I did some exploring of C++ myself today and found that something has changed since I last looked at these issues in detail: namely, that there is absolutely no contravariance support!
I assume you're talking about contravariant return types, not argument types?

Yup.
Isn't it also now true that trygve doesn't support contravariant return types, as of version 2.19? If it supported both covariant and contravariant return types, then the entire code example in my previous post should compile, should it not?

I would think "the direction of inheritance" means going in the direction from base classes to derived classes. So Derived2.func4() is an example of a contravariant return type, since it goes the opposite direction, is that right?

class Base2 {

    public Derived func4() { return null }
}

class Derived2 extends Base2 {
    public Base func4() { return null }
}

James O Coplien

unread,
May 11, 2016, 5:28:31 PM5/11/16
to object-co...@googlegroups.com

On 11 May 2016, at 21:50, Matthew Browne <mbro...@gmail.com> wrote:

Isn't it also now true that trygve doesn't support contravariant return types, as of version 2.19? If it supported both covariant and contravariant return types, then the entire code example in my previous post should compile, should it not?

Covariant return types are unsafe (lead to MESSAGE-NOT-UNDERSTOOD) unless you turn off messaging in those cases and turn on compile-time binding. That just didn’t seem the right thing to do. I want a uniform binding model.

Matthew Browne

unread,
May 11, 2016, 6:13:49 PM5/11/16
to object-co...@googlegroups.com
I think there might be a discrepancy in the use of the terms "covariant" and "contravariant" here...Wikipedia gives this as an example of a covariant return type:

class AnimalShelter {
    Animal getAnimalForAdoption() {
      ...

    }
}

class CatShelter extends AnimalShelter {
    Cat getAnimalForAdoption() {
        return new Cat();
    }
}


...which it seems is what trygve now allows. The opposite (contravariant, by Wikipedia's definition) is not compiling in trygve as of version 2.19.

In my recent reading on this topic, I haven't seen mention of any language that supports contravariant return types by Wikipedia's definition. And I haven't seen any alternative definitions except in your posts; perhaps your usage is more correct but in any case it would be nice to have a clear definition that we all agree to use on this list. If I'm just misreading your posts and the Wikipedia article I apologize for taking up your time with this, but as far as I can gather at the moment, you are using the term differently.

Matthew Browne

unread,
May 11, 2016, 6:24:33 PM5/11/16
to object-co...@googlegroups.com

P.S. I checked a few C++ books on Google Books as well, just to be sure I wasn't misunderstanding the Wikipedia article and that Wikipedia wasn't using the term incorrectly.

Matthew Browne

unread,
May 11, 2016, 10:18:55 PM5/11/16
to object-co...@googlegroups.com

I think I may have found the reason for the discrepancy...according to this article:

"A return value is contravariant if you can assign the return type to a variable of a less derived type than the formal parameter."

So from the perspective of the calling code, I think this would be an example of a contravariant return type:

class Base2 {
    public Base func3() { return null }
}

class Derived2 extends Base2 {
    public Derived func3() { return null }
}

Derived2 d = new Derived2();
Base b = d.func3();

...because b is given the type Base rather than the declared return type of Derived2.func3(), making it contravariant since Base is less specific than Derived.

But when discussing the classes themselves, we can say (I think) that the return type of func4() is contravariant since it's more generic in the derived class, which is also causes a compile error since instances of Derived2 would no longer necessarily be able to substitute for instances of Base2:

class Base2 {
    public Derived func4() { return null }
}

class Derived2 extends Base2 {
    public Base func4() { return null }
}

Does that sound about right?

James O Coplien

unread,
May 12, 2016, 4:25:49 AM5/12/16
to object-co...@googlegroups.com
Yup.
Reply all
Reply to author
Forward
0 new messages