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