Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# 8 nullables and Result container

I have a IResult<T> container that I use to handle errors. It looks like this:

public interface IResult<out T>
{
    ResultOutcome Outcome { get; }   //enum: {Failure, Uncertain, Success}
    string Description { get; }      //string describing the error, in case of !Success
    bool IsSuccess();                //Outcome == Success
    T Data { get; }                  //If success, it contains the data passed on, otherwise NULL
}

And you'd use it like this:

IResult<int> GetSomething()
{
    try{
        int result = //things that might throw...
        return Result<int>.Success(result);  
    } 
    catch(Exception e) 
    {
        return Result<int>.Failure($"Something went wrong: {e.Message}");
    }
}

And then:

var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.

int resultData = result.Data; //<- no errors, so there is something in here.


Until now, all good. When I introduce the nullable types, though, I have a problem:

public interface IResult<out T> where T : class // unfortunately this is necessary
{
    ...
    T? Data { get; }                  //If success, it contains the data passed on, otherwise NULL
}
var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.

int resultData = result.Data; //<- WARNING!!! POSSIBLE DEREFERENCE OF NULL


Now the question: I am sure that result.Data contains something, as it passed the IsSuccess() step. How can I reassure the compiler about it? Is there a way or C#8 nullable concept is just not compatible with this?
Are there other ways to handle results in a similar fashion? (passing on containers instead of exceptions).

P.s. 1
Please, don't suggest to use result.Data!;.

P.s. 2
This code is already used on a thousand of lines or more, so if the change can be on the interface, rather than on the usages, it would be way better.

like image 725
Alvin Sartor Avatar asked Jan 12 '20 09:01

Alvin Sartor


People also ask

What C is used for?

C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...

What is C in C language?

What is C? C is a general-purpose programming language created by Dennis Ritchie at the Bell Laboratories in 1972. It is a very popular language, despite being old. C is strongly associated with UNIX, as it was developed to write the UNIX operating system.

What is the full name of C?

In the real sense it has no meaning or full form. It was developed by Dennis Ritchie and Ken Thompson at AT&T bell Lab. First, they used to call it as B language then later they made some improvement into it and renamed it as C and its superscript as C++ which was invented by Dr.

Is C language easy?

Compared to other languages—like Java, PHP, or C#—C is a relatively simple language to learn for anyone just starting to learn computer programming because of its limited number of keywords.


2 Answers

Update

If you did change the usage, and converted IsSuccess to a property, you could get rid of nullability issues and get exhaustive matching. This switch expression, is exhaustive ie the compiler can check that all possibilities have been met. It does require that each branch only retrieves a valid property though :

var message=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
                            {IsSuccess:false,Description:var error} => $"Oops {error}",
             };  

If your methods accept and return IResult<T> objects, you could write something like :

IResult<string> Doubler(IResult<string> input)
{
    return input switch { {IsSuccess:true,Data:var data} => new Ok<string>(data+ "2"),
                          {IsSuccess:false} => input
    };  
}

...

var result2=new Ok<string>("3");
var message2=Doubler(result2) switch { 
                     {IsSuccess:true,Data:var data} => $"Got some: {data}",
                     {IsSuccess:false,Description:var error} => $"Oops {error}",
             };  

Original Answer

It looks like the real problem is the implementation of the Result pattern. This pattern has two characteristics :

  • It prevents the use of invalid result values at the type level. It does this by It uses two different types to represent good and bad results. By doing so, each type only carries what it needs.
  • It forces the clients to handle all cases or explicitly ignore them.

Some languages like Rust have a built-in type for this. Functional languages that support option types/discriminated unions like F#, can easily implement it with just :

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

Exhaustive pattern matching means clients have to handle both cases. That type is so common though, it made it into the language itself.

C# 8

In C# 8 we can implement the two types, without the exhaustive pattern matching. For now, the types need a common class, either an interface or abstract class, which doesn't really need to have any members. There are many ways to implement them, eg :

public interface IResult<TSuccess,TError>{}

public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
    public TSuccess Data{get;}

    public Ok(TSuccess data)=>Data=data;

    public void Deconstruct(out TSuccess data)=>data=Data;
}

public class Fail<TSuccess,TError>:IResult<TSuccess,TError>
{
    public TError Error{get;}

    public Fail(TError error)=>Error=error;

    public void Deconstruct(out TError error)=>error=Error;
}

We could use structs instead of classes.

Or, to use a syntax closer to C# 9's discriminated unions, the classes can be nested. The type can still be an interface, but I really don't like writing new IResult<string,string>.Fail or naming an interface Result instead of IResult :

public abstract class Result<TSuccess,TError>
{
    public class Ok:Result<TSuccess,TError>
    {
        public TSuccess Data{get;}
        public Ok(TSuccess data)=>Data=data;
        public void Deconstruct(out TSuccess data)=>data=Data;
    }

    public class Fail:Result<TSuccess,TError>
    {
       public TError Error{get;}
        public Fail(TError error)=>Error=error;
        public void Deconstruct(out TError error)=>error=Error;
    }

    //Convenience methods
    public static Result<TSuccess,TError> Good(TSuccess data)=>new  Ok(data);
    public static Result<TSuccess,TError> Bad(TError error)=>new  Fail(error);
}

We can use pattern matching to handle Result values. Unfortunatelly, C# 8 doesn't offer exhaustive matching so we need to add a default case too.

var result=Result<string,string>.Bad("moo");
var message=result switch { Result<string,string>.Ok (var Data) => $"Got some: {Data}",
                            Result<string,string>.Fail (var Error) => $"Oops {Error}"
                            _ => throw new InvalidOperationException("Unexpected result case")
                      };

C# 9

C# 9 is (probably) going to add discriminated unions through enum classes. We'll be able to write :

enum class Result
{
    Ok(MySuccess Data),
    Fail(MyError Error)
}

and use it through pattern matching. This syntax already works in C# 8 as long as there's a matching deconstructor. C# 9 will add exhaustive matching and probably simplify the syntax too :

var message=result switch { Result.Ok (var Data) => $"Got some: {Data}",
                            Result.Fail (var Error) => $"Oops {Error}"
                          };

Updating the existing type through DIMs

Some of the existing functions like IsSuccess and Outcome are just convenience methods. In fact, F#'s option types also expose the "kind" of the value as a tag . We can add such methods to the interface and return a fixed value from the implementations :

public interface IResult<TSuccess,TError>
{
    public bool IsSuccess {get;}
    public bool IsFailure {get;}
    public bool ResultOutcome {get;}
}

public class Ok<TSuccess,string>:IResult<TSuccess,TError>
{
    public bool IsSuccess     =>true;
    public bool IsFailure     =>false;
    public bool ResultOutcome =>ResultOutcome.Success;
    ...
}

The Description and Data properties can be implemented too, as a stop gap measure - they break the Result pattern and pattern matching makes them obsolete anyway :

public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
    ...
    public TError Description=>throw new InvalidOperationException("A Success Result has no Description");
    ...
}

Default interface members can be used to avoid littering the concrete types :

public interface IResult<TSuccess,TError>
{
    //Migration methods
    public TSuccess Data=>
        (this is Ok<TSuccess,TError> (var Data))
        ?Data
        :throw new InvalidOperationException("An Error has no data");

    public TError Description=> 
        (this is Fail<TSuccess,TError> (var Error))
        ?Error
        :throw new InvalidOperationException("A Success Result has no Description");

    //Convenience methods
    public static IResult<TSuccess,TError> Good(TSuccess data)=>new  Ok<TSuccess,TError>(data);
    public static IResult<TSuccess,TError> Bad(TError error)=>new  Fail<TSuccess,TError>(error);

}

Modifications to add exhaustive matching

We could avoid the default cases in the pattern matching exceptions if we use only one flag and the migration properties :

public interface IResult<TSuccess,TError>
{
    public bool IsSuccess{get;}
    public bool IsFailure=>!IsSuccess;
    //Migration methods
    ...
}

var message2=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
                             {IsSuccess:false,Description:var error} => $"Oops {error}",
             };  

This time, the compiler detects there are only two cases, and both are covered. The migration properties allow the compiler to retrieve the correct type. The consuming code has to change and use the correct pattern, but I suspect it already worked that

like image 56
Panagiotis Kanavos Avatar answered Sep 29 '22 23:09

Panagiotis Kanavos


With c# 9 there is MemberNotNullWhen attribute which can hide corresponding warning when IsSuccess is checked

public interface IResult<out T>
{
    [MemberNotNullWhen(true, nameof(Data))]
    bool IsSuccess();
    T? Data { get; }
}

IResult<string> res = GetSomeResult();
if(!res.IsSuccess())
   throw new Exception(); // or just return something else

var len = res.Data.Length; // no nullability warning

The official microsoft docs are not updated yet. I received more nullability information about it within this lection. In order to use the above attribute, you must use .net50, or set language version within csproj file as c#9. Another way to backport these attributes is to use Nullable package.

like image 36
unsafePtr Avatar answered Sep 30 '22 00:09

unsafePtr