1

I've been experimenting with functional programming concepts for C# and encountered with the Either monad, which in F# would be Choice if I'm not mistaken. C# doesn't have this type natively implemented and I've come with a very naive implementation of it.

// Don't use this code in production, this is just an experiment type
public record Either<T1, T2> where T1: class where T2 : class
{
    public static implicit operator Either<T1, T2>(T1 typeOne)
        => (Either<T1, T2>)(object)typeOne;

    public static implicit operator Either<T1, T2>(T2 typeTwo)
        => (Either<T1, T2>)(object)typeTwo;

    public static implicit operator T1(Either<T1, T2> either)
        => (T1)(object)either;

    public static implicit operator T2(Either<T1, T2> either)
        => (T2)(object)either;
}

The compiler accepts any implicit conversion I do with the types defined when constructing the generic.

// Accepted by the compiler
Either<Foo, Bar> someBar = new Bar();
Bar actualBar = someBar;

// Rejected by the compiler as expected
Either<Foo, Bar> wrong = new MyClass();

But it doesn't accept pattern matching the same type with the respective generics

_ = someBar switch
{
    // Error: an expression of type 'Either<Foo, Bar>' cannot be handled by a pattern of type 'Foo'
    Foo => Console.WriteLine("Foo works!"),

    // Error: an expression of type 'Either<Foo, Bar>' cannot be handled by a pattern of type 'Bar'
    Bar => Console.WriteLine("Bar works!"),

    // Works fine
    null => Console.WriteLine("Null works!")
}

I've done some research and the only requirement I found for a type to use pattern matching is having implicit or explicit conversions, not sure if this is right but I found not only in this answer, but in others too.

How am I supposed to design my generic type to work with pattern matching?

Thadeu Fernandes
  • 505
  • 1
  • 6
  • 23
  • Not related to what you are asking, but you do realise that your implicit conversions will throw exceptions when they are run right? – Sweeper Jul 27 '21 at 12:18
  • Have you heard of [`OneOf`](https://github.com/mcintyre321/OneOf/)? – Fildor Jul 27 '21 at 12:21
  • @Sweeper yes, that's why I mentioned it being naive, I just needed the compiler to accept my implicit conversions, the implementation for the conversion to work is a detail I didn't want to spend time on right now – Thadeu Fernandes Jul 27 '21 at 12:23
  • @Fildor yes, I've considered it but I really wanted to use the language feature and keep my code clean, I know I could do `(OneOf).Value switch` but if `Value` is implicitly converted, I wanted the type itself to be converted as well, without the need of calling the `Value` property – Thadeu Fernandes Jul 27 '21 at 12:25
  • 1
    Heard the "cleanlyness" argument before - won't go into debate about that. But in the end it boils down to: C#/.Net doesn't provide you with enough features to do it the way you want. So somehow, you need to work around. Now it's the old question: "buy or make"... – Fildor Jul 27 '21 at 12:31

1 Answers1

2

In short - you can't. Type patterns require an implicit reference conversion to exist between two types:

The implicit reference conversions are those conversions between reference_types that can be proven to always succeed, and therefore require no checks at run-time.

Reference conversions, implicit or explicit, never change the referential identity of the object being converted. In other words, while a reference conversion may change the type of the reference, it never changes the type or value of the object being referred to.

Which does not include user defined conversions.

Naive approach (if you don't want to look into exisiting implementations like Either from language-ext or OneOf) can look something like this:

public record Either<T1, T2> where T1: class where T2 : class
{
    public T1 Left { get; set; }
    public T2 Right { get; set; }
    public bool IsLeft { get; }
}

And:

var x = someBar switch
{
     {IsLeft: true} => someBar.Left
};
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Thanks for clarifying! It's understandable that it's a language limitation, I wanted to see how far I could go functional with the language. I'll reconsider using existing libraries for this and I'm also looking forward to seeing new functional patterns to be added to the language. – Thadeu Fernandes Jul 27 '21 at 12:53