templating opEquals/opCmp (e.g. for DSL/expression templates)

Nicholas Wilson iamthewilsonator at hotmail.com
Wed Feb 13 01:50:21 UTC 2019


On Wednesday, 13 February 2019 at 00:56:48 UTC, H. S. Teoh wrote:
> Frankly, I think it's a very good thing that in D comparison 
> operators are confined to opEquals/opCmp.

So do I. Many more objects have partial ordering than arithmetic, 
having opCmp under opBinary would be annoying.

> If anything, I would vote for enforcing opEquals to return bool 
> and bool only.

That would be a backwards incompatible change, like it or not.

> The reason for this is readability and maintainability.  
> Symbols like <= or == should mean one and only one thing in the 
> language, and should not be subject to random overloaded 
> interpretations. Being built-in operators, they are used 
> universally in the language, and any inconsistency in semantics 
> hurts readability, comprehension, and maintainability, such as 
> C++'s free-for-all operator overloading, where any piece of 
> syntax can have wildly-divergent interpretations (even 
> completely different parse trees) depending on what came 
> before.  For example, recently I came up with a C++ monstrosity 
> where the lines:
>
>         fun<A, B>(a, b);
>         gun<T, U>(a, b);
>
> have wildly-different, completely unrelated *parse trees*, as 
> an illustration of how unreadable C++ can become.
>
> Yes, it's extremely flexible, yes it's extremely powerful and 
> can express literally *whatever* you want it to express. It's 
> also completely unreadable and unmaintainable, because the 
> surface structure of the code text becomes completely detached 
> from the actual underlying semantics. I don't even want to 
> imagine what debugging such kind of code must be like.

Thank goodness we use ! for template and don't have `,` available 
for overloading!

> Operator overloading should be reserved for objects that behave 
> like arithmetical entities in some way.

Like symbolic math.

> And especially comparison operators should not have any other 
> meaning than the standard meaning.  It should be illegal to 
> make == and <= mean something completely unrelated to each 
> other.

They do already mean something completely different, <= is an 
ordering, == is equality. Yes it would be bad for (a <= b) == (a 
== b) to be false. I'm sure you could already achieve that 
outcome, would you though? Of course not, it'd be stupid.

> If you need to redefine comparison operators, what you want is 
> really a DSL wherein you can define operators to mean whatever 
> you want.

Yes.

> The usual invocation of such a DSL as a compile-time argument, 
> say something like this:
>
> 	myDsl!'a <= b'
>
> contains one often overlooked, but very important element: the 
> identifier `myDsl`, that sets it apart from other uses of `<=`, 
> and clearly identifies which interpretation should be ascribed 
> to the symbols found in the template argument.

Its also much uglier and does not commute with things that use <= 
i.e. generic functions.

> See, the thing is, when you see a random expression with 
> arithmetical operators in it, the expected semantics is the 
> computation of some kind of arithmetic objects producing an 
> arithmetical result -- because that's what such expressions 
> mean in general, in the language.  It's abusive to overload 
> that to mean something else entirely -- because there is no 
> warning sign to the reader of the code that something different 
> is happening.

Yes thats the use/abuse distinction, see my other post.

When you see a line like:
>
>         fun<A, B>(a, b);
>
> and then somewhere else a line like:
>
>         gun<T, U>(a, b);
>
> the actual behaviour of the code should be similar enough that 
> you can correctly guess the semantics.  It should not be that 
> the first line instantiates and calls a template function, 
> whereas the second is evaluated as an expression with a bunch 
> of overloaded comma and comparison operators.

Indeed! That is not what is being proposed at all!

> Similarly, it should not be the case that:
>
> 	auto x = a <= b;
>
> evaluates a comparison expression and assigns a boolean value 
> to x, whereas:
>
> 	auto y = p <= q;
>
> creates an expression object capturing p and q, that needs to 
> be called later before it yields a boolean value.

No. auto y = p <= q; should not e.g. open a socket (you could 
probably do that already with an impure opCmp). Being able to 
capture the expression `p <= q` is the _entire point_ of the 
proposal.

> With a string DSL, that little identifier `myDsl` (or whatever 
> identifier you choose for this purpose) serves as a cue to the 
> reader of the code that something special is happening here.  
> For example:
>
> 	auto y = deferred!`p <= q`;
>
> immediately tells the reader of the code that the <= is to be 
> understood with a different meaning than the usual <= operator.

Its also much uglier and does not commute with things that use <= 
i.e. generic functions.

> Just as an expression like:
>
> 	auto dg = (p, q) => p <= q;
>
> by virtue of its different syntax tells the reader that the 
> expression `p <= q` isn't being evaluated here and now, as it 
> otherwise would be.

Can't do symbolic computation with that.

> The presence of such visual cues is good, and is the way things 
> should be done.
>
> It should not be that something that looks like an expression, 
> evaluated here and now, should turn out to do something else. 
> That kind of free-for-all, can-mean-literally-anything 
> semantics makes code unreadable, unmaintainable, and a ripe 
> breeding ground for bugs -- someone (i.e., yourself after 3 
> months) will inevitably forget (or not know) the special 
> meaning of <= in this particular context and write wrong code 
> for it.

Type autocompletion will tell you the result of  p <= q; which at 
that point if it is still unclear you have bigger problems. In 
generic code you have no choice but to assume that p <= q; is a 
comparison, if someone is using that with a symbolic engine then 
the meaning doesn't change.

> P.S. And as a bonus, a string DSL gives you the freedom to 
> employ operators not found among the built-in D operators, for 
> example:
>
> 	auto result = eval!`(f∘g)(√x ± √y)`;
>
> And if you feel the usual strings literals are too cumbersome 
> to use for long expressions, there's always the under-used 
> token strings to the rescue:
>
> 	auto result = eval!q{
> 		( (f ∘ g)(√(x + y) ± √(x - y)) ) / |x|·|y| +
> 		2.0 * ∫ f(x)·dx
> 	};
>
> The `eval` tells the reader that something special is happening 
> here, and also provides a name by which the reader can search 
> for the definition of the template that processes this 
> expression, and thereby learn what it means.
>
> Without this little identifier `eval`, it would be anyone's 
> guess as to what the code is trying to do.

I would expect it the compute the tuple
(f(g(sqrt(x+y)) + sqrt(x-y)/(abs(x).dot(abs(y)) + 2*integrate(f),
(f(g(sqrt(x+y)) - sqrt(x+y)/(abs(x).dot(abs(y)) + 2*integrate(f)

(you missed the bounds on the integral and x is ambiguous in the 
integral)

What else would I think it would do? If that guess is wrong then 
the person has abused the operators, if its correct that thats a 
win. I'd bet money you could do just that in Julia. I'm not 
suggesting we go that far but they would definitely consider that 
a feature.

> Throw in C++-style SFINAE and Koenig lookup, and what ought to 
> be a 10-second source tree search for an identifier easily 
> turns into a 6-hour hair-pulling session of trying to 
> understand exactly which obscure rules the C++ compiler applied 
> to resolve those operators to which symbol(s) defined in which 
> obscure files buried deep in the source tree.

Its a good thing we don't have SFINAE and Koenig lookup.



More information about the Digitalmars-d mailing list