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.