www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - The tailconst problem (and suggestions for solution)

reply "Janice Caron" <caron800 googlemail.com> writes:
Walter, I have thought very hard about what you said, and I have come
to the conclusion that Andrei is absolutely correct. However, there
might be some things you can do to mitigate things a little, to make
things slightly easier for the programmer.

Once I realised that my ampersand idea wouldn't work, because it would
set up a contradiction between the requirements of generic
programming, and the requirement of being able to wrap anything in a
struct, my next thought was a new keyword: "tailconst". (Actually, two
new keywords, because you'd also need "tailinvariant"). With this
keyword,

    class C {}
    tailconst(C) c;

would mean "c is a mutable ref to const data". Of course that does
then beg the question, what does tailconst(T) mean for T in general,
if T is not a class. The answer is:

(1) from the perspective of a human being writing code, consider it
undefined. Just don't use it. Except with classes, there is never a
need to use it all, as everything you want can be expressed more
clearly with const, and

(2) from the perpective of the compiler, one can come up with some
rules to make it all work properly under the hood. I did actually come
up with most of what those rules would need to be (...and I can
certainly tell you if you want...), but then it occurred to me that
that must be exactly what Andrei did when he proposed his template
idea.

In other words, I suspect that my "tailconst(C)" idea is identical in
practice to Andrei's "TailConst!(T)" idea, the only difference being
that I made up new keywords and Andrei didn't. I still think that the
tailconst keyword idea is /slightly/ preferable to the TailConst!
template idea, if only because of the implementation of the
specialization for classes. "tailconst" could give us compiler support
for the notion of "mutable ref to const data", whereas TailConst!
might involve a lot of fiddling about.

The third possibility is of course, doing without it altogether.

That's easy to do in principle: If you want a mutable reference, use a pointer!

    class C {}
    const(C)* c;

...or arrays

    class C {}
    const(C)*[] a;

There are two problems with this, however. Neither of the following
two lines will compile:

    a[3] = new C;
    auto x = a[3],member;

The first one won't compile, because the compiler won't implicitly
convert a reference-to-class-data into a
pointer-to-reference-to-class-data (and with good reason!). The second
one won't compile because the type of a[3] is
pointer-to-reference-to-class-data, not reference-to-class-data, and
the dot operator will only dereference once.

The only way I can think of getting around this is to allow two new
rules (one of which I've actually asked for before). The first is:

(1) Make it possible to implicitly convert a reference-to-class-data
into a pointer-to-reference-to-class-data. The implementation of this
trick must first copy the reference onto the heap, so that the pointer
points into the heap, not into the stack.

(2) (and this is the one I've asked for before) allow dereferencing to
be recursive, so that p.member will work even if p is a
pointer-to-pointer-to-pointer-to-pointer-to-data.

I think those two rules will let us work with const(C)*[], and hence
do away with the need for tailconst altogether.
Dec 07 2007
next sibling parent reply Walter Bright <newshound1 digitalmars.com> writes:
Janice Caron wrote:
 Once I realised that my ampersand idea wouldn't work, because it would
 set up a contradiction between the requirements of generic
 programming, and the requirement of being able to wrap anything in a
 struct, my next thought was a new keyword: "tailconst". (Actually, two
 new keywords, because you'd also need "tailinvariant"). With this
 keyword,
 
     class C {}
     tailconst(C) c;
 
 would mean "c is a mutable ref to const data".

Regardless of the technical merits of tailconst and tailinvariant, I suspect that going from 2 kinds of const to 4 kinds will produce a revolt and a general sense that D has miserably failed at a comprehensible const design. This is on top of the problem that I have never been able to find the words to explain in a straightforward manner what tailconst even means.
 Of course that does
 then beg the question, what does tailconst(T) mean for T in general,
 if T is not a class. The answer is:
 
 (1) from the perspective of a human being writing code, consider it
 undefined. Just don't use it. Except with classes, there is never a
 need to use it all, as everything you want can be expressed more
 clearly with const, and

That doesn't work because of the need to have a consistent system for use with generic code.
 (2) from the perpective of the compiler, one can come up with some
 rules to make it all work properly under the hood.

I've never been smart enough to keep all the const rules in my head at the same time, which is why the last const design is coming up short. I forgot a couple :-( This is possibly why I also have trouble explaining it.
 I did actually come
 up with most of what those rules would need to be (...and I can
 certainly tell you if you want...), but then it occurred to me that
 that must be exactly what Andrei did when he proposed his template
 idea.
 
 In other words, I suspect that my "tailconst(C)" idea is identical in
 practice to Andrei's "TailConst!(T)" idea, the only difference being
 that I made up new keywords and Andrei didn't. I still think that the
 tailconst keyword idea is /slightly/ preferable to the TailConst!
 template idea, if only because of the implementation of the
 specialization for classes. "tailconst" could give us compiler support
 for the notion of "mutable ref to const data", whereas TailConst!
 might involve a lot of fiddling about.

If it can be done with a template, then I'd prefer the template solution because: 1) templates need to be powerful enough to do such things and this would add credence to D templates and 2) the language is complex enough already and 3) there would have to be a big advantage to putting it in the core language and I don't see one.
 The third possibility is of course, doing without it altogether.
 
 That's easy to do in principle: If you want a mutable reference, use a pointer!
 
     class C {}
     const(C)* c;
 
 ...or arrays
 
     class C {}
     const(C)*[] a;
 
 There are two problems with this, however. Neither of the following
 two lines will compile:
 
     a[3] = new C;
     auto x = a[3],member;
 
 The first one won't compile, because the compiler won't implicitly
 convert a reference-to-class-data into a
 pointer-to-reference-to-class-data (and with good reason!). The second
 one won't compile because the type of a[3] is
 pointer-to-reference-to-class-data, not reference-to-class-data, and
 the dot operator will only dereference once.
 
 The only way I can think of getting around this is to allow two new
 rules (one of which I've actually asked for before). The first is:
 
 (1) Make it possible to implicitly convert a reference-to-class-data
 into a pointer-to-reference-to-class-data. The implementation of this
 trick must first copy the reference onto the heap, so that the pointer
 points into the heap, not into the stack.

I'm uncomfortable with solutions that imply heap allocation in a non-obvious manner.
 (2) (and this is the one I've asked for before) allow dereferencing to
 be recursive, so that p.member will work even if p is a
 pointer-to-pointer-to-pointer-to-pointer-to-data.

The problem with this is the C problem with confusing * and [] for pointers and arrays. Such ambiguities work well initially, but as the language grows more and more problems result from it.
 I think those two rules will let us work with const(C)*[], and hence
 do away with the need for tailconst altogether.

I'd like to accumulate more experience with const to see if there is a pressing need to solve this problem, otherwise we risk throwing in complex solutions for non-issues.
Dec 07 2007
next sibling parent Don Clugston <dac nospam.com.au> writes:
Janice Caron wrote:
 On Dec 7, 2007 11:00 AM, Walter Bright <newshound1 digitalmars.com> wrote:
 That doesn't work because of the need to have a consistent system for
 use with generic code.

I think I got misunderstood. But no matter - it's unimportant. It just goes to show how hard it is to have a conversation about this stuff at all! :-)
 If it can be done with a template, then I'd prefer the template solution

I believe it can be done with a template, yes. However, that template needs to be written by someone who really understands what goes on under the hood, because (if it works like I think it will have to work) that template is going to need to do something which the manual says is undefined: it will have to cast away const (in at least one place). That's not a problem in a standard-library-provided template, of course. It's probably better done there than in user code. That said, Andrei is much smarter than I, so maybe he can do it without even needed that cast.

Don't you just need to wrap it in a struct ?? struct A { const B b; } A a; Then you can't change a.b, but you can replace a. Or did I miss something?
Dec 07 2007
prev sibling parent reply "Craig Black" <cblack ara.com> writes:
 I'd like to accumulate more experience with const to see if there is a 
 pressing need to solve this problem, otherwise we risk throwing in complex 
 solutions for non-issues.

Thank you for that good explanation. I know it must be frustrating going over this stuff again and again with us mortals. What I don't understand is why. const(C) c; Means that the reference is constant. It would be more intuitive (to me) if it meant that the instance was constant. Was it decided that it is more important to have constant references than constant instances? -Craig
Dec 07 2007
parent "Craig Black" <cblack ara.com> writes:
"Janice Caron" <caron800 googlemail.com> wrote in message 
news:mailman.250.1197041224.2338.digitalmars-d puremagic.com...
 On Dec 7, 2007 3:12 PM, Craig Black <cblack ara.com> wrote:
 What I don't understand is
 why.

 const(C) c;

 Means that the reference is constant.

Look at it another way. You'd want const T t; t = something; to fail, always - regardless of the type of T. It has to be true when T is a class, because it has to be true for everything. Remember, the two new (vastly simplified, but brilliant) rules are: (1) const type identifer; means the same thing as const(type) identifier; (2) Everything inside the brackets is const

I think you are misunderstanding my question. Everything inside the parenthesis should be constant. I am in total agreement here. But in this example, the only thing that appears inside the parenthesis is the class type, and it is interpreted as the class instance is mutable, but the reference is constant. I've been reading the other const thread and Sean Kelly mentioned the same thing regarding arrays. I think Walter mentioned fixing this in the next release. I'm not sure though.
Dec 07 2007
prev sibling next sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On Dec 7, 2007 11:00 AM, Walter Bright <newshound1 digitalmars.com> wrote:
 That doesn't work because of the need to have a consistent system for
 use with generic code.

I think I got misunderstood. But no matter - it's unimportant. It just goes to show how hard it is to have a conversation about this stuff at all! :-)
 If it can be done with a template, then I'd prefer the template solution

I believe it can be done with a template, yes. However, that template needs to be written by someone who really understands what goes on under the hood, because (if it works like I think it will have to work) that template is going to need to do something which the manual says is undefined: it will have to cast away const (in at least one place). That's not a problem in a standard-library-provided template, of course. It's probably better done there than in user code. That said, Andrei is much smarter than I, so maybe he can do it without even needed that cast.
 I'd like to accumulate more experience with const to see if there is a
 pressing need to solve this problem, otherwise we risk throwing in
 complex solutions for non-issues.

Makes total sense to me. I agree completely. Thanks
Dec 07 2007
prev sibling next sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On Dec 7, 2007 2:14 PM, Don Clugston <dac nospam.com.au> wrote:
 Don't you just need to wrap it in a struct ??

 struct A
 {
    const B b;
 }

 A a;
 Then you can't change a.b, but you can replace a.
 Or did I miss something?

I thought the intent was that TailConst!(T) would be accessed exactly like T or const(T). As in, you want to be able to write "a.member", but access "b.member". As in: class T { int x; this() { x = 42; } int get() const { return x; } void set(int n) { x = n; } } T mt; TailConst!(T) tt; const(T) ct; // new mt = new T; // OK tt = new TailConst!(T); // OK ct = new const(T); // ERROR // reading int n; n = mt.get; // OK n = tt.get; // OK n = ct.get; // OK // writing mt.set(1); // OK tt.set(1); // ERROR ct.set(1); // ERROR I think that wrapping it in a struct will work, providing we have alias this. (Right now, we don't, so that didn't occur to me). As in struct tailConst(T) { const(T) __this; alias __this this; } but without the alias this, you'd have to explicitly provide an extra level of dereferencing. So yeah! Give us alias this and that will work just fine. Problem solved!
Dec 07 2007
prev sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On Dec 7, 2007 3:12 PM, Craig Black <cblack ara.com> wrote:
 What I don't understand is
 why.

 const(C) c;

 Means that the reference is constant.

Look at it another way. You'd want const T t; t = something; to fail, always - regardless of the type of T. It has to be true when T is a class, because it has to be true for everything. Remember, the two new (vastly simplified, but brilliant) rules are: (1) const type identifer; means the same thing as const(type) identifier; (2) Everything inside the brackets is const
Dec 07 2007