-
Notifications
You must be signed in to change notification settings - Fork 1
Result monad
The monad allows to specify the result of some calculation.
To create a result just use one of the methods of the helper class named Result
.
For example, to create a value that is the result of successful calculation, call Result.Success
method.
var result = Result.Success(5);
The same way, to create a value that is the result of failed calculation, call Result.Fail
method.
var result = Result.Fail<int>();
To obtain the value of the result, access Value
property of the result instance.
var resultValue = result.Value;
If result object is in failed state, then call to Value
property results to ApplicationException
signalling that "Cannot obtain value for not succeed result"
. To check either result is succeed or not you can look for IsSucceed
or IsFailed
properties.
var isSucceed = result.IsSucceed;
var isFailed = result.IsFailed;
Another option is to use the fluent syntax that enables flexible way to operate with results. If you want to conditionally call to subsequent calculations only in case if result is still succeed, then you can chain functions using Success
extension method.
var result = CalculationMethodA()
.Success(v => CalculationMethodB(v))
.Success(v => CalculationMethodC(v));
Each calculation method should return result value, that will be unwrapped and passed to the next method only if the result is still succeed.
The same way, you can make conditional call to method in case if result is already failed.
var result = CalculationMethodA()
.Failed(() => CalculationMethodB())
.Failed(() => CalculationMethodC());
Or mix both methods:
var result = CalculationMethodA()
.Success(v => CalculationMethodB(v))
.Failed(() => CalculationMethodC());
In this scenario, the CalculationMethodB
will be called only if CalculationMethodA
is succeed and CalculationMethodC
will be called if either of previous calls is failed.
You can compose the results of calculations and make subsequent calculations in case if all or any of results are succeed or failed.
var resultA = CalculationMethodA();
var resultB = CalculationMethodB();
var result = resultA.And(resultB)
.Success(v => CalculationMethodC(v))
.Failed(() => CalculationMethodD());
In this scenario, the CalculationMethodC
will be called if both CalculationMethodA
and CalculationMethodB
are succeed. This method should receive the tuple of two results as the parameter. In case if you want to compose more than two results using subsequent calls to And
method then the tuple will contain the number of composed results up to 8. More than 8 composed results are available using AggregateResult
class. The CalculationMethodD
will be called if any of the previous calls is failed.
var resultA = CalculationMethodA();
var resultB = CalculationMethodB();
var result = resultA.Or(resultB)
.Success(v => CalculationMethodC(v))
.Failed(() => CalculationMethodD());
In this scenario the CalculationMethodC
will be called if any of the previous calls is succeed and vise versa the CalculationMethodD
will be called if all of the previous calls are failed.
It is a little bit inconvenient to mix both approach to handle the errors: by using result values and by handling exceptions. If you like to merge both approaches into single one then you can convert some known exceptions into the result values. Say, you want to call some method and to consider some of known exceptions that can be thrown by that method as result values. To achieve such behavior call to Result.Eval
static method.
var result = Result.Eval(SomeFunctionThatCanThrowExceptions,
typeof(ExceptionTypeA),
typeof(ExceptionTypeB));
This example code calls SomeFunctionThatCanThrowExceptions
and returns the result that is failed if exception of one of the given types is thrown. If method will throw an exception of type that is not listed in arguments to Eval
method than this exception will be provided to the caller code unhandled.
Result values can be composed using And
and Or
extension methods. The result of composition is wrapped into AggregateResult
type. To access the individual result value you can use AggregateValue
property that returns a tuple of result values.
var resultA = CalculationMethodA();
var resultB = CalculationMethodB();
var resultC = CalculationMethodC();
var resultD = CalculationMethodD();
var result = resultA
.And(resultB)
.And(resultC)
.And(resultD);
resultA == result.AggregateValue.Item1
resultA == result.AggregateValue.Item2
resultC == result.AggregateValue.Item3
resultD == result.AggregateValue.Item4
In case if aggregate result is composed of more than 8 result values than composition is packed into AggregateCollectionResult
. Both AggregateResult
and AggregateCollectionResult
can be iterated.
var resultA = CalculationMethodA();
var resultB = CalculationMethodB();
var resultC = CalculationMethodC();
var resultD = CalculationMethodD();
var result = resultA
.And(resultB)
.And(resultC)
.And(resultD);
resultA == result.ElementAt(0)
resultA == result.ElementAt(1)
resultC == result.ElementAt(2)
resultD == result.ElementAt(3)
Is some scenarios you want to have not just a value and a market showing the success of calculation, but some other value telling what exactly goes wrong if the result is in failed state. That the ResultBuilder
type is for. The Result
static type follows the builder pattern and provides two methods: For
and And
. Let's see how to make the result value that looks very similar to the simple result value in success state.
var result = Result.For(5).And<string>();
This code creates the result value which stores the success integer value equals to 5
and can store some string that describes what goes wrong. The same way we create the result that is failed.
var result = Result.For<int>().And("It was terribly incorrect input data!");
And this code creates the result value which stores string value linked to the error state and can store some integer success value. Please, note that you cannot create a result value with both success and fail values set. At every single moment the result can store the only one value - either success or fail ones.
Let's imagine a function returns a result value that stores the maybe
value. If you intend to chain that function call with subsequent processing of internal maybe
value, than you could face with the sort of inconvenience. For such scenarios here is the special function named Twist
that can unwrap result value, then unwrap internal monadic value, and then pack them in reverse order.
var result = GiveMeAResultStream(); // returns Result<Maybe<Stream>>
var maybe = result.Twist(); // Maybe<Result<Stream>>
Now you can handle Maybe
value as usual, optionally accessing wrapped result value.
The same way it works in reverse order.
var maybe = GetMeAMaybeString(); // returns Maybe<Result<Stream>>
var result = maybe.Twist(); // Result<Maybe<Stream>>
Now you can use all the power of result values while optionally accessing wrapped maybe value.
It's very common scenario when you need to convert string value into one of the base type values. Among them are integers, booleans, date and times, unique identifiers, etc. Basic class library gives you a bunch of methods with output parameters like TryParse
but it requires more code lines and increases code complexity.
This library offers you to use extension methods that return a result value, thus moving you to the area where you can easily handle with results using common way described above.
Target type | Extension method | Result value |
---|---|---|
Boolean | ToBool() |
Result<bool> |
DateTime |
ToDateTime() ToDateTime(DateTimeStyles, IFormatProvider) ToDateTime(string, DateTimeStyles, IFormatProvider)
|
Result<DateTime> |
Guid |
ToGuid() ToGuid(string)
|
Result<Guid> |
Int32 |
ToInt() ToInt(NumberStyles, IFormatProvider)
|
Result<int> |
UInt32 |
ToUInt() ToUInt(NumberStyles, IFormatProvider)
|
Result<uint> |
Int64 |
ToLong() ToLong(NumberStyles, IFormatProvider)
|
Result<long> |
UInt64 |
ToULong() ToULong(NumberStyles, IFormatProvider)
|
Result<ulong> |
Int16 |
ToShort() ToShort(NumberStyles, IFormatProvider)
|
Result<short> |
UInt16 |
ToUShort() ToUShort(NumberStyles, IFormatProvider)
|
Result<ushort> |
By the way, you can convert any functions with output parameters to new one without them, but returning the result value. Imagine, you have old-school function
bool GetSomeIntResult(SomeType parameter1, SomeType2 parameter2, out int intResult)
This function tries to return the its result via the output parameter and the status of the operation via the result value itself. Looks nasty! Just convert it into nice looking function:
Result<int> GetSomeIntResultBetterVersion(SomeType parameter1, SomeType2 parameter2)
It is very easy using Funcs.MakeResult
static call.
var func = Funcs.MakeResult<SomeType, SomeType2, int>(GetSomeIntResult);
And just call it.
var result = func(value, value2);