Extended Type Design.
Bruno Medeiros
brunodomedeiros+spam at com.gmail
Sat Mar 17 14:50:51 PDT 2007
Bruno Medeiros wrote:
> What is the status of the experimental designs ...
I asked this because yesterday, out of nowhere, I started thinking about
this problem, and as a kind of a mental exercise I came to a working
design. It seems kinda pointless, since you already made your design,
but I'll show this one nevertheless. Consider it a late entry to the max
challenge :P . Still, there are some aspects presented here, that I
don't how they would work on your planned design (like those keyed by
the questions).
Most of the terms here are tentative, and so is the syntax. Please
consider the syntax separately from the semantics and conceptualization.
Errors may be present in the text. This design is presented as is,
without any warranty of any kind. :P
CONCEPTUALIZATION
There are 3 major kinds of D entities:
Values (expressions), types, and templates. (and labels too I guess...)
Values are characterized by properties that define what one can do with
the value. The most important of these properties is the type. D offers
a very rich mechanism to query and manipulate types (typeof(..), is(..)
expression, auto, type parameters, etc.), much better than any other
statically typed language I know. But the type is not the only
"property" of an expression. There are others, such as whether an
expression is an lvalue or not, if it can be assigned, etc.. The problem
so far is that D does not offer a good mechanism to query and manipulate
such "type properties".
Let's consider the following type properties:
R: is the value readable.
C: is the value readable at compile time (compile time constant).
&: is the value referenciable.
W: is the value writable. (explained later)
I can't find a good term for these "extended type properties" or
"extended value properties" so I'll just call it QUX for now. Silly, I
know, but whatever. And I will call "core type" to the current notion of
type, which is what typeof(..) returns.
So, with these QUX, what are the valid combinations of them?
They are:
R
CR
R&
RW&
W&
(I'm skipping the explanation of why, since I think that's clear, see
examples below). Furthermore, these property combinations are related in
the following hierarchy of conversion:
R
/ \
CR R& W&
\ /
RW&
Examples of the QUX for various values in current D:
42 // CR - A literal is a constant.
var // RW& - As in: int var;
fvar // R& - As in: const fvar; fvar = 2; It's like 'final'
func() // R - the value of a function is readonly and not referenciable
// W - No example in current D for W
Let's give some tentative keywords for the possible QUX combinations:
CR - const
RW& - ref
W& - wronly ref
R& - rdonly ref
R - rdonly
Now, recall the following: QUX describe the properties of a value. The
first thing you may think now is that one can use QUX to declare
variables of the same QUX type. That's not entirely accurate. For
example you can't declare a variable of QUX R, because a var is always
either referenciable, or a (compile-time) constant. There are no R
variables.
Specifying a var as rdonly will create a R&. Specifying no QUX will
create a RW&. Specifying ref in a var declaration will alse create a RW&
var, but the identity will be the same as the var in the given
initializer. ref will preserve the "reference" (memory location) of the
initializer. This is mentioned to clear the declaration semantics.
Examples:
int varA = varX; // var is RW&
const int varB = 1; // var is CR
rdonly int varC = 2; // var is R&
rdonly ref int varD = &varX; // var is RW& too
wronly ref int varE = &varX; // var is W&
varA will be a copy of varX, while varD will be the same as varX (same
identity). After definition, varA and varD will have the same QUX.
And what about the definitions of function parameters, and function
return types?
There are some minor differences. In function return types, if no QUX is
specified, then the QUX is rdonly (as it is currently in D). In function
parameters, if no QUX is specified the QUX is rdonly too (this is
different from current D, but is considered a nice improvement. Function
arguments are almost never modified anyway).
What about composing types? That is, when one has a composite type
(array, pointers, etc.), how does one specify QUX for each of the type
components?
Well for this var:
int[]* var;
then QUX are specified like this:
rdonly int[]* var; // var (the pointer) is rdonly
(rdonly int[])* var; // the pointer target (the array) is rdonly
(rdonly int)[]* var; // the members of the array are rdonly
rdonly (rdonly (rdonly int)[])* var; // All are rdonly
Note that some QUX don't make sense in certain declarations, like
declaring an array member as ref, like this:
(ref int)[] var;
because the members of arrays are refs already. This could be an error
or simply ignored.
What about auto?
auto does not in any way capture the QUX, just the core type (as in
typeof(..) ).
rdonly var;
auto foo = var; // foo is not rdonly
How do we templatize and parameterize QUX?
Let's see by example, looking at previous design challenges:
The id function:
T id(expr T) (T a) {
return a;
}
So, "expr T" denotes that T is not a normal type parameter, but an
"extended type parameter". Besides the core type, it will also hold
information about the QUX. id can be instanced manually or with IFTI.
The max function (challenge #3) will show more advanced scenarios of QUX
manipulation, but let's first recall what max does.
Consider these vars:
a = 3;
b = 9;
const fvar; // fvar is 'final'
fvar = 1;
And now some examples of max usage:
max(1, 2) 2 of QUX CR
max(a, 2) a of QUX R
max(a, b) b of QUX RW&
max(a, func()) a of QUX R
max(a, fvar) a of QUX R&
As requested, max preservers the greatest common QUX information.
Here's how we define max:
maxExtType!(A,B) max(expr A :: rdonly, expr B :: rdonly) (A a, B b) {
if(a >= b)
return a;
else
return b;
}
Of note: The 'T :: U' syntax means specialize the template if T can be
converted to U. This is a variant of the current 'T : U' syntax which
means specialize if T is the *same* as U. In both these constructs, U
can be a QUX, but only if T is an "extended type parameter" (a parameter
declared with expr).
maxExtType is the key to complete the challenge. It defines the maximum
common extended type of A and B. This is defined as:
template maxExtType(expr A, expr B) {
static if( !is(typeof(A) == typeof(B)) ) {
alias maxCoreType(A, B) maxType; // Type cannot be ref
} else {
static if( is(A == ref) && is(B == ref))
alias (ref typeof(A)) maxType;
else static if( is(A :: rdonly ref) && is(B :: rdonly ref))
alias (rdonly ref typeof(A)) maxType;
else static if( is(A :: wronly ref) && is(B :: wronly ref) )
alias (wronly typeof(A)) maxType;
else static if( is(A :: rdonly) && is(B :: rdonly) )
alias (rdonly typeof(A)) maxType;
else
static assert(false, "No common extended type for:"+A+" and "+B);
}
}
So, like mentioned in the original challenge thread, if the core type of
A and B are not the same, then maxExtType cannot be a ref. That's what
the first static if checks for (note: an exception can be made for
object types). The subsequent static ifs check for increasingly less
restrictive common QUXs. It's possible that a common QUX does not exist
if one is R& and the other is W& for example.
What about lazy?
In this design, lazy simply isn't considered as a QUX, as it simply is
not a property of an expression. There are no lazy expressions. After a
lazy FOO variable is created (which must be initialized), the variable
becomes for *all effects* indistinguishable from a FOO delegate() , that
is, a delegate returning type FOO. Thus, lazy can't also be
parameterized/templatized.
IMMUTABILITY
Immutability, as in, "transitive immutability" is achieved as a type
modifier with the keyword "immut". An immut value means that any other
member obtained from the original value cannot be modified, and so on.
The members of immut values are rdonly and immut. immut is not a QUX, it
is a type modifier that modifies (and is part of) the core type. This
means that immut appears in "typeof(..)", and consequentely is also
captured by auto. This is the only sensible behavior, since immut
describes a property of the referenced data of that expression, and must
be preserved upon assignments (and thus part of the core type). This is
unlike QUX, since QUX only describe properties of the immediate-value of
an expression, which is copied in assignments. I.e., you can assign a
rdonly value to a non-rdonly var, but you can't assign an immut value to
a non-immut (normal) var. This shows how immut and QUX are somewhat
different in nature. Also immut vars and not automatically rdonly, they
are rdonly only if 'rdonly' is also specified.
An example:
immut Foo[] fooar;
then:
typeof(fooar[0]) == rdonly immut Foo;
TODO
*Syntax to specify the "this" of a method as immut. Maybe do it like C++?
*A way to conveniently specify/templatize methods that are identical and
only vary in the mutability of it's types (like 'romaybe' in Javari).
The following describes a particular use case for rdonly and wronly:
VARIANT COMPOSITE TYPES.
Consider this hierarchy:
FooBar extends Foo extends Object
Xpto extends Object
Suppose we have an array of Foo:
Foo[] fooarr;
The classic contravariance problem is, is fooarr safely castable to
Object[] ? On first sight one might think yes, since Foo is an Object,
then Foo[] is an Object[]. But that is not the case since an Object[]
array is an array that one can put an Xpto object into:
(cast(Object[]) fooarr)[0] = new Xpto();
which would break type safety, since we would have a Xpto in an array of
Foo's. What happens is that we have some array operations (like readers)
that remain safe , but others do not (like writers).
Java allows that cast, but has runtime checks on array member
assignments, and throws an exception if the type safety is violated like
in the example above.
Can a language provide (compile time) support for safe casting? With
rdonly and wronly it can.
We have that Foo[] cannot be cast to Object[], but it can be safely
cast to (rdonly Object)[]. And then:
fooarr2 = cast((rdonly Object)[]) fooarr;
fooarr2[0] = new Xpto(); // Not allowed
fooarr2[0].doSomething(); // Allowed
Assignments won't be allowed, but reading is allowed. Conversely, the
array type parameter can be contravariantly cast, from Foo[] to (wronly
Foobar)[].
// Ok because fooarr[] is of type wronly FooBar
fooarr[0] = new FooBar();
// Not allowed because you can't read fooarr[0];
fooarr[0].doSomething();
This was the main motivation I saw for the use of wronly, however, this
mechanism is quite simple (as in, simplistic) and limited. It's not as
powerful as Java's generics, which allow a greater degree of
functionality with lower-bounded types. As such, it may not be worth
having wronly just because of this. Still, I guess wronly could also be
used in place of 'out' parameters.
SYNTAX AND TERMS SUBJECT TO CHANGE:
QUX - EVT (Extended Value Properties) or ETP (Extended Type Properties)
? Or 'attributes' instead of 'properties' ? But definitely not "storage
class", that term sucks. :o
immut - perhaps 'immutable'?
rdonly - perhaps 'readonly' or 'final'?
wronly - perhaps 'writeonly'?
expr
"::" - Ideally, it would be better that the ":" of template
specialization would behave the same as the ":" of the is(..) expression.
Comments are welcome.
--
Bruno Medeiros - MSc in CS/E student
http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
More information about the Digitalmars-d
mailing list