Created
October 1, 2013 06:37
-
-
Save mausch/6774627 to your computer and use it in GitHub Desktop.
Applicative validation is simple.
This file contains hidden or 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
using System; | |
using System.Collections.Generic; | |
using System.Collections.Specialized; | |
// As opposed to magic validation libraries that rely on reflection and attributes, | |
// applicative validation is pure, total, composable, type-safe, works with immutable types, and it's easy to implement. | |
public abstract class Result<T> { | |
private Result() { } | |
private class ErrorResult<A> : Result<A> { | |
private readonly IReadOnlyDictionary<string, string> ErrorsDict; // I chose a dictionary here to associate form name with its error, but you could use any semigroup. | |
public ErrorResult(IReadOnlyDictionary<string, string> errorsDict) { | |
ErrorsDict = errorsDict; | |
} | |
public override R Match<R>(Func<A, R> success, Func<IReadOnlyDictionary<string, string>, R> errors) { | |
return errors(ErrorsDict); | |
} | |
} | |
private class SuccessResult<A> : Result<A> { | |
private readonly A Value; | |
public SuccessResult(A value) { | |
Value = value; | |
} | |
public override R Match<R>(Func<A, R> success, Func<IReadOnlyDictionary<string, string>, R> errors) { | |
return success(Value); | |
} | |
} | |
public static Result<T> Success(T value) { | |
return new SuccessResult<T>(value); | |
} | |
public static Result<T> Errors(IReadOnlyDictionary<string, string> errors) { | |
return new ErrorResult<T>(errors); | |
} | |
public abstract R Match<R>(Func<T, R> success, Func<IReadOnlyDictionary<string, string>, R> errors); | |
} | |
public static class Result { | |
public static Result<T> Success<T>(T value) { | |
return Result<T>.Success(value); | |
} | |
public static Result<T> Errors<T>(IReadOnlyDictionary<string, string> errors) { | |
return Result<T>.Errors(errors); | |
} | |
public static Result<T> Error<T>(string name, string msg) { | |
return Errors<T>(new Dictionary<string, string> {{name, msg}}); | |
} | |
public static Result<R> Select<A,R>(this Result<A> value, Func<A, R> map) { | |
return value.Match(success: v => Success(map(v)), errors: Errors<R>); | |
} | |
static IReadOnlyDictionary<K,V> Union<K,V>(this IReadOnlyDictionary<K,V> a, IReadOnlyDictionary<K,V> b) { | |
var r = new Dictionary<K, V>(); | |
foreach (var d in new[] {a,b}) | |
foreach (var kv in d) | |
r[kv.Key] = kv.Value; | |
return r; | |
} | |
public static Result<R> SelectMany<A,B,R>(this Result<A> value, Func<Result<A>, Result<B>> ap, Func<A,B,R> map) { | |
var valueB = ap(value); | |
return value.Match( | |
success: a => valueB.Match( | |
success: b => Success(map(a, b)), | |
errors: Errors<R>), | |
errors: e => valueB.Match( | |
success: _ => Errors<R>(e), | |
errors: e2 => Errors<R>(e.Union(e2)))); | |
} | |
} | |
// Model to validate & "hydrate"/bind from submitted form | |
class Model { | |
public readonly string Name; | |
public readonly int Age; | |
public Model(string name, int age) { | |
Name = name; | |
Age = age; | |
} | |
public override string ToString() { | |
return string.Format("Name: {0}, Age: {1}", Name, Age); | |
} | |
} | |
class Program { | |
// example | |
static Result<int> ValidateInt(string name, string value) { | |
int v; | |
if (int.TryParse(value, out v)) | |
return Result.Success(v); | |
return Result.Error<int>(name, "Invalid number"); | |
} | |
static Result<string> ValidateName(NameValueCollection form) { | |
if (string.IsNullOrEmpty(form[FormName])) | |
return Result.Error<string>(FormName, "Name is required"); | |
return Result.Success(form[FormName]); | |
} | |
const string FormName = "name"; | |
const string FormAge = "age"; | |
static Result<Model> Validate(NameValueCollection form) { | |
return | |
from name in ValidateName(form) | |
from age in ValidateInt(FormAge, form[FormAge]) | |
select new Model(name, age); | |
} | |
static void Main(string[] args) { | |
var form = new NameValueCollection { | |
{FormAge, "abc"}, | |
{FormName, ""}, | |
}; | |
Validate(form) | |
.Match( | |
success: model => { | |
Console.WriteLine(model); | |
return 0; | |
}, | |
errors: e => { | |
foreach (var kv in e) | |
Console.WriteLine("Error in form element {0}: {1}", kv.Key, kv.Value); | |
return 0; | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
More examples and links about applicative validation: http://bugsquash.blogspot.com/2012/03/example-of-applicative-validation-in.html