Home > other >  Can I implement operator overloading for D's SumType alias?
Can I implement operator overloading for D's SumType alias?

Time:08-15

TLDR: Is there a way make D's SumType play nice with opCmp while maintaining its functionality?

Context

I'm writing a program for which D's native SumType works almost completely. However, I would like to be able to do the following:

alias Foo = SumType!(int, string);
Foo x = 3;
Foo y = 5;
writeln(max(x, y));

However, since no ordering is natively defined for SumType, I receive the following error:

C:\D\dmd2\windows\bin\..\..\src\phobos\std\algorithm\comparison.d(1644): Error: static assert:  "Invalid arguments: Cannot compare types SumType!(int, string) and SumType!(int, string) for ordering."
mwe.d(11):        instantiated from here: `max!(SumType!(int, string), SumType!(int, string))`

I was able to remedy this specific issue using the following method:

import std.stdio : writeln;
import std.exception : assertThrown;
import std.algorithm.comparison : max;
import core.exception : AssertError;
import std.sumtype;

struct Foo {
    SumType!(int, string) value;
    this(T)(T v) {
        value = v;
    }
    
    ref Atom opAssign(T)(T rhs) {
        value = rhs;
        return this;
    }
    
    int opCmp(Foo other) {
        return match!(
            (a, b) => a < b ? -1 : a == b ? 0 : 1,
            (_1, _2) => assert(0, "Cannot match")
        )(value, other.value);
    }
}

void main() {
    Foo x = 3;
    Foo y = 7;
    Foo z = "asdf";
    assert(x < y);                   // comparing ints works correctly
    assertThrown!AssertError(x < z); // cannot compare int and string
    assert(max(x, y) == y);          // D's max works
}

The Problem

While I can now use x.value.match!(...) where I used to use x.match!(...), I would like to still be able to call .match! directly on x, and also use match!(...)(x, y) instead of match!(...)(x.value, y.value). I do not like the idea of inserting hundreds of .value throughout my code just to make certain functions like max work, and would prefer if there were a more elegant solution. I tried tinkering around with defining a custom opDispatch using mixins but I couldn't get that to play nicely with the existing SumType:

struct Foo {
    SumType!(int, string) value;
    this(T)(T v) {
        value = v;
    }
    
    ref Atom opAssign(T)(T rhs) {
        value = rhs;
        return this;
    }
    
    int opCmp(Foo other) {
        return match!(
            (a, b) => a < b ? -1 : a == b ? 0 : 1,
            (_1, _2) => assert(0, "Cannot match")
        )(value, other.value);
    }
    
    auto opDispatch(string name, T...)(T vals) {
        return mixin("value." ~ name)(vals);
    }
}

void main() {
    Foo y = 7;
    
    y.match!(
        (int intValue) => writeln("Received an integer"),
        (string strValue) => writeln("Received a string")
    );
}

And I am unable to decode the error which results:

mwe.d(38): Error: none of the overloads of template `std.sumtype.match!(function (int intValue) @safe
{
writeln("Received an integer");
return ;
}
, function (string strValue) @safe
{
writeln("Received a string");
return ;
}
).match` are callable using argument types `!()(Foo)`
C:\D\dmd2\windows\bin\..\..\src\phobos\std\sumtype.d(1659):        Candidate is: `match(SumTypes...)(auto ref SumTypes args)`
  with `SumTypes = (Foo)`
  must satisfy the following constraint:
`       allSatisfy!(isSumType, SumTypes)`

Beyond that I am out of ideas as to how to find a less clunky solution.

CodePudding user response:

I suggest giving alias this a try. Similar to class inheritance, this lets you specialize a type and let other things fall back to the original member.

import std.stdio : writeln;
import std.exception : assertThrown;
import std.algorithm.comparison : max;
import core.exception : AssertError;
import std.sumtype;

struct Foo {
    SumType!(int, string) value;
    this(T)(T v) {
        value = v;
    }
    
    int opCmp(Foo other) {
        return match!(
            (a, b) => a < b ? -1 : a == b ? 0 : 1,
            (_1, _2) => assert(0, "Cannot match")
        )(value, other.value);
    }

    alias value this;
}

void main() {
    Foo x = 3;
    Foo y = 7;
    Foo z = "asdf";
    assert(x < y);                   // comparing ints works correctly
    assertThrown!AssertError(x < z); // cannot compare int and string
    assert(max(x, y) == y);          // D's max works

    // this will now automatically fall back to y.value.match
    y.match!(
        (int intValue) => writeln("Received an integer"),
        (string strValue) => writeln("Received a string")
    );
}

See, you still must construct your special type, but then after that, it will look up there for members. It will find the opCmp, letting it extend the type. But then for everything else, since it isn't there, it will try checking obj.value instead, falling back to the original type.

This doesn't always work, and it means it will implicitly convert too, meaning you can pass a Foo to a void thing(SumType!(int, string)) with it passing foo.value to the function, which may or may not be desirable.

But I think it is the closest thing to what you want here.

(note btw why you got an error originally is that match isn't actually a member of SumType. it is an outside free function that takes all the match lambdas as template arguments. An opDispatch could forward template arguments too - it can be done in a two-level definition - but since match is not a member anyway, it isn't quite going to solve things anyway whereas the alias this does seem to work)

  • Related