www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Generic operator overloading for immutable types?

reply Gary Willoughby <dev nomad.so> writes:
In the following code is there any way to make the `opBinary` 
method generic to be able to accept immutable as well as a 
standard type? The code currently passes the unit test but I 
wonder if I could get rid of the duplication to overload the 
operator? I'm failing badly.


import std.stdio;

struct Rational
{
	public long numerator;
	public long denominator;

	public immutable Rational opBinary(string op)(immutable Rational 
rhs)
	{
		static if (op == "+")
		{
			return Rational(0, 0);
		}
		else
		{
			static assert(0, "Operator '" ~ op ~ "' not implemented");
		}
	}

	public Rational opBinary(string op)(Rational rhs)
	{
		static if (op == "+")
		{
			return Rational(0, 0);
		}
		else
		{
			static assert(0, "Operator '" ~ op ~ "' not implemented");
		}
	}
}

unittest
{
	auto foo = Rational(1, 3);
	auto bar = Rational(1, 6);
	writefln("%s", foo + bar);

	auto baz = immutable Rational(1, 3);
	auto qux = immutable Rational(1, 6);
	writefln("%s", baz + qux);
}
Jun 12 2017
next sibling parent reply "H. S. Teoh via Digitalmars-d-learn" <digitalmars-d-learn puremagic.com> writes:
On Mon, Jun 12, 2017 at 07:38:44PM +0000, Gary Willoughby via
Digitalmars-d-learn wrote:
 In the following code is there any way to make the `opBinary` method
 generic to be able to accept immutable as well as a standard type? The
 code currently passes the unit test but I wonder if I could get rid of
 the duplication to overload the operator? I'm failing badly.
This is what inout was designed for: public inout Rational opBinary(string op)(inout Rational rhs) { static if (op == "+") { return inout(Rational)(0, 0); } else { static assert(0, "Operator '" ~ op ~ "' not implemented"); } } That should do the trick. T -- Frank disagreement binds closer than feigned agreement.
Jun 12 2017
next sibling parent reply Gary Willoughby <dev nomad.so> writes:
I don't know how H. S. Teoh managed to answer 'before' I posted 
but thanks guys! :)
Jun 12 2017
next sibling parent arturg <var.spool.mail700 gmail.com> writes:
On Monday, 12 June 2017 at 19:51:37 UTC, Gary Willoughby wrote:
 I don't know how H. S. Teoh managed to answer 'before' I posted 
 but thanks guys! :)
might be a bug, happened here http://forum.dlang.org/post/ohbr5l$2mng$1 digitalmars.com also.
Jun 12 2017
prev sibling parent Steven Schveighoffer <schveiguy yahoo.com> writes:
On 6/12/17 3:51 PM, Gary Willoughby wrote:
 I don't know how H. S. Teoh managed to answer 'before' I posted but
 thanks guys! :)
D programmers are *that* good. Seriously though, for NNTP connections, timestamp is taken from the submitter's PC. -Steve
Jun 13 2017
prev sibling next sibling parent reply Gary Willoughby <dev nomad.so> writes:
On Monday, 12 June 2017 at 19:36:52 UTC, H. S. Teoh wrote:
 On Mon, Jun 12, 2017 at 07:38:44PM +0000, Gary Willoughby via 
 Digitalmars-d-learn wrote:
 In the following code is there any way to make the `opBinary` 
 method generic to be able to accept immutable as well as a 
 standard type? The code currently passes the unit test but I 
 wonder if I could get rid of the duplication to overload the 
 operator? I'm failing badly.
This is what inout was designed for: public inout Rational opBinary(string op)(inout Rational rhs) { static if (op == "+") { return inout(Rational)(0, 0); } else { static assert(0, "Operator '" ~ op ~ "' not implemented"); } } That should do the trick. T
Quick question about the signature, if I change it to (note the parens): public inout(Rational) opBinary(string op)(inout(Rational) rhs) It no longer works, why is that?
Jun 12 2017
parent reply =?UTF-8?Q?Ali_=c3=87ehreli?= <acehreli yahoo.com> writes:
On 06/12/2017 01:03 PM, Gary Willoughby wrote:
 On Monday, 12 June 2017 at 19:36:52 UTC, H. S. Teoh wrote:
      public inout Rational opBinary(string op)(inout Rational rhs)
 Quick question about the signature, if I change it to (note the parens):

    public inout(Rational) opBinary(string op)(inout(Rational) rhs)

 It no longer works, why is that?
That's frequently faced issue with D. :) In the first declaration, inout applies to the member function. In the second one, it applies only to the return type. Walter has his rationale for doing it that way but it's confusing. Ali
Jun 12 2017
parent reply "H. S. Teoh via Digitalmars-d-learn" <digitalmars-d-learn puremagic.com> writes:
On Mon, Jun 12, 2017 at 01:08:13PM -0700, Ali Çehreli via Digitalmars-d-learn
wrote:
 On 06/12/2017 01:03 PM, Gary Willoughby wrote:
 On Monday, 12 June 2017 at 19:36:52 UTC, H. S. Teoh wrote:
      public inout Rational opBinary(string op)(inout Rational rhs)
 Quick question about the signature, if I change it to (note the parens):

    public inout(Rational) opBinary(string op)(inout(Rational) rhs)

 It no longer works, why is that?
That's frequently faced issue with D. :) In the first declaration, inout applies to the member function.
More precisely, it applies to the `this` reference implicitly passed to the member function. You need inout to apply to `this`, otherwise a.opBinary(b) won't work when a is an immutable instance.
 In the second one, it applies only to the return type. Walter has his
 rationale for doing it that way but it's confusing.
[...] A few years ago we tried lobbying for the language to enforce putting modifiers that only apply to the function itself on the far right side, e.g.: public Rational opBinary(string op)(inout Rational rhs) inout But it was rejected for various reasons. Therefore, nowadays I always recommend writing parenthesis with type modifiers, so that the intent it unambiguous, i.e., always write `inout(Rational)` rather than `inout Rational`, unless you intend for `inout` to apply to the function rather than the return value. (And I apologize for the slip-up in my code example above, where I failed to do this for the function parameter.) T -- Маленькие детки - маленькие бедки.
Jun 12 2017
parent Gary Willoughby <dev nomad.so> writes:
On Monday, 12 June 2017 at 20:10:17 UTC, H. S. Teoh wrote:
 Therefore, nowadays I always recommend writing parenthesis with 
 type modifiers, so that the intent it unambiguous, i.e., always 
 write `inout(Rational)` rather than `inout Rational`, unless 
 you intend for `inout` to apply to the function rather than the 
 return value. (And I apologize for the slip-up in my code 
 example above, where I failed to do this for the function 
 parameter.)


 T
Ok, thanks.
Jun 13 2017
prev sibling parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 6/12/17 3:36 PM, H. S. Teoh via Digitalmars-d-learn wrote:
 On Mon, Jun 12, 2017 at 07:38:44PM +0000, Gary Willoughby via
Digitalmars-d-learn wrote:
 In the following code is there any way to make the `opBinary` method
 generic to be able to accept immutable as well as a standard type? The
 code currently passes the unit test but I wonder if I could get rid of
 the duplication to overload the operator? I'm failing badly.
This is what inout was designed for: public inout Rational opBinary(string op)(inout Rational rhs) { static if (op == "+") { return inout(Rational)(0, 0); } else { static assert(0, "Operator '" ~ op ~ "' not implemented"); } } That should do the trick.
Nope, const works just fine. A clue is in your return type -- it's not inout! This should work: public Rational opBinary(string op)(Rational rhs) const If Rational had any indirections, then inout would be required, and your signature wouldn't work (because you can't convert an inout(SomethingWithPointers) to SomethingWithPointers). Why do I need to make the member function const, but not the parameter? Because the parameter is taken by value, 'this' is a reference. -Steve
Jun 13 2017
parent reply Gary Willoughby <dev nomad.so> writes:
On Tuesday, 13 June 2017 at 11:36:45 UTC, Steven Schveighoffer 
wrote:
 Nope, const works just fine. A clue is in your return type -- 
 it's not inout!

 This should work:

 public Rational opBinary(string op)(Rational rhs) const

 If Rational had any indirections, then inout would be required, 
 and your signature wouldn't work (because you can't convert an 
 inout(SomethingWithPointers) to SomethingWithPointers).

 Why do I need to make the member function const, but not the 
 parameter? Because the parameter is taken by value, 'this' is a 
 reference.

 -Steve
Is it possible for the `result` variable in the following code to be returned as an immutable type if it's created by adding two immutable types? import std.stdio; struct Rational { public long numerator; public long denominator; public inout Rational opBinary(string op)(inout(Rational) other) const { static if (op == "+") { return inout(Rational)(0, 0); } else { static assert(0, "Operator '" ~ op ~ "' not implemented"); } } } unittest { auto baz = immutable(Rational)(1, 3); auto qux = immutable(Rational)(1, 6); auto result = baz + qux; writeln(typeof(result).stringof); // Result is not immutable! }
Jun 13 2017
next sibling parent reply ag0aep6g <anonymous example.com> writes:
On 06/13/2017 09:29 PM, Gary Willoughby wrote:
 Is it possible for the `result` variable in the following code to be 
 returned as an immutable type if it's created by adding two immutable 
 types?
Qualify the return type as `inout`: inout(Rational) opBinary(/*...*/)(/*...*/) inout {/*...*/} (That second `inout` is the same as the first one in your code.) Then you get a mutable/const/immutable result when you call the method on a mutable/const/immutable instance. It doesn't matter a lot, though. `Rational` is a value type, so you can convert the return value between qualifiers as you want, anyway. But it affects what you get with `auto`, of course. [...]
 struct Rational
 {
      public long numerator;
      public long denominator;
 
      public inout Rational opBinary(string op)(inout(Rational) other) const
`inout` and `const` kinda clash here. Both apply to `this`. But apparently, the compiler thinks `inout const` is a thing. No idea how it behaves. I don't think it's a thing in the language. As far as I understand, you should only put one of const/immutable/inout.
Jun 13 2017
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 6/13/17 3:58 PM, ag0aep6g wrote:
 On 06/13/2017 09:29 PM, Gary Willoughby wrote:
 Is it possible for the `result` variable in the following code to be
 returned as an immutable type if it's created by adding two immutable
 types?
Qualify the return type as `inout`: inout(Rational) opBinary(/*...*/)(/*...*/) inout {/*...*/} (That second `inout` is the same as the first one in your code.) Then you get a mutable/const/immutable result when you call the method on a mutable/const/immutable instance. It doesn't matter a lot, though. `Rational` is a value type, so you can convert the return value between qualifiers as you want, anyway. But it affects what you get with `auto`, of course.
Yes exactly. I prefer personally to have value types always return mutable. You can always declare the result to be immutable or const, but declaring mutable is not as easy. e.g.: immutable ReallyLongValueTypeName foo1(); ReallyLongValueTypeName foo2(); version(bad) { auto x = foo1; ReallyLongValueTypeName y = foo1; } version(good) { immutable x = foo2; auto y = foo2; }
 [...]
 struct Rational
 {
      public long numerator;
      public long denominator;

      public inout Rational opBinary(string op)(inout(Rational) other)
 const
`inout` and `const` kinda clash here. Both apply to `this`. But apparently, the compiler thinks `inout const` is a thing. No idea how it behaves. I don't think it's a thing in the language. As far as I understand, you should only put one of const/immutable/inout.
const(inout) actually *is* a thing :) It's a type constructor that can be implicitly cast from immutable. This has advantages in some cases. See (horribly written) table at the bottom if the inout function section here: http://dlang.org/spec/function.html#inout-functions Also I talked about it last year at Dconf 2016: http://dconf.org/2016/talks/schveighoffer.html However, I find it very surprising that inout and const can combine the way you have written it. Even though it does make logical sense. I'd suspect it would be a good candidate for a "you didn't really mean that" error from the compiler, but it might be hard to decipher from the parsed code. -Steve
Jun 13 2017
parent reply ag0aep6g <anonymous example.com> writes:
On 06/13/2017 10:50 PM, Steven Schveighoffer wrote:
 const(inout) actually *is* a thing :)
 
 It's a type constructor that can be implicitly cast from immutable. This 
 has advantages in some cases.
 
 See (horribly written) table at the bottom if the inout function section 
 here: http://dlang.org/spec/function.html#inout-functions
 
 Also I talked about it last year at Dconf 2016: 
 http://dconf.org/2016/talks/schveighoffer.html
Huh. There it is. Took me some experimenting to understand what it does. If anyone else is interested, this is where it clicked for me: ---- inout(int*) f(inout int* x, inout int* y) { return y; } inout(int*) g(inout int* x) { immutable int* y; return f(x, y); /* Error: cannot implicitly convert expression f(x, y) of type inout(const(int*)) to inout(int*) */ } ---- That code can't compile because g's inout return type says that it has to return a mutable result for a mutable argument x. But y is immutable, so that can't work. We see in the error message that `f(x, y)` results in a `inout(const int*)`. We can change g's return type to that (or `auto`) and the code compiles. `inout const` enforces that the result is const or immutable. It cannot be mutable. This raises a question: There is no analogous inout variant that goes the other way (to mutable and const but not immutable), is there? This code doesn't work, and I see no way to make it work: ---- inout(int*) f(inout int* x, inout int* y) { return y; } auto g(inout int* x) { int* y; return f(x, y); } void main() { int* x; int* r1 = g(x); } ----
Jun 13 2017
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 6/13/17 5:58 PM, ag0aep6g wrote:
 On 06/13/2017 10:50 PM, Steven Schveighoffer wrote:
 const(inout) actually *is* a thing :)

 It's a type constructor that can be implicitly cast from immutable.
 This has advantages in some cases.

 See (horribly written) table at the bottom if the inout function
 section here: http://dlang.org/spec/function.html#inout-functions

 Also I talked about it last year at Dconf 2016:
 http://dconf.org/2016/talks/schveighoffer.html
Huh. There it is. Took me some experimenting to understand what it does. If anyone else is interested, this is where it clicked for me: ---- inout(int*) f(inout int* x, inout int* y) { return y; } inout(int*) g(inout int* x) { immutable int* y; return f(x, y); /* Error: cannot implicitly convert expression f(x, y) of type inout(const(int*)) to inout(int*) */ } ---- That code can't compile because g's inout return type says that it has to return a mutable result for a mutable argument x. But y is immutable, so that can't work. We see in the error message that `f(x, y)` results in a `inout(const int*)`. We can change g's return type to that (or `auto`) and the code compiles. `inout const` enforces that the result is const or immutable. It cannot be mutable. This raises a question: There is no analogous inout variant that goes the other way (to mutable and const but not immutable), is there?
No, the fact that immutable implicitly casts to const(inout) is a special property enabled by the knowledge that immutable data can NEVER change, so it's OK to assume it's (at least) const for all references. The same cannot be true of const or mutable.
 This code doesn't work, and I see no way to make it work:

 ----
 inout(int*) f(inout int* x, inout int* y)
 {
     return y;
 }

 auto g(inout int* x)
 {
     int* y;
     return f(x, y);
 }

 void main()
 {
     int* x;
     int* r1 = g(x);
 }
 ----
This cannot work, because g() has no idea what the true mutability of x is. inout is not a template. This is why you can't implicitly cast inout to anything except const, and you can't implicitly cast anything to inout. So it MUST return const(int *) (if you did auto r1 = g(x), typeof(r1) would be const(int *)). -Steve
Jun 13 2017
parent reply ag0aep6g <anonymous example.com> writes:
On 06/14/2017 12:45 AM, Steven Schveighoffer wrote:
 No, the fact that immutable implicitly casts to const(inout) is a 
 special property enabled by the knowledge that immutable data can NEVER 
 change, so it's OK to assume it's (at least) const for all references. 
 The same cannot be true of const or mutable.
In other words: Immutable can't ever become mutable, at most it can become const. In the same vein: Mutable can't ever become immutable, at most it can become const. I don't see the fundamental difference. Mutable and immutable act very much alike. They're just on opposing ends of the scale.
 This code doesn't work, and I see no way to make it work:

 ----
 inout(int*) f(inout int* x, inout int* y)
 {
     return y;
 }

 auto g(inout int* x)
 {
     int* y;
     return f(x, y);
 }

 void main()
 {
     int* x;
     int* r1 = g(x);
 }
 ----
This cannot work, because g() has no idea what the true mutability of x is. inout is not a template. This is why you can't implicitly cast inout to anything except const, and you can't implicitly cast anything to inout.
I don't follow. `inout const` doesn't need a template to do its thing. I'm not sure what you mean about implicit conversions. Obviously, an inout result matches the corresponding inout argument(s). Doesn't matter if that's an implicit conversion or whatever. The goal is something that works just like `inout const`, but switching mutable and immutable. I've realized that my example can be simplified: ---- bool condition; auto f(inout int* x) { immutable int* y; return condition ? x : y; } void main() { const r1 = f(new int); immutable r2 = f(new immutable int); } ---- That's `inout const` at work. Put a mutable or const int* in, you get a const int* out. Put immutable in, you get immutable out. You can't get mutable out, because the function might return y which is immutable, and it cannot become mutable. Now, this code could be made to work: ---- bool condition; auto f(inout int* x) { int* y; /* mutable now */ return condition ? x : y; } void main() { int* r1 = f(new int); const r1 = f(new immutable int); } ---- Mutable in, mutable out. Const in, const out. Immutable in, const out. You can't get immutable out, because y is mutable, and it cannot become immutable. Same principle as above. You never go from mutable to immutable or the other way around, so everything's fine, no? Except, there's no type in D that has this meaning.
Jun 13 2017
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 6/13/17 7:51 PM, ag0aep6g wrote:
 On 06/14/2017 12:45 AM, Steven Schveighoffer wrote:
 No, the fact that immutable implicitly casts to const(inout) is a
 special property enabled by the knowledge that immutable data can
 NEVER change, so it's OK to assume it's (at least) const for all
 references. The same cannot be true of const or mutable.
In other words: Immutable can't ever become mutable, at most it can become const. In the same vein: Mutable can't ever become immutable, at most it can become const. I don't see the fundamental difference. Mutable and immutable act very much alike. They're just on opposing ends of the scale.
The fundamental difference is that const and immutable share a characteristic that mutable doesn't -- you can't mutate the data. The reason const(inout) works is because const(immutable) evaluates to just immutable. const(<mutable>) just evaluates to const. In this way, mutable is less "special" than immutable.
 This cannot work, because g() has no idea what the true mutability of
 x is. inout is not a template. This is why you can't implicitly cast
 inout to anything except const, and you can't implicitly cast anything
 to inout.
I don't follow. `inout const` doesn't need a template to do its thing.
Right, it's because the point at which the inout is "unwrapped" (i.e. at the point of return), it either becomes const or immutable. Inout functions can compile oblivious to how they will be called because the caller can completely determine the type that should be returned. It was the impetus to create inout in the first place -- why generate all these functions that do the same thing, just to change the return type, let the caller figure it out.
 I'm not sure what you mean about implicit conversions. Obviously, an
 inout result matches the corresponding inout argument(s). Doesn't matter
 if that's an implicit conversion or whatever.
There is actually a difference. inout wraps one and exactly one type modifier. The table in that function shows what the modifier is depending on your collection of mutability parameters. If ALL of them are the same, it becomes that same thing. If any are different, you use the table to figure out. Most differences become const, some special cases become const(inout). This special case is solely to allow a function that takes both immutable and inout to potentially return immutable instead of just const.
 Now, this code could be made to work:

 ----
 bool condition;
 auto f(inout int* x)
 {
     int* y; /* mutable now */
     return condition ? x : y;
 }
 void main()
 {
     int* r1 = f(new int);
     const r1 = f(new immutable int);
 }
 ----

 Mutable in, mutable out. Const in, const out. Immutable in, const out.
 You can't get immutable out, because y is mutable, and it cannot become
 immutable. Same principle as above. You never go from mutable to
 immutable or the other way around, so everything's fine, no?
The soundness of the function above seems good, but I don't know how to reason about the return type of f. Because mutable has no type modifier, it's hard to imagine doing this without one. And any time I think about how to define it, it breaks down. I can't imagine const(blah(T)) evaluating to mutable, no matter what blah is called, or how it works. Literally the ONLY place this would be useful is for inout functions, and Andrei pretty much has declared that inout should be completely stricken from Phobos/druntime in favor of templates. I can't imagine any leeway for another type modifier. const(inout) literally was an afterthought observation that we could relax the rules for this one case and get a little more usability. -Steve
Jun 13 2017
parent ag0aep6g <anonymous example.com> writes:
On 06/14/2017 03:47 AM, Steven Schveighoffer wrote:
 The fundamental difference is that const and immutable share a 
 characteristic that mutable doesn't -- you can't mutate the data.
(... through the reference at hand.) const and mutable share this: The data may be mutated from elsewhere. Mutable shares as much with const as immutable does. But it's the other side of const, of course.
 The reason const(inout) works is because const(immutable) evaluates to 
 just immutable.
 
 const(<mutable>) just evaluates to const. In this way, mutable is less 
 "special" than immutable.
Yeah. That makes it impossible to express the proposed type. But it's just because of the immutable > const > mutable progression in D, which makes for arbitrary asymmetries between mutable and immutable. If it were const > mutable = immutable (and if we had a `mutable` keyword), then `inout immutable` would work like today's `inout const`, `inout mutable` would be what I'm talking about, and `inout const` would just be the same as const. That's not going to happen, of course. It would break everything. And I'm sure there are many obvious issues that I'm just ignoring here because the thing won't happen anyway. But it would make `inout foo` more symmetric. [...]
 The soundness of the function above seems good, but I don't know how to 
 reason about the return type of f. Because mutable has no type modifier, 
 it's hard to imagine doing this without one. And any time I think about 
 how to define it, it breaks down. I can't imagine const(blah(T)) 
 evaluating to mutable, no matter what blah is called, or how it works.
 
 Literally the ONLY place this would be useful is for inout functions, 
 and Andrei pretty much has declared that inout should be completely 
 stricken from Phobos/druntime in favor of templates. I can't imagine any 
 leeway for another type modifier.
 
 const(inout) literally was an afterthought observation that we could 
 relax the rules for this one case and get a little more usability.
Ok, I think we're in agreement. The described variant of inout would make sense, but isn't possible in D. It would be very awkward to implement the feature without changing how mutability qualifiers interact. And changing that would be way too disruptive. It's also not at all clear that it would go well with the rest of the language.
Jun 14 2017
prev sibling parent Kagamin <spam here.lot> writes:
On Tuesday, 13 June 2017 at 19:29:26 UTC, Gary Willoughby wrote:
 Is it possible for the `result` variable in the following code 
 to be returned as an immutable type if it's created by adding 
 two immutable types?
Why do you even want that? Such plain data structure is implicitly convertible to any const flavor: https://dpaste.dzfl.pl/c59c4c7131b2
Jun 14 2017
prev sibling parent ketmar <ketmar ketmar.no-ip.org> writes:
Gary Willoughby wrote:

 In the following code is there any way to make the `opBinary` method 
 generic to be able to accept immutable as well as a standard type? The 
 code currently passes the unit test but I wonder if I could get rid of 
 the duplication to overload the operator? I'm failing badly.
public inout(Rational) opBinary(string op)(inout(Rational) rhs) inout { static if (op == "+") { return Rational(0, 0); } else { static assert(0, "Operator '" ~ op ~ "' not implemented"); } }
Jun 12 2017