www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Yet Another Const Proposal

reply =?ISO-8859-15?Q?S=F6nke_Ludwig?= writes:
(WARNING: LONG POST AHEAD)

After numerous suggested const-designs, of which none was fully
convincing, I'd like to make another suggestion.
First the latest suggestion made by Walter:


  =======================
  ==  latest proposal  ==
  =======================


   head       tail        decl                  meaning
--------------------------------------------------------------------------------
1 mutable    mutable     T t               t fully modifyable
2 mutable    const       TailConst!(T)
3 mutable    invariant   TailInvariant!(T)
4 const      mutable     --
5 const      const       const(T) t        t not reassignable, members const
6 const      invariant   --
7 invariant  mutable     --
8 invariant  const       --
9 invariant  invariant   invariant(T) t    t not reassignable, members invariant

with const T <=> const(T)

This system, while having a simple and consistent syntax, is basically also
really limited in its abilities. The important cases (2) and (3) are actually
not possible to do transparently _today_. Also (IMO) these cases are so basic
that they deserve to be possible in the core language.

Also, an important observation:

----
class C {}
struct S {
	invariant(C) inst;
}
S s = S(new C);
invariant(C)* pc = &s.inst;
s = S(null);
assert( *pc != null ); // fails
   // *pc is null now, despite beeing invariant(C)*, which should
   // mean, that the memory pointed to by pc will never change!
----

So to make the system semantically consistent, you'd have to forbid structure
assignments, if there are any invariant members. This in turn makes the
TailInvariant!()-solution impossible without severly hacking around the type
system.




  ====================
  ==  new proposal  ==
  ====================

My solution now would use basically the same syntax, as in 2.008 - but
instead of specifying a storage class with 'const X', specify head-constness.
Transitivity still holds: if X is head const, it is also tail const:
   const T <=> const const(T)
The cosmetic problem of const T not beeing equivalent to const(T) remains,
of course.


   head       tail        decl                  meaning
--------------------------------------------------------------------------------
1 mutable    mutable     T t                   t fully modifyable
2 mutable    const       const(T) t            assignable, tail const
3 mutable    invariant   invariant(T) t        assignable, tail invariant
4 const      mutable     --
5 const      const       const T t             head const, tail const
6 const      invariant   const invariant(T) t  head const, tail invariant
7 invariant  mutable     --
8 invariant  const       --
9 invariant  invariant   invariant T t         head invariant, tail invariant

[case (6) is there for completeness, but normally (9) would be used instead]

So this gives at least a semantically consistent system. But of course you still
have to learn the difference between 'const C' and 'const(C)'.


Some usage examples:

Class/Struct
============

S s:
	s = t; // ok
	s.x = 1; // ok
	typeof(&s): S*
	
const(S) s:
	s = t; // ok
	s.x = 1; // error
	typeof(&s): const(S)*
	
const S:
	s = t; // error
	s.x = 1; // error
	typeof(&s): (const S)*

Primitives
==========

Same as structs/classes, just without the member access.

int i:
	i = 1; // ok
	i++; // ok
	typeof(&i): int*
	
const(int) i:
	i = 1; // ok
	i++; // ok, handled as reassignment (debatable)
	typeof(&i): const(int)*
	
const int i:
	i = 1; // error
	i++; // error
	typeof(&i): (const int)*

Array
=====

T[] a:
	a = b; // ok
	a ~= t; // ok
	a[0] = t; // ok
	a[0].x = 1; // ok
	typeof(a[0]): T
	foreach( ref x; a ): typeof(x): T
	
const(T)[] a:
	a = b; // ok
	a ~= t; // ok
	a[0] = t; // ok
	a[0].x = 1; // error
	typeof(a[0]): const(T)
	foreach( ref x; a ): typeof(x): const(T)
	
(const T)[]:
	a = b; // ok
	a ~= t; // ok
	a[0] = t; // error
	a[0].x = 1; // error
	typeof(a[0]): const T
	foreach( ref x; a ): typeof(x): const T
	
const(T[]) a:
	a = b; // ok
	a ~= t; // error
	a[0] = t; // error
	a[0].x = 1; // error
	typeof(a[0]): const T
	foreach( ref x; a ): typeof(x): const T
	
const T[]:
	a = b; // error
	a ~= t; // error
	a[0] = t; // error
	a[0].x = 1; // error
	typeof(a[0]): const T
	foreach( ref x; a ): typeof(x): const T


Reference parameters
====================

These are exactly the same, as non reference parameters:

ref S s:
	s = t; // ok
	s.x = 1; // ok
	typeof(s): S
	typeof(&s): S*
ref const(S) s:
	s = t; // ok
	s.x = 1; // ok
	typeof(s): const(S)
	typeof(&s): const(S)*
	
ref const S s:
	s = t; // ok
	s.x = 1; // ok
	typeof(s): const S
	typeof(&s): (const S)*



Grammar
=======

Just to see if there might be any ambiguities I've also made a matching grammar.
When building a type, invariant always transitively overrides const - but not
the other way around.

TypeDecl
     -> HeadConstTypeDecl

HeadConstTypeDecl
     -> ConstTypeDecl
     -> const NonConstTypeDecl
     -> invariant NonConstTypeDecl

ConstTypeDecl
     -> NonConstTypeDecl
     -> const(NonConstTypeDecl)
     -> invariant(NonConstTypeDecl)

NonConstTypeDecl
     -> PrimitiveTypeDecl
     -> PrimitiveTypeDecl[]
     -> (HeadConstTypeDecl)[]
     -> PrimitiveTypeDecl*
     -> (HeadConstTypeDecl)*

PrimitiveTypeDecl:
     -> void | int | ...
     -> Identifier


Of course, some redundant declarations are possible, not sure if
this is a problem (it's acually neccessary to allow this on a semantic level
to transparently work with aliases):

const (const (const T)*)* <=> const T**


I'd be glad to hear any suggestions or mistakes I may have made.

Sönke
Dec 07 2007
next sibling parent reply "Janice Caron" <caron800 googlemail.com> writes:
On Dec 7, 2007 3:20 PM, Sönke Ludwig
<ludwig informatik_dot_uni-luebeck.de> wrote:
 assert( *pc != null ); // fails

Shouldn't that be assert(*pc !is null) ? ( *pc != null ) will attempt to call (*pc).opCmp(), and will fall over if (*pc) is null.
Dec 07 2007
parent =?ISO-8859-1?Q?S=F6nke_Ludwig?= writes:
Janice Caron schrieb:
 On Dec 7, 2007 3:20 PM, Sönke Ludwig
 <ludwig informatik_dot_uni-luebeck.de> wrote:
 assert( *pc != null ); // fails

Shouldn't that be assert(*pc !is null) ? ( *pc != null ) will attempt to call (*pc).opCmp(), and will fall over if (*pc) is null.

Of course! Should have checked the code before posting..
Dec 07 2007
prev sibling next sibling parent =?ISO-8859-15?Q?S=F6nke_Ludwig?= writes:
I should add what the intention was for going this direction. The main benefit 
is that this method is backwards compatible to the old system and to the 2.008 
system and does not introduce breaking changes.
Dec 07 2007
prev sibling next sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On 12/7/07, Sönke Ludwig <ludwig informatik_dot_uni-luebeck.de> wrote:
 Also, an important observation:

 So to make the system semantically consistent, you'd have to forbid structure
 assignments, if there are any invariant members.

He's got a point. Here's a more drastic example. class A { invariant(char)[5] a; } A a; a = A("hello"); string s = a.a; writefln(s); // prints "hello" a = A("world"); writefln(s); // prints "world" Whoops! The assignment of a seems to modified s ... an invariant! I haven't actually tried to compile this, so it might not work. (Anyone want to try it?). But Sönke's rule is sound: if a struct contains an invariant (or, I would add, const) member, then assigning the struct should be ruled out.
Dec 07 2007
prev sibling next sibling parent reply "Janice Caron" <caron800 googlemail.com> writes:
On 12/7/07, Sönke Ludwig <ludwig informatik_dot_uni-luebeck.de> wrote:
 I'd be glad to hear any suggestions or mistakes I may have made.

I have no suggestions, and haven't spotted any mistakes. In fact, good catch on the invariant overwriting problem! But I have to ask - what's the problem with Walter's latest suggestion? It seems absolutely perfect to me. I can't fault it. What do think it doesn't do, that we need?
Dec 07 2007
parent =?ISO-8859-1?Q?S=F6nke_Ludwig?= writes:
Janice Caron wrote:
 On 12/7/07, Sönke Ludwig <ludwig informatik_dot_uni-luebeck.de> wrote:
 I'd be glad to hear any suggestions or mistakes I may have made.

I have no suggestions, and haven't spotted any mistakes. In fact, good catch on the invariant overwriting problem! But I have to ask - what's the problem with Walter's latest suggestion? It seems absolutely perfect to me. I can't fault it. What do think it doesn't do, that we need?

My main (only?) concern are tail-const classes. If they had a transparent syntax, I wouldn't care for them to be a special case. But using a template makes it feel strange - but that can probably be resolved using macros [TailConst!(X) -> tailconst(X)]. Another problem with templates is, that it is currently not possible to transparently wrap a type in a struct. But that should be resolved at some point, so it should only be a temporary problem. The second thing is that I think from a language point of view it's unclean to not be able to provide such a typing-feature without bypassing the type system (casting away the const/invariantness at some point). However, if only point 2 remains, I think, from a users point of view, such a system would probably be as clean as it would get. So actually, I'm not really sure which one I would prefer right now <g>.
Dec 07 2007
prev sibling next sibling parent Walter Bright <newshound1 digitalmars.com> writes:
Sönke Ludwig wrote:
 Also, an important observation:
 
 ----
 class C {}
 struct S {
     invariant(C) inst;
 }
 S s = S(new C);
 invariant(C)* pc = &s.inst;
 s = S(null);
 assert( *pc != null ); // fails
   // *pc is null now, despite beeing invariant(C)*, which should
   // mean, that the memory pointed to by pc will never change!
 ----
 
 So to make the system semantically consistent, you'd have to forbid 
 structure
 assignments, if there are any invariant members. This in turn makes the
 TailInvariant!()-solution impossible without severly hacking around the 
 type
 system.

I hadn't thought of that. Good catch.
Dec 08 2007
prev sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On 12/7/07, Sönke Ludwig <ludwig informatik_dot_uni-luebeck.de> wrote:
 This in turn makes the
 TailInvariant!()-solution impossible without severly hacking around the type
 system.

Yes, I had it in my head that TailConst! and TailInvariant! would be impossible without casting away const somewhere along the line. I got temporarily distracted when Walter said it could be done without casting away const by wrapping the class in a struct. I should have spotted then that const-correctness was being violated, but I wasn't paying enough attention. I don't necessarily think this is a problem though (although overwriting const data obviously /is/), because TailConst! and TailInvariant! would be library-supplied templates, written by people who completely understood what was happening "under the hood", and one would assume that writers such as Walter or Andrei could write them in a "safe" way. (I'm sure I could have a bash myself, for that matter). But yeah - the assignment of structs which contain const or invariant members, is a violation of const correctness, and I believe it needs to be fixed.
Dec 08 2007