equivariant functions

Denis Koroskin 2korden at gmail.com
Tue Oct 14 15:20:15 PDT 2008


On Wed, 15 Oct 2008 00:31:15 +0400, Steven Schveighoffer  
<schveiguy at yahoo.com> wrote:

> "Andrei Alexandrescu" wrote
>> Steven Schveighoffer wrote:
>>> What about returning a member?  i.e.:
>>>
>>> inout(typeof(s.ptr)) getptr(inout const(char)[] s) { return s.ptr;}
>>
>> Yah there is a recurring problem - we need to find a notation that works
>> nicely and expressively for member functions as well as free functions.
>>
>> For free functions an easy-to-explain-and-understand way is to have
>> "inout" mark the incoming / outgoing TYPE entirely, not only its
>> qualifier. Then:
>>
>> inout stripl(inout const(char)[] s);
>>
>> means: accept any subtype of const(char)[] and call that type "inout" in
>> the return type. Then it's easy to access dependent stuff:
>>
>> typeof(inout.ptr) at(inout const(char)[] s);
>>
>> Notice that for one-parameter functions there's not even a need to  
>> specify
>> the inout in the argument list because it's unambiguous:
>>
>> inout stripl(const(char)[] s);
>> typeof(inout.ptr) at(const(char)[] s);
>>
>> When you get into member functions things aren't quite nice:
>>
>> class A
>> {
>>     private int a;
>>     inout clone() inout; // ehm
>>     typeof(&inout.a) getPtrToA() inout; // ehm
>> }
>>
>>> is that what you had in mind?
>>>
>>> this syntax is going to be used often, since it's what you would use  
>>> for
>>> an accessor.  So it should be simple to understand if possible.
>>
>> Yah I agree. At this point I don't have any solid solution for  
>> notation...
>> please continue rolling out ideas.
>
> Damn, I think there is some confusion here, because we are trying to  
> solve
> two related, but unequal problems.
>
> First is const equivariance (is that the right term?).  I want to  
> specify a
> function that treats an argument as const, but does not affect the const
> contract that the caller has with that argument (i.e. my original scoped
> const proposal).
>
> The second is type equivariance.  I want to specify that I will treat an
> argument as a base type, but I will not change the derived type that the
> caller is using.
>
> These are similar, if you consider that const is a 'base type' of its
> mutable or invariant version.  But there is one caveat that is hard to
> explain using this terminology.  const is a type modifier, not a type.   
> So
> it's not really a base type of a type, and it can be applied to any type  
> in
> existance.  Not the same as specifying a base type.
>
> So the first requirement (const equivariance) does not require that I  
> return
> a derivative of the argument, it requires that I return an argument that  
> is
> part of the input, but contains the same const contract as the passed in
> variable at the *call site*.  It does not require that the return type is
> derived from the input.
>
> It is this first requirement that is the only requirement necessary for
> functions which return unrelated types from their arguments.  i.e., for  
> an
> accessor, which is not returning something of the same type as its  
> argument,
> the only thing that is important to keep the same at the call site is the
> const modifiers.  For example, you can return a base type that is
> automatically converted to a derived type, but that must be accomplished
> with an overridden function.  That is already handled using covariance,  
> and
> I really don't think there is a way to enforce this in the compiler.
>
> An example, a linked list.
>
> class Link
> {
>    private Link next;
>    Link getNext() { return next;}
> }
>
> class DerivedLink : Link
> {
>    int someExtraData;
> }
>
> We currently solve this by adding a covariant getNext() to DerivedLink.
>
> But would it be enough to just change the getNext function in the base  
> type
> to:
>
> inout getNext() { return next; } inout;
>
> I don't think it can be enforced.  Because some other Link function can  
> set
> next to another type of Link (possibly a base Link).  It is up to the
> designer of DerivedLink to ensure that next always points to a  
> descendent of
> DerivedLink.  In this case, I think it's a requirement to use covariant
> functions.
>
> However, it *would* be advantageous to declare getNext as not altering  
> the
> const contract of the caller.  That is, it returns the same constancy  
> that
> it is called with (in this example, inout implies const):
>
> inout(Link) getNext() { return next; } inout;
>
> For the second requirement, I can see value in things like call-chaining:
>
> class C
> {
>   inout doSomething() inout { return this;}
> }
>
> class D : C
> {
>    inout doSomethingElse() inout { return this;}
> }
>
> auto d = new D;
> d.doSomething().doSomethingElse();
>
> However, the return value MUST be exactly the same value as the input.   
> If
> it's only the same type, we lose all compiler enforcibility.
>
> So does it make sense to split these two requirements?
>
> I propose to use inout only to deal with const contracts, and use another
> syntax to signify which parameter will be returned:
>
> inout(X) foo(inout(Y) y); // X and Y are unrelated types
>
> a stab at 'returning this exact parameter'
> X foo(return X x); // The return value from foo will be x, so the  
> compiler
> is free to upcast automatically.
>
> inout(X) foo(return inout(X) x); // combination, signifies that I will
> return x, and I promise not to modify it in the function.
>
> With this scheme, I think possibly inout might not be as clear...
>
> -Steve
>
>

I think you brought very interesting topic with a second example!
However, I believe it may be solved easily. Let's start with your example:

class A
{
     final A foo() {
         // do something
         return this;
     }
}

class B : A
{
}

It is obvious that for any type B which is a subclass of A it is safe to  
do the following:

B b = ...;
b = cast(B)b.foo(); // this is guarantied to be safe

This, however, is only true if the foo() method is final (I talk about  
current D for now).

So you say, why not to allow this without casting by making some rules so  
that compiler could ensure code correctness. I believe this is possible  
and the method signature could be as Andrei proposed:

class A
{
     final typeof(this) foo() {
         // do something
         return this;
     }
}

This is consistent with the behaviour: return type is always covariant  
with the this type at callsite.

Once again, this operation is guarantied to be safe if method returns  
'this' and is final. But we want to take it one step forward! For example,  
we may want to make it virtual, not final. In this case we can lift the  
restriction to return this as well, subject to the following constrains:

1) Method may be made final if and only if it returns this.
2) If base class method is not final then the derived class have to  
override the method.
3) Class may return some other pointer, not only this, but it should  
statically check that the pointer type is covariant with this type.

Note1: Class could also be allowed to not override non-final method of the  
base class if it returns this (it is safe to do so). But this greatly  
increases checking rules complexity. In some cases body might not be  
available so compiler can't whether we return this and therefore are  
forced to override it anyway.

Note2: method that returns null might be also allowed to be final.

Let's put some more examples:

module example1;

class A
{
     final typeof(this) foo() { return this; }
}

class B  // ok
{
     void bar();
}

(new B()).foo().bar();

module example2;

class A
{
     final typeof(this) foo1(); // body is in some other file, guarantied  
to return this
     typeof(this) foo2();
}

class B
{
     typeof(this) foo2() { super.foo2(); return this; }
     void bar() {}
}

(new B()).foo2().foo1().bar();

module example3;

class Link
{
     typeof(this) next() {
         return _next; // ok, typeof(next) == typeof(this). Note that we  
can't make this method final
     }

     private Link _next;
}

class DerivedLink1 : Link
{
     int someExtraData; // fails to compile: class doesn't override  
typeof(this) next();
}

class DerivedLink2 : Link
{
     int someExtraData;
     typeof(this) next() {
         return cast(DerivedLink)super.next(); // ok, might return null
     }
}

auto data = (new DerivedLink2()).next().next().someExtraData;

module example4;

class A
{
     typeof(this) clone() {
         return new A(this); // note that we can't make this method final
     }
}

class B1 : A
{
     // fails to compile, *have to* override clone()
}

class B2 : A
{
     /*final*/ typeof(this) clone() {
         return null; // it's ok. But shall we allow final methods to  
return null?
     }
}

class B3 : A
{
     typeof(this) clone() {
         return new A(); // error, is (A : typeof(this)) is false
     }
}

class B4 : A
{
     typeof(this) clone() {
         return new B4();
     }

     void bar() {}
}

(new B4()).clone().clone().bar();

I think this may work.



More information about the Digitalmars-d mailing list