Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
    }
    
like image 762
stakx - no longer contributing Avatar asked Mar 12 '11 14:03

stakx - no longer contributing


People also ask

What is a type-safe union?

Using std::variant as a type-safe union In C++, union is a special class type that, at any point, holds a value of one of its data members. Unlike regular classes, unions cannot have base classes nor can they be derived, and they cannot contain virtual functions (that would not make sense anyway).

What is a discriminated union C#?

Discriminated Unions are a functional programming convenience that indicates that something is one of several different types of objects. For example, a User might be an unauthenticated user, a regular user, or an administrator.

What is tagged union in C?

In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, sum type or coproduct, is a data structure used to hold a value that could take on several different, but fixed, types.

Does C# have union types?

Union types are common in functional languages, but have yet to make it to C# and similar. These types solve a simple and very common problem, data that is one of a finite set of possibilities.


1 Answers

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.

like image 119
Marc Gravell Avatar answered Sep 21 '22 02:09

Marc Gravell