Skip to content

Instantly share code, notes, and snippets.

@battermann
Last active February 25, 2019 22:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save battermann/ad3ac2ed80158019fa5f to your computer and use it in GitHub Desktop.
Save battermann/ad3ac2ed80158019fa5f to your computer and use it in GitHub Desktop.
// 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@example.com");
// 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" })));
}
}
}
// 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 = "john@example.com" }
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 = "john@example.com" }
validate customer
>>= update
>>= send
|> shouldEqual (ok { id = 42; name = "John"; email = "john@example.com" })
[<Test>]
let ``apply valid inputs should create valid customer``() =
Create 42 "John" "john@example.com"
|> shouldEqual (ok { id = 42; name = "John"; email = "john@example.com" })
[<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