Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nullable Reference Types and Either Pattern

I'm trying out the new Nullable Reference Types in C# 8.0, and I'm facing the following issue.

Given this struct:

public readonly struct Either<TReturn, TError>
    where TReturn : struct
    where TError : struct
{
    public TError? Error { get; }
    public TReturn? Response { get; }

    public Either(TError? error, TReturn? response)
    {
        if (error == null && response == null)
        {
           throw new ArgumentException("One argument needs not to be null.");
        }
        if (error != null && response != null)
        {
            throw new ArgumentException("One argument must be null.");
        }
        Error = error;
        Response = response;
    }
}

How can I tell the compiler that either Error or Response is not null, and that they can't both be null? Is there a way to do such a thing with the new attributes?

like image 451
Romain Vergnory Avatar asked Jan 01 '23 15:01

Romain Vergnory


1 Answers

Update for structs

The code doesn't change when the result types change to structs. To use struct type parameters, the following constraints have to be added to the interface and the types:

where TResult : struct
where TError  : struct

When I think about the Either pattern, I think about F#, pattern matching and discriminated unions, not nulls. In fact, Either is a way to avoid nulls. In fact, the question's code looks like an attempt to create a Result type, not just an Either. Scott Wlaschin's Railway Oriented Programming shows how such a type can be used to implement error handling in a functional language.

In F#, the Result type is defined as:

type Result<'T,'TError> = 
    | Ok of ResultValue:'T 
    | Error of ErrorValue:'TError

We can't do that in C# 8 yet, because there are no discriminated unions. Those are planned for C# 9.

Pattern Matching

What we can do, is use pattern matching to get the same behavior eg:

interface IResult<TResult,TError>{} //No need for an actual implementation

public class Success<TResult,TError>:IResult<TResult,TError>

{
    public TResult Result {get;}

    public Success(TResult result) { Result=result;}
}

public class Error<TResult,TError>:IResult<TResult,TError>
{
    public TError ErrorValue {get;}

    public Error(TError error) { ErrorValue=error;}
}

This way there's no way to create an IResult<> that is both a success and error. This can be used with pattern matching, eg:

IResult<int,string> someResult=.....;

if(someResult is Success<int,string> s)
{
    //Use s.Result here
}

Simplifying the expressions

Given C# 8's property patterns, this could be rewritten as :

if(someResult is Success<int,string> {Result: var result} )
{
    Console.WriteLine(result);
}

or, using switch expressions, a typical railway-style call :

IResult<int,string> DoubleIt(IResult<int,string> data)
{
    return data switch {    Error<int,string> e=>e,
                            Success<int,string> {Result: var result}=>
                                       new Success<int,string>(result*2),
                            _ => throw new Exception("Unexpected type!")
                            };
}    

F# wouldn't need that throw as there's no way that an Result<'T,'TError> would be something other than Ok or Error. In C#, we don't have that feature yet.

The switch expression allows exhaustive matching. I think the compiler will generate a warning if the default clause is missing too.

With deconstructors

The expressions can be simplified a bit more if the types have deconstructors, eg:

public class Success<TResult,TError>:IResult<TResult,TError>
{
    public TResult Result {get;}

    public Success(TResult result) { Result=result;}

    public void Deconstruct(out TResult result) { result=Result;}
}

public class Error<TResult,TError>:IResult<TResult,TError>
{
    public TError ErrorValue {get;}

    public Error(TError error) { ErrorValue=error;}

    public void Deconstruct(out TError error) { error=ErrorValue;}
}

In that case the expression can be written as :

return data switch {    
                Error<int,string> e => e,
                Success<int,string> (var result) => new Success<int,string>(result*3),
                _ => throw new Exception("Unexpected type!")
};

Nullability

The question started with nullable reference types, so what about nullability? Will we get a warning in C# 8 if we try to pass a nulll?

Yes, as long as NRTs are enabled. This code :

#nullable enable

void Main()
{
     IResult<string,string> data=new Success<string,string>(null);
     var it=Append1(data);
     Console.WriteLine(it);
}

IResult<string,string> Append1(IResult<string,string> data)
{
    return data switch {    Error<string,string> e=>e,
                            Success<string,string> (var result)=>
                                new Success<string,string>(result+"1"),
                            _ => throw new Exception("Unexpected type!")
                            };
}

Genereates CS8625: Cannot convert null literal to non-nullable reference type

Trying

string? s=null;
IResult<string,string> data=new Success<string,string>(s);

Generates CS8604: Possible null reference argument ....

like image 145
Panagiotis Kanavos Avatar answered Jan 13 '23 21:01

Panagiotis Kanavos