Last active
February 25, 2019 22:57
-
-
Save battermann/ad3ac2ed80158019fa5f to your computer and use it in GitHub Desktop.
Functional error handling examples in F# and C# - http://blog.leifbattermann.de/2015/09/12/error-handling-with-applicative-functors-in-f-and-c/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// The NUnit and Chessie Nuget packages are required for this | |
// To install NUnit and Chessie run the following commands from the Package Manager Console | |
// PM> Install-Package NUnit -Version 2.6.4 | |
// PM> Install-Package Chessie | |
using System; | |
using System.Text.RegularExpressions; | |
using Chessie.ErrorHandling; | |
using Chessie.ErrorHandling.CSharp; | |
using Microsoft.FSharp.Core; | |
using NUnit.Framework; | |
namespace ErrorHandlingSamples.CSharp | |
{ | |
public class Customer | |
{ | |
public int Id { get; private set; } | |
public string Name { get; private set; } | |
public string Email { get; private set; } | |
private Customer() { } | |
internal static Customer Create(int id, string name, string email) { return new Customer{ Id = id, Name = name, Email = email }; } | |
public static Result<Customer, string> CreateResult(int id, string name, string email) | |
{ | |
var idResult = ValidateId(id); | |
var nameResult = ValidateName(name); | |
var emailResult = ValidateEmail(email); | |
return | |
new Func<int, string, string, Customer>(Create) | |
.Curry() | |
.Map(idResult) | |
.Apply(nameResult) | |
.Apply(emailResult); | |
} | |
// some dummy checks | |
private static Result<string, string> ValidateEmail(string email) | |
{ | |
var regex = new Regex(@"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"); | |
return !String.IsNullOrWhiteSpace(email) && regex.IsMatch(email) | |
? Result<string, string>.Succeed(email) | |
: Result<string, string>.FailWith("email not valid"); | |
} | |
private static Result<string, string> ValidateName(string name) | |
{ | |
return name.Length > 1 | |
? Result<string, string>.Succeed(name) | |
: Result<string, string>.FailWith("name not valid"); | |
} | |
private static Result<int, string> ValidateId(int id) | |
{ | |
return id > 0 | |
? Result<int, string>.Succeed(id) | |
: Result<int, string>.FailWith("id not valid"); | |
} | |
} | |
public static class Extensions | |
{ | |
public static Result<T2, TMessage> Apply<T1, T2, TMessage>(this Result<Func<T1, T2>, TMessage> wrappedFunc, Result<T1, TMessage> result) | |
{ | |
var convertedFunc = wrappedFunc.Select(f => FSharpFunc<T1, T2>.FromConverter(x => f(x))); | |
return Trial.apply(convertedFunc, result); | |
} | |
public static Result<T2, TMessage> Map<T1, T2, TMessage>(this Func<T1, T2> f, Result<T1, TMessage> result) | |
{ | |
return result.Select(f); | |
} | |
public static Func<T1, Func<T2, Func<T3, T4>>> Curry<T1, T2, T3, T4>(this Func<T1, T2, T3, T4> f) | |
{ | |
return x1 => x2 => x3 => f(x1, x2, x3); | |
} | |
} | |
[TestFixture] | |
public class ErrorHandlingSamplesCSharpTests | |
{ | |
[Test] | |
public void Select_Test() | |
{ | |
var rOk = Result<int, string>.Succeed(16); | |
var rBad = Result<int, string>.FailWith("error"); | |
rOk | |
.Select(x => x * 2) | |
.Match( | |
ifSuccess: (v,_) => Assert.That(v, Is.EqualTo(32)), | |
ifFailure: _ => Assert.Fail("Should be ok, but was bad.")); | |
rBad | |
.Select(x => x * 2) | |
.Match( | |
ifSuccess: (v, _) => Assert.Fail("Should be bad, but was ok."), | |
ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[]{"error"}))); | |
} | |
[Test] | |
public void SelectMany_Test() | |
{ | |
var validate = new Func<Customer, Result<Customer, string>>(Result<Customer, string>.Succeed); | |
var update = new Func<Customer, Result<Customer, string>>(c => Result<Customer, string>.FailWith("update error")); | |
var send = new Func<Customer, Result<Customer, string>>(c => Result<Customer, string>.FailWith("send error")); | |
var customer = Customer.Create(42, "John", "[email protected]"); | |
// with LINQ query syntax | |
var resultLinq = | |
from v in validate(customer) | |
from u in update(v) | |
from s in send(u) | |
select s; | |
resultLinq.Match( | |
ifSuccess: (v, _) => Assert.Fail("Should be bad, but was ok."), | |
ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] { "update error" }))); | |
// with extensions methods | |
var resultExt = validate(customer) | |
.SelectMany(update) | |
.SelectMany(send); | |
resultExt.Match( | |
ifSuccess: (v,_) => Assert.Fail("Should be bad, but was ok."), | |
ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] {"update error"}))); | |
} | |
[Test] | |
public void Apply_Test() | |
{ | |
var idResult = Result<int, string>.FailWith("id not valid"); | |
var nameResult = Result<string, string>.Succeed("John"); | |
var emailResult = Result<string, string>.FailWith("email not valid"); | |
// with LINQ query syntax | |
var customerLinq = | |
from id in idResult | |
join name in nameResult on 1 equals 1 | |
join email in emailResult on 1 equals 1 | |
select Customer.Create(id, name, email); | |
customerLinq.Match( | |
ifSuccess: (v, _) => Assert.Fail("Should be bad, but was ok."), | |
ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] { "id not valid", "email not valid" }))); | |
// with extension methods | |
var customerExt = idResult | |
.Select(new Func<int, Func<string, Func<string, Customer>>>(id => name => email => Customer.Create(id, name, email))) | |
.Apply(nameResult) | |
.Apply(emailResult); | |
customerExt.Match( | |
ifSuccess: (v, _) => Assert.Fail("Should be bad, but was ok."), | |
ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] { "id not valid", "email not valid" }))); | |
// with Map extension method | |
var create = new Func<int, string, string, Customer>(Customer.Create).Curry(); | |
var customerMap = create | |
.Map(idResult) | |
.Apply(nameResult) | |
.Apply(emailResult); | |
customerMap.Match( | |
ifSuccess: (v, _) => Assert.Fail("Should be bad, but was ok."), | |
ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] { "id not valid", "email not valid" }))); | |
} | |
[Test] | |
public void Bypass_Test() | |
{ | |
var idResult = Result<int, string>.FailWith("id not valid"); | |
var nameResult = Result<string, string>.Succeed("John"); | |
var emailResult = Result<string, string>.FailWith("email not valid"); | |
var result = | |
from id in idResult | |
from name in nameResult | |
from email in emailResult | |
select Customer.Create(id, name, email); | |
result.Match( | |
ifSuccess: (v, _) => Assert.Fail("Should be bad, but was ok."), | |
ifFailure: errs => Assert.That(errs, Is.EquivalentTo(new[] { "id not valid" }))); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// The Chessie and NUnit Nuget packages are required for this | |
// To install Chessie and NUnit run the following commands from the Package Manager Console | |
// PM> Install-Package Chessie | |
// PM> Install-Package NUnit -Version 2.6.4 | |
module ErrorHandlingSamples.FSharp | |
open Chessie.ErrorHandling | |
open System.Text.RegularExpressions | |
type Customer = { id:int; name:string; email:string } | |
// some dummy checks | |
let validateId id = if id > 0 then ok id else fail "id not valid" | |
let validateName (name: string) = if name.Length > 1 then ok name else fail "name not valid" | |
let validateEmail (email: string) = | |
let regex = new Regex(@"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") | |
match regex.IsMatch(email) with | |
| true -> ok email | |
| false -> fail "email not valid" | |
let Create id name email = | |
let idResult = validateId id | |
let nameResult = validateName name | |
let emailResult = validateEmail email | |
let create = fun id name email -> { id = id; name = name; email = email } | |
create | |
<!> idResult | |
<*> nameResult | |
<*> emailResult | |
let Create' id (name:string) (email:string) = | |
let idResult = validateId id | |
let nameResult = validateName name | |
let emailResult = validateEmail email | |
let create = fun id name email -> { id = id; name = name; email = email } | |
idResult | |
>>= fun id -> nameResult | |
>>= fun name -> emailResult | |
>>= fun email -> ok (create id name email) | |
module Tests = | |
open Chessie.ErrorHandling | |
open NUnit.Framework | |
// curried version of AreEqual | |
let shouldEqual (x : 'a) (y : 'a) = Assert.AreEqual(x, y, sprintf "Expected: %A\nActual: %A" x y) | |
[<Test>] | |
let ``lift should multiply inner value of success by 2``() = | |
ok 16 | |
|> Trial.lift ((*) 2) | |
|> shouldEqual (ok 32) | |
[<Test>] | |
let ``lift applied to a failure should fail``() = | |
fail "error" | |
|> Trial.lift ((*) 2) | |
|> shouldEqual (Bad [ "error" ]) | |
[<Test>] | |
let ``<!> should multiply inner value of success by 2``() = | |
((*) 2) | |
<!> ok 16 | |
|> shouldEqual (ok 32) | |
[<Test>] | |
let ``<!> applied to a failure should fail``() = | |
((*) 2) | |
<!> fail "error" | |
|> shouldEqual (Bad [ "error" ]) | |
[<Test>] | |
let ``bind should bypass after first error``() = | |
let validate c = ok c | |
let update c = fail "update error" | |
let send c = fail "send error" | |
let customer = { id = 42; name = "John"; email = "[email protected]" } | |
validate customer | |
>>= update | |
>>= send | |
|> shouldEqual (Bad [ "update error" ]) | |
[<Test>] | |
let ``bind should create valid customer if no failures``() = | |
let validate c = ok c | |
let update c = ok c | |
let send c = ok c | |
let customer = { id = 42; name = "John"; email = "[email protected]" } | |
validate customer | |
>>= update | |
>>= send | |
|> shouldEqual (ok { id = 42; name = "John"; email = "[email protected]" }) | |
[<Test>] | |
let ``apply valid inputs should create valid customer``() = | |
Create 42 "John" "[email protected]" | |
|> shouldEqual (ok { id = 42; name = "John"; email = "[email protected]" }) | |
[<Test>] | |
let ``apply invalid inputs should fail with accumulated messages``() = | |
Create -1 "John" "foo" | |
|> shouldEqual (Bad [ "id not valid"; "email not valid" ]) | |
[<Test>] | |
let ``bind invalid inputs should fail with first messages``() = | |
Create' -1 "John" "foo" | |
|> shouldEqual (Bad [ "id not valid" ]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment