digitalmars.D - Life, Liberty, and Property: Three Proposals and Their Tradeoffs
- Zach the Mystic (280/280) Feb 27 2013 Sheesh, these forums are busy! I hope you have time for this one.
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).
Feb 27 2013