This library adds a Result class for FP-style error handling in C#.
There are two classes in the ResultDotNet namespace: a Result<tVal, tErr>
generic data type, and a Result
static class. The former is a data type that can be used to model values that might come back as an error, along with members to consume that data. The latter is a set of static methods that work on the Result data type that don't read well as members - notably Map2 through Map4 and Bind2 through Bind4 - as well as functions for creating new Result data types.
using ResultDotNet;
...
Result<double, string> divide(double numerator, double denominator) =>
(denominator == 0)
? Result.Error<double, string>("Cannot divide by 0!")
: Result.Ok<double, string>(numerator / denominator);
You could also use the C#6 using static
feature to simplify the above to:
using ResultDotNet;
using static ResultDotNet.Result;
...
Result<double, string> divide(double numerator, double denominator) =>
(denominator == 0)
? Error<double, string>("Cannot divide by 0!")
: Ok<double, string>(numerator / denominator);
The easiest way to get the result is to use the Match()
member:
using ResultDotNet;
...
string pricePerUnitForDisplay(Invoice invoice) =>
divide(invoice.Total, invoice.NumberOfUnits).Match(
ok: ppu => ppu.ToString(),
error: err => $"N/A: {err}");
Sometimes you end up with a Result<TVal, TErr>
, but you really just want the value, and the program should crash if the Result is an Error. Now you could do this with a regular Match statement:
var value = result.Match(
ok: val => val
err: { throw new ResultExpectedException("Something went wrong"); });
But we provide a method just for doing that more conveniently:
var value = result.Unless("Something went wrong");
(And if you track code coverage, you don't even need to assemble a test with the error condition to get full code coverage).
We also provide a method for cases where there's no need to provide an additional message, the error type speaks for itself:
var session = tryLogin(username, password).Expect();
Result is a union of types Result<tVal, tErr>.Ok
and Result<tVal, tErr>.Error
(Result<tVal,tErr>
itself is abstract, and has the two unioned types as concrete child classes), so you can also manually check the types:
using ResultDotNet;
...
string pricePerUnitForDisplay(Invoice invoice) {
var ans = divide(invoice.Total, invoice.NumberOfUnits);
if (ans is Result<double, string>.Ok)
return (ans as Result<double, string>.Ok).Item.ToString();
else {
var err = (ans as Result<double, string>.Error).Item;
return $"N/A: {err}";
}
}
Following the same idea, you could use pattern matching from C# 7, to write something like:
using ResultDotNet;
...
string pricePerUnitForDisplay(Invoice invoice) =>
var ans = divide(invoice.Total, invoice.NumberOfUnits);
if (ans is Result<double,string>.Ok div)
return div.Item.ToString();
else if (ans is Result<double,string>.Error err)
return $"N/A: {err.Item}";
else ...
(I apologize for the totally contrived examples)
using ResultDotNet;
...
Result<double, string> pricePerUnit(Invoice invoice) => divide(invoice.Total, invoice.NumberOfUnits);
Result<double, string> savingsPerUnit(Invoice invoice, double dollarsOff) =>
pricePerUnit(invoice).Bind(ppu => divide(dollarsOff, ppu));
Result<double, string> pricePerUnitWithDiscount(Invoice invoice, double dollarsOffPerUnit) =>
pricePerUnit(invoice).Map(ppu => ppu - dollarsOffPerUnit);
You can also use LINQ to build expressions using Results. You can think of the Result a bit like a collection that contains the successful result when assembling a LINQ expression. It can often be more intuitive and readable, at the expense of being slightly more total code:
using ResultDotNet;
...
Result<double, string> pricePerUnit(Invoice invoice) => divide(invoice.Total, invoice.NumberOfUnits);
Result<double, string> savingsPerUnit(Invoice invoice, double dollarsOff) =>
from ppu in pricePerUnit(invoice)
from spu in divide(dollarsOff, ppu)
select spu;
Result<double, string> pricePerUnitWithDiscount(Invoice invoice, double dollarsOffPerUnit) =>
from ppu in pricePerUnit(invoice) select ppu - dollarsOffPerUnit;
map and bind themselves have static functions:
using ResultDotNet;
...
Result<double, string> pricePerUnit(Invoice invoice) => divide(invoice.Total, invoice.NumberOfUnits);
Result<double, string> savingsPerUnit(Invoice invoice, double dollarsOff) =>
Result.Bind(ppu => divide(dollarsOff, ppu), pricePerUnit(invoice));
Result<double, string> pricePerUnitWithDiscount(Invoice invoice, double dollarsOffPerUnit) =>
Result.Map(ppu => ppu - dollarsOffPerUnit, pricePerUnit(invoice));
but there are also functions for Map2 through Map4 and Bind2 through Bind4 that only exist as static functions (object methods are hard to read when binding or mapping with multiple Results)
using ResultDotNet;
...
Invoice createInvoice(double total, double numberOfUnits) => new Invoice(total, numberOfUnits);
Result<Invoice, string> createInvoice(Result<double, string> total, Result<double, string> numberOfUnits) =>
Result.Map2(createInvoice, total, numberOfUnits);
if you need to take an action on ok or error instead of returning a value, you can use the overloads for the Match()
member, or the IfOk()
and IfError()
members:
using ResultDotNet;
...
Result<DataTable, string> result = executeDatabaseQuery(sql);
result.IfError(err => logger.Log(err));
using ResultDotNet;
...
Result<DataTable, string> result = executeDatabaseQuery(sql);
result.Match(
ok: val => logger.Log($"DB query ran successfully: {sql}"),
error: err => logger.Log($"DB query FAILED: {sql}"));
Since Result uses many higher order functions, using the C# interface doesn't interop well with F# (since F# prefers FSharpFunc
s instead of System.Func
s).
To make usage from F# easier, there's a ResultDotNet.FSharp namespace that shadows the Result module with one that uses F#-friendly functions
open ResultDotNet
...
let divide (numerator:float) (denominator:float) =
if denominator = 0.
then Error "Cannot divide by 0!"
else Ok (numerator / denominator)
Result is a union of types Ok of 'tVal
and Error of 'tErr
, so the easiest way to get the result is to use a match
statement:
open ResultDotNet
...
let pricePerUnitForDisplay invoice =
match divide invoice.Total invoice.NumberOfUnits with
| Ok ppu -> ppu.ToString()
| Error err -> "N/A: " + err
Sometimes you end up with a Result<'tVal, 'tErr>
, but you really just want the value, and the program should crash if the Result is an Error. Now you could do this with a regular Match statement:
let value =
match result with
| Ok val -> val
| Error err -> raise (ResultExpectedException("Something went wrong"))
But we provide a function just for doing that more conveniently:
let value = result |> Result.unless("Something went wrong");
(And if you track code coverage, you don't even need to assemble a test with the error condition to get full code coverage).
We also provide a function for cases where there's no need to provide an additional message, the error type speaks for itself:
let session = tryLogin username password |> Result.expect
(I apologize for the totally contrived examples)
open ResultDotNet
open ResultDotNet.FSharp
...
type Invoice = { Total:float; NumberOfUnits:float }
let pricePerUnit invoice = divide invoice.Total invoice.NumberOfUnits
let savingsPerUnit invoice dollarsOff =
pricePerUnit invoice |> Result.bind (fun ppu -> divide dollarsOff ppu)
// you could of course `pricePerUnit () |> Result.bind (divide dollarsOff)`
// but I find it counterintuitive that dollarsOff would be the numerator with that syntax
let pricePerUnitWithDiscount invoice dollarsOffPerUnit =
pricePerUnit invoice |> Result.map (fun ppu -> ppu - dollarsOffPerUnit)
open ResultDotNet
open ResultDotNet.FSharp
...
type Invoice = { Total:float; NumberOfUnits:float }
let newInvoice total numberOfUnits = { Total = total; NumberOfUnits = numberOfUnits }
let createInvoice (total:Result<double, string>) (numberOfUnits:Result<double, string>) =
Result.map2 newInvoice total numberOfUnits
if you need to take an action on ok or error instead of returning a value, you can use the match statement as normal, or you can use the ifOk
and ifError
functions:
open ResultDotNet
open ResultDotNet.FSharp
...
let result:Result<DataTable, string> = executeDatabaseQuery sql
result |> Result.ifError (logger.Log);
when using ResultDotNet from F#, you can use a computation expression in place of bind & map. These are repeats of the above examples now using computation expressions:
open ResultDotNet
open ResultDotNet.FSharp
...
type Invoice = { Total:float; NumberOfUnits:float }
let pricePerUnit invoice = divide invoice.Total invoice.NumberOfUnits
let savingsPerUnit invoice dollarsOff =
result {
let! ppu = pricePerUnit invoice
return! divide dollarsOff ppu
}
let pricePerUnitWithDiscount invoice dollarsOffPerUnit =
result {
let! ppu = pricePerUnit invoice
return ppu - dollarsOffPerUnit
}
open ResultDotNet
open ResultDotNet.FSharp
...
type Invoice = { Total:float; NumberOfUnits:float }
let newInvoice total numberOfUnits = { Total = total; NumberOfUnits = numberOfUnits }
let createInvoice (total:Result<double, string>) (numberOfUnits:Result<double, string>) =
result {
let! t = total
let! n = numberOfUnits
return newInvoice t n
}