www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Operator/concept interoperability

reply "Mason McGill" <mmcgill caltech.edu> writes:
I have a numerical/multimedia library that defines the concept of 
an n-dimensional function sampled on a grid, and operations on 
such grids. `InputGrid`s (analogous to `InputRange`s) can be 
dense or sparse multidimensional arrays, as well the results of 
lazy operations on other grids and/or functions 
(map/reduce/zip/broadcast/repeat/sample/etc.).

UFCS has been extremely beneficial to my API, enabling things 
like this:

   DenseGrid!(float, 2) x = zeros(5, 5);
   auto y = x.map!exp.reduce!max;

without actually defining `map` inside `DenseGrid` or `reduce` 
inside `MapResult`. `map` and `reduce` are defined once, at 
module scope, and work with any `InputGrid`.

As this is numerical code, it would be great to be able to do 
this with operators, as is possible in C++, Julia, and F#:

   auto opUnary(string op, Grid)(Grid g) if (isInputGrid!Grid)
     { /* Enable unary operations for *any* `InputGrid`. */ }

   DenseGrid!(float, 2) x = zeros(5, 5);
   auto y = -x;

This is currently not supported, which means users of my library 
get functions like `map` and `reduce` that work "out of the box" 
for any grids they define, but they need to do extra work to use 
"convenient" operator syntax for NumPy-style elementwise 
operations.

Based on my limited knowledge of DMD internals, I take it this 
behavior is the result of an intentional design decision rather 
than a forced technical one. Can anyone explain the reasoning 
behind it?

Also, does anyone else have an opinion for/against allowing the 
definition of operators that operate on concepts?

Thanks for your time,
-MM
Jun 03 2014
next sibling parent "TheFlyingFiddle" <kurtyan student.chalmers.se> writes:
On Tuesday, 3 June 2014 at 19:55:39 UTC, Mason McGill wrote:
 I have a numerical/multimedia library that defines the concept 
 of an n-dimensional function sampled on a grid, and operations 
 on such grids. `InputGrid`s (analogous to `InputRange`s) can be 
 dense or sparse multidimensional arrays, as well the results of 
 lazy operations on other grids and/or functions 
 (map/reduce/zip/broadcast/repeat/sample/etc.).

 UFCS has been extremely beneficial to my API, enabling things 
 like this:

   DenseGrid!(float, 2) x = zeros(5, 5);
   auto y = x.map!exp.reduce!max;

 without actually defining `map` inside `DenseGrid` or `reduce` 
 inside `MapResult`. `map` and `reduce` are defined once, at 
 module scope, and work with any `InputGrid`.

 As this is numerical code, it would be great to be able to do 
 this with operators, as is possible in C++, Julia, and F#:

   auto opUnary(string op, Grid)(Grid g) if (isInputGrid!Grid)
     { /* Enable unary operations for *any* `InputGrid`. */ }

   DenseGrid!(float, 2) x = zeros(5, 5);
   auto y = -x;

 This is currently not supported, which means users of my 
 library get functions like `map` and `reduce` that work "out of 
 the box" for any grids they define, but they need to do extra 
 work to use "convenient" operator syntax for NumPy-style 
 elementwise operations.

 Based on my limited knowledge of DMD internals, I take it this 
 behavior is the result of an intentional design decision rather 
 than a forced technical one. Can anyone explain the reasoning 
 behind it?

 Also, does anyone else have an opinion for/against allowing the 
 definition of operators that operate on concepts?

 Thanks for your time,
 -MM

 Based on my limited knowledge of DMD internals, I take it this 
 behavior is the result of an intentional design decision rather 
 than a forced technical one. Can anyone explain the reasoning 
 behind it?

Well one reason for this is that unlike methods it is hard to resolve ambiguity between diffrent operator overloads that have been defined in diffrent modules. Example: 2D-vectors //vector.d struct Vector { float x, y; } //cartesian.d Vector opBinary(string op : "+")(ref Vector lhs, ref Vector rhs) { //Code for adding two cartesian vectors. } //polar.d Vector opBinary(string op : "+")(ref Vector lhs, ref Vector rhs) { //Code for adding two polar vectors. } //main.d import polar, vector; void main() { auto a = Vector(2, 5); auto b = Vector(4, 10); auto c = a + b; //What overload should we pick here? //This ambiguity could potentially be resolved like this: auto d = polar.opBinary!"+"(a, b); //But... This defeats the whole purpose of operators. } Side note: You can achieve what you want to do with template mixins. Example: //Something more meaningful here. enum isInputGrid(T) = true; mixin template InputGridOperators() { static if(isInputGrid!(typeof(this))) auto opUnary(string s)() { //Unary implementation } static if(isInputGrid!(typeof(this))) auto opBinary(string s, T)(ref T rhs) if(isInputGrid!(T)) { } //etc. } struct DenseGrid(T, size_t N) { mixin InputGridOperators!(); //Implemtation of dense grid } While this implementation is not as clean as global operator overloading it works today and it makes it very simple to add operators to new types of grids.
Jun 03 2014
prev sibling parent "Mason McGill" <mmcgill caltech.edu> writes:
On Tuesday, 3 June 2014 at 22:05:29 UTC, TheFlyingFiddle wrote:
 Well one reason for this is that unlike methods it is hard to
 resolve ambiguity between diffrent operator overloads that have
 been defined in diffrent modules.

 Example: 2D-vectors
 //vector.d
 struct Vector
 {
     float x, y;
 }

 //cartesian.d
 Vector opBinary(string op : "+")(ref Vector lhs, ref Vector rhs)
 {
    //Code for adding two cartesian vectors.
 }

 //polar.d
 Vector opBinary(string op : "+")(ref Vector lhs, ref Vector rhs)
 {
     //Code for adding two polar vectors.
 }

 //main.d
 import polar, vector;
 void main()
 {
     auto a = Vector(2, 5);
     auto b = Vector(4, 10);

     auto c = a + b; //What overload should we pick here?

     //This ambiguity could potentially be resolved like this:
     auto d = polar.opBinary!"+"(a, b);
     //But... This defeats the whole purpose of operators.
 }

Interesting point. However, I believe this is also the case for ordinary functions used with UFCS: // cartesian.d Vector add(ref Vector lhs, ref Vector rhs) { /* Code for adding two cartesian vectors. */ } // polar.d Vector add(ref Vector lhs, ref Vector rhs) { /* Code for adding two polar vectors. */ } // main.d import polar, vector; void main() { auto a = Vector(2, 5); auto b = Vector(4, 10); auto c = a.add(b); // What overload should we pick here? // The fully qualified form looks quite different, // but it's rare, and intentionally explicit about what's // really going on, so that's OK. auto d = polar.add(a, b); } It strikes me that ambiguous cases are not nearly as common as unambiguous cases, especially if you use selective imports. I believe Julia uses a re-write rule for its operators: the first thing the compiler does when evaluating an operator expression is re-write it as a function call; after that, all the language's resolution rules apply as they would to any other function call. D could easily take this approach, simplifying the language while maintaining backwards-compatibility.
 Side note:

 You can achieve what you want to do with template mixins.

 ...

 While this implementation is not as clean as global operator
 overloading it works today and it makes it very simple to add
 operators to new types of grids.

Thanks for the example. This is actually my current solution :) However, as you allude to, I'm not a huge fan of it because 1) It requires boilerplate on the part of library users. 2) It makes you scratch your head and go "Huh; operator and non-operator functions have different compile-time polymorphism capabilities; why is that?"
Jun 03 2014