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);
}
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).
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With