Coming from the great book "Functional Programming in C#", I try to run a bunch of validators on an object and harvest all the errors that multiple different validators might find.
The book shows a solution where all errors are created by mapping the sequence of validators against their result when invoked with the test object.
So I come up with
List<Error> errors;
The problem now simply is: how do I instantiate my final Validation with this list of errors? I mean, a Validation<Error, T> already knows how to handle a sequence of my Error type!
The Prelude's helper function though only accepts a single value for error. Why?
So this fails to compile:
{
var errors = validators
.Map(validate => validate(t))
.Bind(v => v.Match(Fail: errs=>Some(errs.Head), Succ: _ => None))
.ToList();
return errors.Count == 0
? Success<Error, T>(t)
: Fail(errors.ToSeq());
};
Because Prelude.Fail does only take ONE Error, but not my sequence.
Can anybody explain to me how this might be done with Language Ext?
Thanks!
I believe the example was something like
static Func<string, Validation<Error, string>> ShouldBeOfLength(int n) => s =>
s.Length == n
? Success<Error, string>(s)
: Fail<Error, string>(Error.New(($"{s} should be of length {n}")));
static Func<string, Validation<Error, string>> ShouldBeLowerCase => s =>
s == s.ToLower()
? Success<Error, string>(s)
: Fail<Error, string>(Error.New($"{s} should be lower case"));
public class Error : NewType<Error, string>
{
public Error(string value) : base(value) { }
}
If I'm undertstanding you correctly you can do the following
var test = "USA";
(ShouldBeLowerCase(test) | ShouldBeOfLength(2)(test))
.Apply(value => value)
.Match(
Succ: ... // Action<string>
Fail: ... // Action<Seq<Error>>
);
Or
var test = "USA";
List(ShouldBeLowerCase, ShouldBeOfLength(2))
.Map(validator => validator(test))
// this returns a validation that you can match on
.Sequence()
.Match(Succ: ..., Fail: ...)
You can also convert the resulting Validation to an Either
var test = "USA";
List(ShouldBeLowerCase, ShouldBeOfLength(2))
.Map(validator => validator(test))
.Sequence()
.ToEither()
.Match(Right: ..., Left: ...)
You can find more examples at https://github.com/louthy/language-ext/blob/master/LanguageExt.Tests/ValidationTests.cs
Happy to know if there's a better way though
Thanks for your replies - one easy answer to my question was right in front of me and I just didn't see it.
My code compiles fine like this:
{
var errors = validators
.Map(validate => validate(t))
.Bind(v => v.Match(Fail: errs=>Some(errs.Head), Succ: _ => None))
.ToList();
return errors.Count == 0
? Success<Error, T>(t)
: errors.ToSeq();
};
Apparently, I do not need the Fail method that lifts a single Error to a Validation.Fail with a sequence of errors containing only this single error. Just supplying the sequence uses automatic conversion to a Validation.Fail!
I think what @Zordid was trying to say is that we need some means to accumulate all the errors instead of short circuiting the validation. I think it will be something like:
public static Validation<Seq<E>, T> Combine<E, T1, T2, T>(Validation<E, T1> v1, Validation<E, T2> v2, Func<T1, T2, T> mapper)
{
// ...
}
which should have the same behavior as Validation.combine in Vavr. Please refer the doc for more information.
@wawaforya There is already a mechanism to construct a Validation with a sequence of errors:
```c#
Validation
Short circuiting happens when you use bind (i.e. a series of `from ... in ...` statements). If you think about it, this is necessary, because if I do this:
```c#
from x in validX
from y in validY
select x + y;
Then allowing the computation to continue after validX has failed to extract x would lead to operations on undefined state. So, the monadic operation must short cut. This is why the computation to gather all errors is an applicative operation: each term can be computed independently and then brought together for a final operation.
Therefore you need to use Apply. If you look at the ValidationTests.cs unit tests, you'll see all of these techniques in action:
```c#
public static Validation
{
var fakeDateTime = new DateTime(year: 2019, month: 1, day: 1);
var cardHolderV = ValidateCardHolder(cardHolder);
var numberV = DigitsOnly(number) | MaxStrLength(16)(number);
var validToday = ValidExpiration(fakeDateTime.Month, fakeDateTime.Year);
// This falls back to monadic behaviour because validToday needs both
// a month and year to continue.
var monthYear = from m in ToInt(expMonth).Bind(ValidMonth)
from y in ToInt(expYear).Bind(PositiveNumber)
from my in validToday(m, y)
select my;
// The items to validate are placed in a tuple, then you call apply to
// confirm that all items have passed the validation. If not then all
// the errors are collected. If they have passed then the results are
// passed to the lambda function allowing the creation of a the
// CreditCard object.
return (cardHolderV, numberV, monthYear).Apply((c, num, my) =>
new CreditCard(c, num, my.month, my.year));
}
`cardHolderV`, `numberV`, and `monthYear` are three `Validation` monads. They're grouped into a tuple:
```c#
(cardHolderV, numberV, monthYear)
And then the extension method Apply is run on that tuple. Which, when give a three parameter lambda, can use the success values to generate a new CreditCard value. If any fail, then the lambda doesn't get called and the errors are aggregated.
You may also notice that the numberV monad is generated by appending two other monads together:
c#
var numberV = DigitsOnly(number) | MaxStrLength(16)(number);
This can be done when all the monadic types in the expression are the same, and only care about the first successful result being returned. And so this will validate the the number is all digits and that it's not longer than 16 characters. Again, the errors are aggregated.
monthYear actually makes use of the monad to short-cut if anything fails. So, you can have the best of both worlds.
@louthy I am sorry for that I didn't have some tests on my own. I found that the Fail of Validation<FAIL, SUCCESS> is initially a Seq<FAIL>. When we Match the validation, all the errors are provided as parameter. Great design!!! Thank you very much.
Most helpful comment
@wawaforya There is already a mechanism to construct a.Fail(Seq fail)
Validationwith a sequence of errors:```c#
Validation
Then allowing the computation to continue after
validXhas failed to extractxwould lead to operations on undefined state. So, the monadic operation must short cut. This is why the computation to gather all errors is an applicative operation: each term can be computed independently and then brought together for a final operation.Therefore you need to use ValidateCreditCard(string cardHolder, string number, string expMonth, string expYear)
Apply. If you look at theValidationTests.csunit tests, you'll see all of these techniques in action:```c#
public static Validation
{
var fakeDateTime = new DateTime(year: 2019, month: 1, day: 1);
var cardHolderV = ValidateCardHolder(cardHolder);
var numberV = DigitsOnly(number) | MaxStrLength(16)(number);
var validToday = ValidExpiration(fakeDateTime.Month, fakeDateTime.Year);
}
And then the extension method
Applyis run on that tuple. Which, when give a three parameter lambda, can use the success values to generate a newCreditCardvalue. If any fail, then the lambda doesn't get called and the errors are aggregated.You may also notice that the
numberVmonad is generated by appending two other monads together:c# var numberV = DigitsOnly(number) | MaxStrLength(16)(number);This can be done when all the monadic types in the expression are the same, and only care about the first successful result being returned. And so this will validate the the number is all digits and that it's not longer than 16 characters. Again, the errors are aggregated.
monthYearactually makes use of the monad to short-cut if anything fails. So, you can have the best of both worlds.