Fixing D's Properties
Chad J
gamerChad at _spamIsBad_gmail.com
Fri Aug 17 08:51:00 PDT 2007
Properties are a blemish of the D language. In the first part of this
post, I'll explain why and try to show how these properties are /not/
good or even better than properties in other languages. Then I'll
suggest some possible solutions. I don't care if the solutions I
suggest are used, but I will be very thankful if this gets fixed somehow.
The first part of this is really for anyone who believes that D's
properties are just dandy. So if you've already been bitten by these,
feel free to skip down to the solution part of the post.
I suppose I should first give some credit to a couple of the strengths
of D properties:
- They do not need any keywords.
- They can be used outside of classes - pretty much anywhere functions
can be used.
When I offer a solution, I'll try to make one that preserves these
advantages.
The following examples are written for D v1.xxx.
They are not tested on D v2.xxx, but may still work.
Here is a list of some of their shortcomings:
-------------------------------------(1)
First, the very common ambiguity:
Does
foo.bar++;
become
foo.bar( foo.bar + 1 );
or
foo.bar() + 1;
??
This is why we can't write things like this:
ubyte[] array = new ubyte[50];
//...
array.length++;
//...
array.length += 10;
// Note how builtin types use properties heavily.
Which means a large set of our beloved shortcut operators are completely
broken on properties.
-------------------------------------(2)
Second, while the above is problematic, it creates yet another problem!
Unlike other languages with properties like C#, D does not allow easy
migration from member fields to properties.
Suppose a programmer have a class like so:
class SomeContainer
{
int data;
}
Someone else using SomeContainer might write code like so:
auto s = new SomeContainer;
s.data++;
Now the programmer does a slight modification to SomeContainer, one that
he does not expect to break reverse compatibility:
class SomeContainer
{
private int m_data;
int data() { return m_data; }
void data( int value ) { m_data = value; }
}
Now that someone else has a problem, because their code no longer compiles,
since s.data++; is no longer a valid expression.
The ability to write a lightweight interface using fields, and then at
will promote it to a heavyweight interface of properties, is extremely
handy.
-------------------------------------(3)
Third, there is a problem with value types and properties.
Consider the following code:
class Widget
{
Rectangle rect() { return m_rect; }
void rect( Rectangle value ) { m_rect = value; }
private Rectangle m_rect;
}
struct Rectangle
{
int x, y, w, h;
// just a basic opCall "constructor", nothing important
static Rectangle opCall( int x, int y, int w, int h )
{
Rectangle result;
result.x = x; result.y = y;
result.w = w; result.h = h;
return result;
}
}
void main()
{
Widget w = new Widget();
// Insert some default rectangle.
// The private field is assigned to so that you can remove the write
property
// and see that the line with w.rect.x = 10; will still compile, even
// without a write property!
w.m_rect = Rectangle( 0, 0, 80, 20 );
// now comes the fun part...
w.rect.x = 10;
assert( w.rect.x == 10 ); // fails
}
That code seems to be trying to write 10 to w's rect's x field. But it
doesn't.
It does this instead:
//----
Rect temp = w.rect;
temp.x = 10;
//----
It never writes the temporary back into the widget's field as expected.
Also, try doing as the comment in the example suggests: uncomment the
write property, then compile.
Observe how it still compiles, seemingly allowing you to write to a
property that doesn't exist!
-------------------------------------(4)
Fourth, delegates get the shaft.
Consider this:
class Widget
{
private void delegate() m_onClick; // called when a click happens.
void delegate() onClick() { return m_onClick; }
void onClick( void delegate() value ) { m_onClick = value; }
// code that polls input and calls onClick() is absent from this
example.
}
void main()
{
void handler()
{
// handles onClick()
printf( "Click happened!" );
// please forgive the use of printf, I don't know whether you are using
// Tango or Phobos.
}
Widget w = new Widget();
// When any click happens, we want handler to do it's thing.
w.onClick = &handler;
// Now suppose, for whatever reason, we want to emulate a click from
within
// our program.
w.onClick(); // This does not result in handler() being called!
}
The program DOES NOT print "Click happened!" as expected. Instead,
w.onClick expanded to something like this:
void delegate() temp = w.onClick();
That's all it does. It does not call 'temp'. It just leaves it there.
That's because there is yet another ambiguity:
Does
foo.bar();
become
T delegate() temp = foo.bar();
or
T delegate() temp = foo.bar();
temp();
Try replacing w.onClick(); with w.onClick()(); and it should work.
IMO, w.onClick()(); is not an obvious way at all to call that delegate.
-------------------------------------(5)
Fifth, delegates aren't over with yet. This isn't nearly as bad as the
other 4, but it should hit eventually.
Properties can be converted into delegates.
This is shown in the following example code:
struct Point
{
private int m_x = 0, m_y = 0;
int x() { return m_x; }
void x( int value ) { m_x = value; }
int y() { return m_y; }
void y( int value ) { m_y = value; }
}
void main()
{
Point p;
int delegate() readX = &p.x;
p.x = 2;
printf( "%d", readX() ); // prints 2
}
The problem is, what if, for whatever reason, the author of similar code
someday wanted to demote those properties back to fields?
In that case, the user code that relies on the delegate will fail.
If properties were treated EXACTLY like fields, you wouldn't be able to
take their address and call it like a function. You could take their
address, but it would just be an address to a value.
Realizing that this behavior of properties can be beneficial, it makes
sense to show that similar code can be written without properties and
without much added difficulty. If we wanted to make a readX function
like in the above code without properties, we could write something like
this:
struct Point
{
int x,y;
}
void main()
{
Point p;
int getX() { return p.x; }
int delegate() readX = &getX;
p.x = 2;
printf( "%d", readX() ); // prints 2
}
========================================
How to fix it
========================================
The most direct solution seems to be explicit properties. This does not
mean that the current implicit properties need to be removed. The two
can exist together, and the disagreement that may follow is whether the
implicit ones should be deprecated or not. I'm not going to worry about
whether to deprecate implicit properties or not right now.
Here is a table that compares some possible semantics of explicit
properties against the current implicit properties:
<TT>
+----------------+----------+----------+
| properties | explicit | implicit |
+----------------+----------+----------+
| write as field | yes | yes |
+----------------+----------+----------+
| read as field | yes | yes |
+----------------+----------+----------+
| lvalue* | yes | no |
+----------------+----------+----------+
| overridable** | yes | yes |
+----------------+----------+----------+
|callable as func| no | yes |
+----------------+----------+----------+
|use as delegate | no | yes |
+----------------+----------+----------+
| freestanding***| yes | yes |
+----------------+----------+----------+
</TT>
*lvalue: If foo is an lvalue, than foo = 5; should set foo's value to
5, instead of evaluating to 5; and doing nothing. Implicit write
properties fake this to some extent, but do not hold in the general
case. Example (3) above and the foo++; and foo+=10; issues are examples
where lvalueness is needed.
**overridable: This refers to whether or not the property can be
overridden when found inside a class.
***freestanding: The property can be placed anywhere in a file, not
just inside of classes. They can also be static and have attributes
like a function would have.
Another possible change might be to forbid return types for explicit
write properties (they must return void). Then, there should be
well-defined behavior in the D spec for code like this:
foo = bar.x = someValue; // x is an explicit property in bar
This could become
bar.x = someValue;
foo = someValue;
or it could be
bar.x = someValue;
foo = bar.x;
or perhaps something else.
The first case is probably the easiest to understand for people reading
code. In the second case foo's final value depends entirely on what
bar.x does.
Now the only issue is, what should the syntax be for the aforementioned
explicit properties.
Perhaps something like this:
int foo|() { return bar; }
T foo|( int baz ) { bar = baz; }
Which reuses the '|' character. Thus these functions named foo are
explicit properties. Other characters like '.' or '^' could be used as
well. This type of syntax disambiguated templates, it should be able to
do the same for properties.
Another option would be to reuse a keyword, perhaps inout.
inout
{
int foo() { return bar; }
T foo( int baz ) { bar = baz; }
}
which would be equivalent to
inout int foo() { return bar; }
inout T foo( int baz ) { bar = baz; }
Thus 'inout' would be an attribute that enforces explicit propertyness
on a function or method.
I suppose 'in' could be used for write properties and 'out' for read
props, but then the curly braced attribute-has-scope syntax would not
work so well.
At the cost of a keyword, some intuitiveness could be gained. If said
single keyword can be added, then perhaps add the keyword 'property'
with syntax similar to the inout example.
This post is a bit lengthy. So assuming you've read even part of it,
thank you for your time.
- Chad
More information about the Digitalmars-d
mailing list