Yes, it is possible. Here's what I came up with.
import std.range;
import std.algorithm;
import std.stdio;
import std.functional;
import std.math;
import std.string;
struct AmbRange(R1, R2, alias Op)
{
public:
this(R1 _r1, R2 _r2) { r1 = _r1; r2 = r2c = _r2; }
void popFront()
{
r2.popFront();
if (r2.empty) { r2 = r2c; r1.popFront(); }
}
@property auto front() { return Op(r1.front, r2.front); }
@property bool empty() { return r1.empty; }
private:
R1 r1;
R2 r2, r2c;
}
struct Amb(R)
{
alias ElementType!(R) E;
public:
this(R r) { this.r = r; }
auto opBinary(string op, T)(T rhs) if (!is(T U : Amb!(U)))
{
alias binaryFun!("a"~op~"b") Op;
return map!((E e) { return Op(e, rhs); })(r);
}
auto opBinaryRight(string op, T)(T lhs) if (!is(T U : Amb!(U)))
{
alias binaryFun!("a"~op~"b") Op;
return map!((E e) { return Op(lhs, e); })(r);
}
auto opBinary(string op, T)(T rhs) if (is(T U : Amb!(U)))
{
alias binaryFun!("a"~op~"b") Op;
return AmbRange!(R, typeof(rhs.r), Op)(r, rhs.r);
}
auto opDispatch(string f, T ...)(T args)
{
mixin("return map!((E e) { return e."~f~"(args); })(r);");
}
auto opDispatch(string f)()
{
mixin("return map!((E e) { return e."~f~"; })(r);");
}
private:
R r;
}
auto amb(R)(R r) { return Amb!R(r); }
void main()
{
auto r1 = 2 * amb([1, 2, 3]);
assert(equal(r1, [2, 4, 6]));
auto r2 = amb(["ca", "ra"]) ~ "t";
assert(equal(r2, ["cat", "rat"]));
auto r3 = amb(["hello", "cat"]).length;
assert(equal(r3, [5, 3]));
auto r4 = amb(["cat", "pat"]).replace("a", "u");
assert(equal(r4, ["cut", "put"]));
auto r5 = amb([1, 2]) * amb([1, 2, 3]);
assert(equal(r5, [1, 2, 3, 2, 4, 6]));
}
Lots of thanks to BCS for figuring out how to resolve the binaryOp ambiguities.
I had to create a new range for traversing the result of a binary op between two Amb
's, but I think that works out best anyway.
For those that are new to D, and are curious, all that string
stuff is done at compile time, so there's no parsing code at runtime or anything like that -- it's pretty much as efficient as hand-coding it in C.