Thread overview
Yet Another Const Proposal
Dec 07, 2007
Sönke Ludwig
Dec 07, 2007
Janice Caron
Dec 07, 2007
Sönke Ludwig
Dec 07, 2007
Sönke Ludwig
Dec 07, 2007
Janice Caron
Dec 07, 2007
Janice Caron
Dec 07, 2007
Sönke Ludwig
Dec 08, 2007
Walter Bright
Dec 08, 2007
Janice Caron
December 07, 2007
(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
December 07, 2007
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.

December 07, 2007
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..
December 07, 2007
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.
December 07, 2007
On 12/7/07, Sönke Ludwig <ludwig@informatik_dot_uni-luebeck.de> wrote:
> Also, an important observation:
<snip>
> 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.

December 07, 2007
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?

December 07, 2007
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>.
December 08, 2007
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.
December 08, 2007
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.