digitalmars.D - Operator/concept interoperability
- Mason McGill (32/32) Jun 03 2014 I have a numerical/multimedia library that defines the concept of
- TheFlyingFiddle (57/93) Jun 03 2014 Well one reason for this is that unlike methods it is hard to
- Mason McGill (35/71) Jun 03 2014 Interesting point. However, I believe this is also the case for
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 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
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 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, -MMBased 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
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