Constness and delegates

Timon Gehr timon.gehr at gmx.ch
Fri Jan 10 21:25:50 UTC 2020


On 10.01.20 14:54, Mafi wrote:
> On Friday, 10 January 2020 at 02:50:16 UTC, Timon Gehr wrote:
>> On 10.01.20 00:11, Mafi wrote:
>>> Regarding the work and comments on a pull request about delegate 
>>> constness (https://github.com/dlang/dmd/pull/10644) I think there is 
>>> some things we need to discuss.
>>>
>>> I think the only sane way to analyze the soundness of delegates is to 
>>> equate:
>>>
>>> R delegate(P) qualifier f;
>>>
>>> with:
>>>
>>> interface I { R f(P) qualifier; }
>>> I f;
>>> ...
>>
>> No, this is not exactly the right way to think about this. A delegate 
>> is not an interface, because it *includes* an _opaque_ context 
>> pointer. The qualifier on a delegate qualifies _both_ the function and 
>> the context pointer and the reason why qualifiers can be dropped 
>> anyway is that `qualified(void*)` can implicitly convert to `void*` 
>> (even though DMD does not know it yet) and the delegate maintains an 
>> invariant that says that the function pointer and the context pointer 
>> are compatible.
> 
> I see. So the additional opacity is that an interface type I, that is 
> qualified mutable, could be upcast to some class C and you would expect 
> C to mutable as well. Therefore you may not convert an interface 
> consisting of only const methods from const to mutable. But a delegate 
> is different, it can never be inspected. Correct? That's interesting!
> ...

Yes. :)

>> ...
>>> Therefore the qualifier should be handled in a contravariant manner 
>>> (and not covariant) because it describes the implicit this-Parameter 
>>> of the referenced method and not the stored this-Pointer. The 
>>> const-ness of the this-Pointer is the one "outside" the delegate.
>>> ...
>>
>> Yes, exactly.
>>
>> ...
>>
>> Yes. And it's not only `const`. You can lose *all* qualifiers.
>>
> 
> So the qualifier on the right of the delegate is not actually 
> contravariant in the type-way but rather the inside-out direction of 
> transitive constness (mutable data can reference const data can 
> reference immutable data). Thus delegate() immutable -> delegate() const 
> -> delegate(). This is also a nice symmetry between constness and other 
> qualifiers.
> 
> Additionally because of the simple opaque nature of delegates 
> 'qualifierA R delegate(P) qualifer1' should be implicitely convertible 
> to 'qualifierB R delegate(P) qualifier1' as long as qualifierA and 
> qualifierB are "weaker" than qualifier1. That is 
> mutable/const/immtutable R delegate() immutable all convert to one 
> another. And mutable/const R delegate(P) const convert between each other.
> 
> Which I think gives this graph (qualifier1/2 => qualifier1 R delegate(P) 
> qualifier2:
> 
> m/m <- m/c <- m/i
>   |      ^      ^
>   v      v      v
> c/m <- c/c <- c/i
>   ^      ^      ^
>   |      |      v
> i/m <- i/c <- i/i
> ...

Yes.

> Where a delegate is callable iff 'qualifier1 qualifier2' is convertible 
> to 'qualifier2'. Therefore only i/m and c/m are not callable (and they 
> don't convert to a callable one). Is this correct?
> 
> 

Yes. I think the best way to think about it is that qualifier1 applies 
to the context pointer and the function pointer, while qualifier2 
applies to the context pointer and the implicit context parameter that 
the function pointer takes as an argument. The qualifier on the context 
(qualifier1 qualifier2) always has to be at least as strong as what the 
function pointer accepts (qualifier2) if the delegate is to be callable, 
while the qualifier on the function pointer itself does not matter (as 
the code it points to is immutable).

Conceptually, the type of a closure mapping A to B can be described as 
an existential type ∃C. C×(A×C→B). All type checking rules basically 
follow from this, for example:

∃C. immutable(C)×(A×immutable(C)→B)
=
∃C. const(immutable(C))×(A×const(immutable(C))→B)
⊆
∃C'. const(C')×(A×const(C')→B)
⊆
∃C''. C''×(A×C''→B)

(Where C' is substituted for immutable(C), and C'' for const(C).)

The implementation using void* is an unsafe approximation made necessary 
by D's type system not being powerful enough, but any code that respects 
this typing of delegates can be @trusted.


More information about the Digitalmars-d mailing list