Life, Liberty, and @Property: Three Proposals and Their Tradeoffs

Zach the Mystic reachBUTMINUSTHISzach at gOOGLYmail.com
Wed Feb 27 13:55:32 PST 2013


Sheesh, these forums are busy! I hope you have time for this one.
Please let me know if a DIP is in order...

I present to you a Philosophical Treatise from the mystical 
corner, wherein I examine three property proposals for their 
strengths and weaknesses. (This is an article-length Treatise.)

The Feature:
Properties as proposed in DIP23:
http://wiki.dlang.org/DIP23

This proposal will be familiar to most readers of this forum. It 
allows a function tagged with '@property' to act as a getter, by 
banishing the use of parentheses on the called function, or a 
setter, by allowing the arguments to appear on the right-hand 
side of an equals sign, depending on the number of arguments in 
the signature.

Two related features are discussed in the DIP, of which I only 
need mention optional parentheses. Assuming they are here to 
stay, they make attaching '@property' to a getter far less 
valuable. It tightens up the code, by forcing you to do what you 
could already do anyway, but frankly, is massively ugly for such 
a limited benefit. '@property' attached to a setter is of 
slightly more benefit, but is still not very attractive to look 
at.


Advantages:
1) It's already here. That means less work for developers and 
fewer things to learn for seasoned users of the language.
2) It provides a basic answer to the question of how to make 
functions look like variables, and is therefore better than no 
answer at all.

Downsides:
1) It's incredibly ugly.
2) It's extremely rigid.
3) It completely ignores the fact that you might want to do more 
with pseudo-variables than getting them and setting them.

Let's move on.

The Feature:
Replace '@property' with '@get' when it's a getter and '@set' 
when it's a setter.

I have selected to use as examples a few properties from druntime 
(object_.d lines 1629-50). Shortened for presentation purposes, 
they currently look like this:

@property bool isNew() { return (n.flags & MInew) != 0; }

@property uint index() { return isNew ? n.index : o.index; }
@property void index(uint i) { if (isNew) n.index = i; else.... }

@property uint flags() { return isNew ? n.flags : o.flags; }
@property void flags(uint f) { if (isNew) n.flags = f; else.... }

@property void function() tlsctor() nothrow pure
{
     .......
}

With '@get' and '@set' syntax, the above become:

@get bool isNew() { return (n.flags & MInew) != 0; }

@get uint index() { return isNew ? n.index : o.index; }
@set void index(uint i) { if (isNew) n.index = i; else.... }

@get uint flags() { return isNew ? n.flags : o.flags; }
@set void flags(uint f) { if (isNew) n.flags = f; else.... }

@get void function() tlsctor() nothrow pure
{
     .......
}

Advantages:
1) It's not ugly. In fact, it's quite dashing, and it no longer 
seems like too high a price to pay for the power it grants.
2) You can tell right away whether you're dealing with a getter 
or a setter, and so can the compiler.

Disadvantages:
1) People who want to upgrade will have to start distinguishing 
between getters and setters.
2) It's no more powerful than the first proposal.

I don't mind this proposal. If D wanted to go with a simple 
property implementation, this is the one I would recommend. Since 
my next proposal is far, far more powerful and sophisticated, and 
since I like this one better than the first, I think it makes 
sense to compare the powerful one to this one instead of to 
DIP23. Let's do it.

The Feature:
The greater part of this feature has already been introduced in 
an article posted on Feb. 5th of this year on this newsgroup, 
entitled "The Atom Consists of Protons, Neutrons, and Electrons".

http://forum.dlang.org/thread/ririagrqecshjljcdubd@forum.dlang.org

For the rest of this article, I will call the proposal described 
there "Enhanced Structs" which are summarized as follows:

1) Structs can now be defined quickly and easily as single 
instances of a type which is hidden from the programmer.

2) Non-static structs nested in other structs or classes now have 
access to their parents' members via a feature I've been thinking 
of calling "static polytypism", or "statically polytypic member 
functions". They incur no performance overhead, operate entirely 
on a pay-for-what-you-use basis, and the expected code breakage 
for existing projects is negligible. The net effect is that 
'static struct' now means the same thing nested in structs that 
it already means in functions. Ordinary structs are now much more 
powerful.

3) 'opGet' is added to the list of operator overloads, performing 
the same service for struct instances as '@property' currently 
does for getters - banishing the use of parentheses. 'opGet' may 
be able to precede 'alias this' when matching.

These Enhanced Structs kind of came out of nowhere. Well, rather 
out of my frustration at knowing how powerful D structs were for 
their own data and wishing that power could be utilized for 
properties too. As they stand, I feel like I've come a long way 
with them, but they're still not perfect. But I'd like to detour 
for a moment before suggesting a final adjustment, to describe 
another advantage of "statically polytypic" structs which has 
nothing to do with properties.

Say you're making your first game and you decide to include a 
dragon on a whim:

module dragon;
private int anger = 10;
Lungs lungs;
struct Lungs
{
     int temperature = 3500;
     void scorch()
     {
         temperature -= 500;
         anger -= 100;
     }
}
void rage()
{
     anger += 50;
     if (anger >= 150) { lungs.scorch; }
}

Let's assume you have good reason for implementing the dragon as 
above, and that the game sells well. Now you need to make a 
sequel. With polytypic structs, you can put your module-level 
code straight into a struct:

Dragon gold, silver, blue, green, black, white, red;
struct Dragon
{
     int anger = 10;
     Lungs lungs;
     struct Lungs
     {
         int temperature = 3500;
         void scorch()
         {
             temperature -= 500;
             anger -= 100;
         }
     }
     void rage()
     {
         anger += 50;
         if (anger >= 150) { lungs.scorch; }
     }
}

Above, struct Lungs still has access to int anger, even though 
it's nested. To refactor the same code without this feature, 
you'd have to move scorch()'s anger decrease into the 
higher-level rage() function. I believe prototyping at module 
level is made more inviting by polytypic structs.

Okay, back to properties. The one remaining weakness with using 
Enhanced Structs as properties is the syntactic overhead they 
incur for the simplest and most common type of property 
definition. As it stands, Enhanced Structs would make the example 
code from "object_.d" look like this:

isNew struct { bool opGet() { return (n.flags & MInew) != 0; } }

index struct
{
     uint opGet() { return isNew ? n.index : o.index; }
     void opSet(uint i) { if (isNew) n.index = i; else.... }
}

flags struct
{
     uint opGet() { return isNew ? n.flags : o.flags; }
     void opSet(uint f) { if (isNew) n.flags = f; else.... }
}

tlsctor struct
{
     void function() opGet() nothrow pure
     {
         .......
     }
}

Compare to that last one:

@get void function() tlsctor() nothrow pure
{
     .......
}

When a property has more than one overload, it makes sense for it 
to have its own indented namespace. Indeed, it's an encapsulation 
advantage, forcing all overloads to be defined in one place. But 
for a one-function property, it's overkill.

Oh, and what does 'opSet' mean, you may ask. Simple, I'm using it 
as an alias of 'opAssign'. It looks better than 'opAssign', and 
is worth adding, if 'opGet' is worth adding, 'opOpSet', 
'opIndexSet', etc.

I want the best of every world I inhabit, if you couldn't tell by 
now. '@get' and '@set' are so attractive to me for simple 
properties that I want to use them for my own.

What if I try letting them function as syntactic sugar for their 
Enhanced Struct counterparts, letting:

@get uint index() { return isNew ? n.index : o.index; }
@set void index(uint i) { if (isNew) n.index = i; else.... }

...lower to:

index struct
{
     uint opGet() { return isNew ? n.index : o.index; }
     void opSet(uint i) { if (isNew) n.index = i; else.... }
}

Well, that's great, but it makes it tricky for the compiler to 
track what to put in the struct. And since you're losing the 
encapsulation advantage mentioned above by having two independent 
functions, I'd rather the compiler issue an error when you 
attempt to overload any function already defined with either 
'@get' or '@set'. Except that you don't really need '@set' 
anymore if that's the case. Just use a struct, like you would 
with any property which had multiple overloads. '@get' is the 
only thing really worth adding to this already powerful proposal.

Using Enhanced Structs Plus @Get, the example becomes:

@get bool isNew() { return (n.flags & MInew) != 0; }

index struct
{
     uint opGet() { return isNew ? n.index : o.index; }
     void opSet(uint i) { if (isNew) n.index = i; else.... }
}

flags struct
{
     uint opGet() { return isNew ? n.flags : o.flags; }
     void opSet(uint f) { if (isNew) n.flags = f; else.... }
}

@get void function() tlsctor() nothrow pure
{
     .......
}

Just to remind you, the code is currently implemented as follows:

@property bool isNew() { return (n.flags & MInew) != 0; }

@property uint index() { return isNew ? n.index : o.index; }
@property void index(uint i) { if (isNew) n.index = i; else.... }

@property uint flags() { return isNew ? n.flags : o.flags; }
@property void flags(uint f) { if (isNew) n.flags = f; else.... }

@property void function() tlsctor() nothrow pure
{
     .......
}

Let's analyze.

Advantages:
1) Looks better
2) It forces encapsulation
3) Is extremely flexible and powerful, including: allowing all 
existing struct operator overloads, allowing the property access 
to its parent's parents, allowing the property to have its own 
properties and even to store data. Did I mention all existing 
struct operator overloads?
4) Existing uses of '@property' are unharmed and unaffected
5) This implementation would be unique to D (see Downside 2 below)
6) Non-static nested structs would gain access to their parents' 
members with no performance overhead in structs or classes.
7) Module-level declarations can be refactored into structs with 
fewer problems.
8) 'static struct' would actually mean something outside of 
function definitions.
9) "Static polytypism" might actually be transferable to existing 
nested classes, boosting their performance.
10) Structs may be defined with a single instance using a great 
new syntax which is easy to write, read, and convert.
11) 'opGet' inside a struct saves you an 'alias this' 
declaration, and has a more precise precedence than 'alias this' 
does.
12) 'opSet' and its relatives, 'opOpSet', 'opIndexSet', etc., are 
a justifiable addition, given 'opGet', and look better than 
'opAssign', 'opOpAssign', etc.
13) '@set' is unnecessary.

Downsides:
1) Change. Changes causes fear, which must be confronted.
2) Risk. Both single-instance structs and statically polytypic 
functions are untested features without track records (conflict 
tradeoff with Advantage 5).
3) Work. Implementing these things will not be trivial.
4) Error messages. They may need adjusting even after the 
implementation itself is solid.
5) Forcing people to define properties which are not simple 
getters inside a struct might annoy some people (tradeoff with 
Advantages 2 and 13 above).
6) 'opSet' as an alias to 'opAssign' could cause confusion. And 
while I personally think the option is worth the cost, 
deprecating 'opAssign' would trigger massive flooding and violent 
storms, sending the world into a new dark age (tradeoff Advantage 
12).


More information about the Digitalmars-d mailing list