Yet Another Const Proposal

Sönke Ludwig ludwig at informatik_dot_uni-luebeck.de
Fri Dec 7 07:20:42 PST 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



More information about the Digitalmars-d mailing list