Thread overview
relax disabled Final!T unary operators
Mar 23, 2017
Q. Schroll
Mar 24, 2017
H. S. Teoh
Mar 24, 2017
H. S. Teoh
Mar 24, 2017
Jonathan M Davis
Mar 29, 2017
H. S. Teoh
Mar 29, 2017
H. S. Teoh
March 23, 2017
In std.experimental.typecons.Final the operators ++ and -- are disabled. I suspect, this was done with simple types as int in mind, where increment is nothing different from += 1, which is by definition an assignment. From the distant standpoint, there is no reason to disable them at all. They are modifying the object, but calling a modifying method on a Final! struct/class is not disabled either. An operation need not be disabled because it is equivalent to an assignment.
I agree that this behavior is natural. Final!int should not be modifiable at all, that's precisely what immutable int does. This leads to the following conclusion:
Making Final an alias to immutable for types without indirections. They cast implicitly to mutable. E.g. Being allowed to assign the components of Tuple!(int, int), but not the tuple itself is ridiculous because that's the same.
My conclusion: Final only makes sense for things that have indirections.

The things are handled similarly for overloading opAssign for classes. Identity assignment is disallowed to overload, but not other forms, because other forms are just modifying operations. The reason why any form of assignment should be disallowed is simple: It is confusing and we would have to determine which assignments to allow. The Final implementation cannot know which assignment to a struct behaves like modifying. Allowing non-identity class assignment is an option, but I'm against it: It's still an assignment by definition.
March 23, 2017
On Thu, Mar 23, 2017 at 11:30:43PM +0000, Q. Schroll via Digitalmars-d wrote:
> In std.experimental.typecons.Final the operators ++ and -- are disabled. I suspect, this was done with simple types as int in mind, where increment is nothing different from += 1, which is by definition an assignment. From the distant standpoint, there is no reason to disable them at all. They are modifying the object, but calling a modifying method on a Final!  struct/class is not disabled either. An operation need not be disabled because it is equivalent to an assignment.

>From another point of view, though, a class that overloads `++` in a
const way is pretty screwed up in terms of abusing the meaning of `++`. I'm not sure if we should go out of our way to support that kind of abuse of operator overloading!

Not to mention that the compiler automatically translates:

	Object obj;
	++obj;

to the equivalent of:

	{ Object tmp = obj; obj.opUnary!"++"(); return tmp; }

so even if Final allows you to call a const opUnary!"++", the preincrement case may still be rejected due to the rebinding in the above lowering.  Then we'd end up with the asymmetric situation where obj++ is allowed but ++obj is not.


> I agree that this behavior is natural. Final!int should not be modifiable at all, that's precisely what immutable int does. This leads to the following conclusion:
>
> Making Final an alias to immutable for types without indirections. They cast implicitly to mutable. E.g. Being allowed to assign the components of Tuple!(int, int), but not the tuple itself is ridiculous because that's the same.

The same is true for static arrays, which are value types, and thus, if the element type is POD, have no indirections. Yet currently, Final!(int[4]) allows you to modify array elements.


> My conclusion: Final only makes sense for things that have indirections.
[...]

I agree.

The grey area, though, is with structs.  What should be the correct behaviour of Final!S when S is a struct?  Since structs are value types, it could be argued that only reference members of the struct ought to be modifiable, but the struct itself should not be modifiable.  Yet the current implementation allows you to modify struct members freely -- you just can't reassign the entire struct. Which, as you say, is kinda ridiculous because that amounts to essentially the same thing, we're just forcing the user to manually assign struct fields individually instead of having the compiler do it for you. And it's kinda pointless to use Final in this case.

But it's not clear how to implement prohibiting the modification of struct members, e.g.:

	struct S {
		int x;
		void method() { x++; }
	}

	Final!S s;
	s.x++;		// suppose we prohibit this
	s.method();	// then should we still allow this?

I'm tempted to say that Final should only be applied to class and pointer types (because head const, the original charter of Final, doesn't seem to make sense for types that are inherently by-value).  But that would exclude structs that wrap around pointers, e.g., RefCounted, which would seem to be a crippling defect, as we'd *want* to support those kinds of "smart pointer" types.


T

-- 
Latin's a dead language, as dead as can be; it killed off all the Romans, and now it's killing me! -- Schoolboy
March 24, 2017
Another grey area (or more precisely, problematic area) with Final is
dynamic arrays. To wit:

	Final!(int[]) arr;
	arr.length += 1;	// correctly rejected
	arr.length = 10;	// accepted - WAT?
	arr.ptr++;		// correctly rejected
	arr.ptr = null;		// correctly rejected

Bugzilla: https://issues.dlang.org/show_bug.cgi?id=17272

Why is it that .length can be assigned, but arr.ptr cannot? (Note that altering .length may alter .ptr as well, even though direct assignment to .ptr is rejected.)

It's probably an implementation detail of how Final is implemented... but still, this does raise the question: should Final allow changing aggregate members? Dynamic arrays being a special kind of aggregate in that we normally think of their members as the array elements themselves, but in implementation dynamic arrays are slices, which are structs consisting of a pointer and a length. If Final is meant to be head-const, wouldn't the "head" portion be .length and .ptr?  So we ought to reject any attempt to modify them. But on the other hand, if we allow modifying Final struct members, then this is again inconsistent.

All in all, it seems that Final, as currently implemented, really only makes sense for class types. It seems to have glaring holes and inconsistency problems with other types.  (Just wait till I try it on a union... that's gonna throw things off, I'm almost certain.)


T

-- 
Those who've learned LaTeX swear by it. Those who are learning LaTeX swear at it. -- Pete Bleackley
March 24, 2017
On Friday, March 24, 2017 11:00:20 H. S. Teoh via Digitalmars-d wrote:
> All in all, it seems that Final, as currently implemented, really only makes sense for class types. It seems to have glaring holes and inconsistency problems with other types.  (Just wait till I try it on a union... that's gonna throw things off, I'm almost certain.)

Well, final in Java (which is what I assume Final is trying to emulate) is basically head-const. So, if you have a pointer or reference, it would be const, and the rest would be mutable, whereas for types that are not essentially pointers end up being fully const. As such, I would have half-expected Final to just work with pointers and class references, but if it did work with other types, then either it would make anything directly in the type read-only, or it would make the whole type const.

Even trying to emulate Java's semantics, it does get a bit funny, because all aggregate types in Java are reference types. So, something like a struct containing a pointer just isn't something that Java's final has to worry about, and if we're trying to emulate it, we're forced to come up with our own semantics. However, I think that what's most in the spirit of Java's final would be to make everything directly in the struct read-only - which in the case of dynamic arrays (which are essentially a struct) would mean that you could alter the elemens but not ptr or the length (regardless of whether altering the length would alter ptr).

Now, how implementable that is, I don't know (certainly, it sounds like there are currently quite a few problems with what we currently thave), but if we can't do it right, we should probably just restrict Final to classes and pointers.

I don't really have much of a horse in the race though, because I've always thought that Java's final was an utter waste of time. It's making exactly the wrong thing const. If we have it, I'd like to see it implemented in a manner which is not buggy and full of holes, because quality matters, but I don't really think that it was worth adding in the first place.

- Jonathan M Davis

March 29, 2017
On Fri, Mar 24, 2017 at 11:00:20AM -0700, H. S. Teoh via Digitalmars-d wrote:
> (Just wait till I try it on a union... that's gonna throw things off,
> I'm almost certain.)
[...]

And just as I predicted:

	https://issues.dlang.org/show_bug.cgi?id=17284

In a nutshell: the compiler rejects assigning to pointer fields in a union when in @safe code (for obvious reasons), but to work around that, just write Final!U instead, and you can freely assign overlapping pointers in @safe code willy-nilly.

I think Final should be modified so that it outright rejects PODs and unions, at the minimum, if not structs as well.

I think it's a bad idea to have it accept structs and then allow modifying struct members.  This would prohibit things like Final!(RefCounted!T), for example; but if anybody wants to use Final on a RefCounted object, they should perhaps use RefCounted!(Final!T) instead.


T

-- 
Without outlines, life would be pointless.
March 29, 2017
On Wednesday, 29 March 2017 at 17:59:10 UTC, H. S. Teoh wrote:
> On Fri, Mar 24, 2017 at 11:00:20AM -0700, H. S. Teoh via Digitalmars-d wrote:
>> (Just wait till I try it on a union... that's gonna throw things off,
>> I'm almost certain.)
> [...]
>
> And just as I predicted:
>
> 	https://issues.dlang.org/show_bug.cgi?id=17284
>
> In a nutshell: the compiler rejects assigning to pointer fields in a union when in @safe code (for obvious reasons), but to work around that, just write Final!U instead, and you can freely assign overlapping pointers in @safe code willy-nilly.
>
> I think Final should be modified so that it outright rejects PODs and unions, at the minimum, if not structs as well.
>
> I think it's a bad idea to have it accept structs and then allow modifying struct members.  This would prohibit things like Final!(RefCounted!T), for example; but if anybody wants to use Final on a RefCounted object, they should perhaps use RefCounted!(Final!T) instead.
>
>
> T

As I explained in the bug report (https://issues.dlang.org/show_bug.cgi?id=17284) the particular @safe-ty issue has nothing to do with Final. That said, I agree that Final is wrongly designed. In fact I argued that it should reject structs even before the PR was merged, but I didn't have much free time time to defend my position back then: https://github.com/dlang/phobos/pull/3862#issuecomment-199164913

March 29, 2017
On Wed, Mar 29, 2017 at 08:11:11PM +0000, via Digitalmars-d wrote: [...]
> As I explained in the bug report (https://issues.dlang.org/show_bug.cgi?id=17284) the particular @safe-ty issue has nothing to do with Final.

You're right.  It seems that the problem is caused by the compiler forgetting to check for overlapping pointer fields when inferring @safe for template functions.  Which isn't specific to Final.


> That said, I agree that Final is wrongly designed. In fact I argued that it should reject structs even before the PR was merged, but I didn't have much free time time to defend my position back then: https://github.com/dlang/phobos/pull/3862#issuecomment-199164913

While arguments could be made for supporting structs in some way, I think a good first step, since after all this *is* std.experimental, is to add a sig constraint to Final to make it reject everything except pointers, classes, and interfaces.

Then in the meantime we can debate over whether / how to implement Final for other types.


T

-- 
Only boring people get bored. -- JM