Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to have variants in C# besides using the visitor pattern?

There is no direct support for variant types (aka tagged unions, discriminated unions) in C#. However one can go with a visitor pattern that enables discrimination via double-dispatching and guarantees that all cases are addressed at the compile time. However it's tedious to implement. I wonder if there is more effortless way to get: some sort of variants with a discrimination mechanism that guarantees that all cases of a union are addressed at the compile time in C#?

// This is a variant type. At each single time it can only hold one case (a value)
// from a predefined set of cases. All classes that implement this interface
// consitute the set of the valid cases of the variant. So at each time a variant can
// be an instance of one of the classes that implement this interface. In order to
// add a new case to the variant there must be another class that implements
// this interface.
public interface ISomeAnimal
{
    // This method introduces the currently held case to whoever uses/processes
    // the variant. By processing we mean that the case is turned into a resulting
    // value represented by the generic type TResult.
    TResult GetProcessed<TResult>(ISomeAnimalProcessor<TResult> processor);
}

// This is the awkward part, the visitor that is required every time we want to
// to process the variant. For each possible case this processor has a corresponding
// method that turns that case to a resulting value.
public interface ISomeAnimalProcessor<TResult>
{
    TResult ProcessCat(Cat cat);
    TResult ProcessFish(Fish fish);
}

// A case that represents a cat from the ISomeAnimal variant.
public class Cat : ISomeAnimal
{
    public CatsHead Head { get; set; }
    public CatsBody Body { get; set; }
    public CatsTail Tail { get; set; }
    public IEnumerable<CatsLeg> Legs { get; set; }
    public TResult GetProcessed<TResult>(ISomeAnimalProcessor<TResult> processor)
    {
        // a processor has a method for each case of a variant, for this
        // particular case (being a cat) we always pick the ProcessCat method
        return processor.ProcessCat(this);
    }
}

// A case that represents a fish from the ISomeAnimal variant.
public class Fish : ISomeAnimal
{
    public FishHead Head { get; set; }
    public FishBody Body { get; set; }
    public FishTail Tail { get; set; }
    public TResult GetProcessed<TResult>(ISomeAnimalProcessor<TResult> processor)
    {
        // a processor has a method for each case of a variant, for this
        // particular case (being a fish) we always pick the ProcessCat method
        return processor.ProcessFish(this);
    }
}

public static class AnimalPainter
{
    // Now, in order to process a variant, in this case we want to
    // paint a picture of whatever animal it prepresents, we have to
    // create a new implementation of ISomeAnimalProcessor interface
    // and put the painting logic in it. 
    public static void AddAnimalToPicture(Picture picture, ISomeAnimal animal)
    {
        var animalToPictureAdder = new AnimalToPictureAdder(picture);
        animal.GetProcessed(animalToPictureAdder);
    }

    // Making a new visitor every time you need to process a variant:
    // 1. Requires a lot of typing.
    // 2. Bloats the type system.
    // 3. Makes the code harder to maintain.
    // 4. Makes the code less readable.
    private class AnimalToPictureAdder : ISomeAnimalProcessor<Nothing>
    {
        private Picture picture;

        public AnimalToPictureAdder(Picture picture)
        {
            this.picture = picture;
        }

        public Nothing ProcessCat(Cat cat)
        {
            this.picture.AddBackground(new SomeHouse());
            this.picture.Add(cat.Body);
            this.picture.Add(cat.Head);
            this.picture.Add(cat.Tail);
            this.picture.AddAll(cat.Legs);
            return Nothing.AtAll;
        }

        public Nothing ProcessFish(Fish fish)
        {
            this.picture.AddBackground(new SomeUnderwater());
            this.picture.Add(fish.Body);
            this.picture.Add(fish.Tail);
            this.picture.Add(fish.Head);
            return Nothing.AtAll;
        }
    }

}
like image 840
Trident D'Gao Avatar asked Oct 14 '13 01:10

Trident D'Gao


2 Answers

Are you looking for something along the lines of Boost Variants? If so, I don't think a direct porting is possible, because C++ template language and C# generics are somewhat different. Moreover boost::variant uses the visitor pattern. Anyway, if you want, you can write something similar. For example (and please note that this code is only a proof of concept), you may define two generic types for visitors and variants:

public interface VariantVisitor<T, U>
{
    void Visit(T item);
    void Visit(U item);
}

public class Variant<T, U>
{
    public T Item1 { get; private set; }
    private bool _item1Set;
    public U Item2 { get; private set; }
    private bool _item2Set;

    public Variant()
    {
    }

    public void Set(T item)
    {
        this.Item1 = item;
        _item1Set = true;
        _item2Set = false;
    }

    public void Set(U item)
    {
        this.Item2 = item;
        _item1Set = false;
        _item2Set = true;
    }

    public void ApplyVisitor(VariantVisitor<T, U> visitor)
    {
        if (_item1Set)
        {
            visitor.Visit(this.Item1);
        }
        else if (_item2Set)
        {
            visitor.Visit(this.Item2);
        }
        else
        {
            throw new InvalidOperationException("Variant not set");
        }
    }
}

And you can use those types like this:

private static object _result;

internal class TimesTwoVisitor : VariantVisitor<int, string>
{
    public void Visit(int item)
    {
        _result = item * 2;
    }

    public void Visit(string item)
    {
        _result = item + item;
    }
}

[Test]
public void TestVisitVariant()
{
    var visitor = new TimesTwoVisitor();
    var v = new Variant<int, string>();

    v.Set(10);
    v.ApplyVisitor(visitor);
    Assert.AreEqual(20, _result);

    v.Set("test");
    v.ApplyVisitor(visitor);
    Assert.AreEqual("testtest", _result);

    var v2 = new Variant<double, DateTime>();
    v2.Set(10.5);
    //v2.ApplyVisitor(visitor);
    // Argument 1: cannot convert from 'TestCS.TestVariant.TimesTwoVisitor' to 'TestCS.TestVariant.VariantVisitor<double,System.DateTime>'
}

This way, the compiler can verify that you are passing the right visitor to the right variant, and the VariantVisitor interface forces you to implement the Visit method for all the types of the variant. Obviously, you can also define variants with more than two parameters:

public interface VariantVisitor<T, U, V>
...
public interface VariantVisitor<T, U, V, W>
...

public class Variant<T, U, V>
...
public class Variant<T, U, V, W>
...

But personally I don't like this approach, and I'd rather turn Visit methods into lambdas and pass them as parameters where needed, as pointed out in the comments above. For example, you could write some kind of poor man's pattern matching, adding this method to class Variant<T, U>:

    public R Match<R>(Func<T, R> f1, Func<U, R> f2)
    {
        if (_item1Set)
        {
            return f1(this.Item1);
        }
        else if (_item2Set)
        {
            return f2(this.Item2);
        }
        else
        {
            throw new InvalidOperationException("Variant not set");
        }
    }

And use it like this:

[Test]
public void TestMatch()
{
    var v = new Variant<int, string>();

    v.Set(10);
    var r1 = v.Match(
        i => i * 2,
        s => s.Length);
    Assert.AreEqual(20, r1);

    v.Set("test");
    var r2 = v.Match(
        i => i.ToString(),
        s => s + s);
    Assert.AreEqual("testtest", r2);
}

But note that real pattern matching has way more features: guards, exhaustiveness checks, fragile pattern matching checks, etc.

like image 66
astrada Avatar answered Sep 29 '22 11:09

astrada


No way. There is not a concept like using visitor pattern at compile time, because implementation of your visitor pattern runs at runtime through instantiating your classes with use of polymorphism, double-dispatching, on object instances at runtime. Double-dispatching can run only on real object instances at run time, it is not related with compile time. Additionally the "Discrimination Mechanism" must run on your objects and if you are talking about objects, you are at runtime..

like image 40
mehmet mecek Avatar answered Sep 29 '22 13:09

mehmet mecek