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