Search code examples
c#interfacef#discriminated-unionalgebraic-data-types

Type-safe discriminated unions in C#, or: How to limit the number of implementations of an interface?


First, sorry for the lengthy post. Basically, my question is this:

I'm trying to reproduce the following F# discriminated union type in C#:

type Relation =
     | LessThan of obj * obj
     | EqualTo of obj * obj
     | GreaterThan of obj * obj

Can anyone suggest a simpler interface-based solution than the following?


interface IRelation // concrete types represent ◊ in the expression "Subject ◊ Object"
{
    object Subject { get; }
    object Object  { get; }
}

struct LessThanRelation    : IRelation { … }
struct EqualToRelation     : IRelation { … }
struct GreaterThanRelation : IRelation { … }

All my algorithms recognise these three relation types, and these only, so I need to prevent any further implementations of IRelation by third parties (i.e. other assemblies).

Footnote: To some, it might occur that if I just got my interface and algorithms right in terms of object orientation / polymorphism, it shouldn't matter that an third-party implementation is injected into my algorithm methods, as long as the interface is implemented correctly. This is a valid critique. But let's just assume that for the moment that I'm favouring a more functional-programming style over strict object-orientation in this case.

My best idea so far is to declare all above types as internal (ie. they will never be seen directly by outsiders) and create a proxy type Relation, which will be the only visible type to third parties:

public struct Relation  // constructors etc. are omitted here for brevity's sake
{
    public RelationType Type { get { … /* concrete type of value -> enum value */ } }

    public Relation Subject  { get { return value.Subject; } }
    public Relation Object   { get { return value.Object;  } }

    internal readonly IRelation value;
}

public enum RelationType
{
    LessThan,
    EqualTo,
    GreaterThan
}

All is well so far, but it gets more elaborate…

  • … if I expose factory methods for the concrete relation types:

    public Relation CreateLessThanRelation(…)
    {
        return new Relation { value = new LessThanRelation { … } };
    }
    
  • … whenever I expose an algorithm working on relation types, because I must map from/to the proxy type:

    public … ExposedAlgorithm(this IEnumerable<Relation> relations)
    {
        // forward unwrapped IRelation objects to an internal algorithm method:
        return InternalAlgorithm(from relation in relations select relation.value);
    }
    

Solution

  • Limiting the interface implementations means it isn't really acting as an interface (which should accept any implementation (substitution), such as decorators) - so I can't recommend that.

    Also, note that with a small exception of generics, treating a struct as an interface leads to boxing.

    So that leaves one interesting case; an abstract class with a private constructor, and a known number of implementations as nested types, which means that they have access to the private constructor.

    Now you control the subtypes, boxing isn't an issue (as it is a class), and there is less expectation of substitution.