louthy / language-ext

C# functional language extensions - a base class library for functional programming
MIT License
6.49k stars 417 forks source link

Examples for validation #557

Closed costa100 closed 5 years ago

costa100 commented 5 years ago

Hi,

This is not an issue with the library, it is a request for examples on how to use validation. I already read the current validation tests, however, I feel it's not enough for a beginner in the FP arts :-). Particularly:

  1. Add an example on how to use the ValidationContext and ValidationUnitContext. I am interested in passing additional data to the validators. Sometimes it is necessary to query databases to validate some piece of data.

  2. Add examples on how to combine the results from validating child classes and parent classes. It would nice to also show an example where a validation is invoked only if a previous validation passed (perhaps add a & operator - I used bind below). I played with this stuff and here is some code that I ran in linqpad.

void Main()
{
    //  var x = (1, 2);
    //  x.Dump();

    ValidateList(new List<string> {"", "1", "", "a", "200"}).Dump();

    Parent parent = new Parent {
        Children1 = new List<Child1> {
            new Child1 {Id = "", Name = "Ai"},
            new Child1 {Id = "1", Name = "X"},
        },
        Children2 = new List<Child2> {
            new Child2 {Id = "", Name = "Ai", Child1Id = "100"},
            new Child2 {Id = "1", Name = "Y", Child1Id = "1"},
        },

    };

    ValidatorsParent(parent).Dump();
}

public class Error : NewType<Error, string>
{
    public Error(string e) : base(e) { }
}

public static Validation<Error, List<String>> ValidateList(List<string> list)
{
    //var errors = list.Map(s => Validators(s)).Filter(v => v.IsFail).SelectMany(value => value.FailToSeq()).ToList();//Map(v => v.FailToSeq().Head).ToList();

    var errors = list.Map(s => Validators(s)).Bind(v => v.Match(Fail: errs => Some(errs), Succ: _ => None)).Bind(x => x).ToSeq(); //.Sequence();
    return errors.Count() == 0 ? Validation<Error, List<String>>.Success(list) : Validation<Error, List<String>>.Fail(errors);
}

public static Validation<Error, string> NonEmpty(string str) =>
  String.IsNullOrEmpty(str)
    ? Validation<Error, String>.Fail(Seq<Error>().Add(Error.New("Non empty string is required")))
    : Validation<Error, String>.Success(str);
public static Validation<Error, string> StartsWithLetterDigit(string str) =>
  !String.IsNullOrEmpty(str) && Char.IsLetter(str[0])
      ? Validation<Error, String>.Success(str)
    : Validation<Error, String>.Fail(Seq<Error>().Add(Error.New($"{str} doesn't start with a letter")));

public static Validation<Error, string> Validators(string str) => NonEmpty(str).Bind(x => StartsWithLetterDigit(str));

public static MemberExpression ExtractMemberExpression<TSource, TProperty>(Expression<Func<TSource, TProperty>> expr)
{
    MemberExpression me;
    switch (expr.Body.NodeType)
    {
        case ExpressionType.Convert:
        case ExpressionType.ConvertChecked:
            var ue = expr.Body as UnaryExpression;
            me = ue?.Operand as MemberExpression;
            break;

        default:
            me = expr.Body as MemberExpression;
            break;
    }

    return me;
}

public static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{

    MemberExpression member = ExtractMemberExpression(propertyLambda);

    if (member == null)
        throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");

    PropertyInfo propInfo = member.Member as PropertyInfo;
    if (propInfo == null)
        throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
    return propInfo;
}
public static Type TypeOf<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{
    return GetPropertyInfo(propertyLambda)
      .PropertyType;
}

public static string NameOf<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{
    return GetPropertyInfo(propertyLambda)
      .Name;
}

public class Parent
{
    public List<Child1> Children1 { get; set; }
    public List<Child2> Children2 { get; set; }
}

public class Child1
{
    public string Id { get; set; }
    public string Name { get; set; }
}

public class Child2
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Child1Id { get; set; }
}

public static Func<T, Validation<Error, T>> NonEmpty<T>(Expression<Func<T, string>> property)
{
    PropertyInfo pi = GetPropertyInfo(property);
    //string propertyName = NameOf(property);
    return obj =>
    {
        string value = property.Compile().Invoke(obj);
        return String.IsNullOrEmpty(value)
          ? Validation<Error, T>.Fail(Seq<Error>().Add(Error.New($"{pi.Name} of {pi.DeclaringType} is required")))
          : Validation<Error, T>.Success(obj);
    };
}

public static Func<T, Validation<Error, T>> ShouldStartWithVowel<T>(Expression<Func<T, string>> property)
{
    PropertyInfo pi = GetPropertyInfo(property);

    return obj =>
    {
        string value = property.Compile().Invoke(obj);
        return String.IsNullOrEmpty(value) || !List('A', 'E', 'I', 'O', 'U', 'Y').Contains(value[0])
          ? Validation<Error, T>.Fail(Seq<Error>().Add(Error.New($"{pi.Name} of {pi.DeclaringType} is required and it should start with a vowel. Its value is invalid: '{value}'.")))
          : Validation<Error, T>.Success(obj);
    };
}

// More generic method
public static Func<T, Validation<Error, T>> CheckPredicate<T>(Func<T, bool> predicate, Func<T, string> errorMessage)
{
    return obj =>
    {       
        return predicate(obj)
          ? Validation<Error, T>.Success(obj)
          : Validation<Error, T>.Fail(Seq<Error>().Add(Error.New(errorMessage(obj))));        
    };
}

public static Func<T, Validation<Error, T>> ShouldStartWithVowel2<T>(Expression<Func<T, string>> property)
{
    PropertyInfo pi = GetPropertyInfo(property);
    return obj =>
     CheckPredicate<T>(
        obj2 => { 
            string value = property.Compile().Invoke(obj2); 
            return !String.IsNullOrEmpty(value) && List('A', 'E', 'I', 'O', 'U', 'Y').Contains(value[0]);
        },
        obj2 => $"{pi.Name} of {pi.DeclaringType} is required and it should start with a vowel. Its value is invalid: '{property.Compile().Invoke(obj2)}'."
    )(obj);
}

//public static List<Validation<Error, string>> Validators2 => new List<Validation<Error, string>> { NonEmpty, StartsWithLetterDigit};

public static Validation<Error, Child1> ValidatorsChild1(Child1 child1)
{
    var v1 = NonEmpty((Child1 c) => c.Id)(child1);
    var v2 = ShouldStartWithVowel2((Child1 c) => c.Name)(child1);
    return v1 | v2;
}

public static Validation<Error, Child2> ValidateIds(Child2 child2, Parent parent)
{
    return parent.Children1.Select(c => c.Id).Contains(child2.Child1Id)
      ? Validation<Error, Child2>.Success(child2)
        : Validation<Error, Child2>.Fail(Seq<Error>().Add(Error.New($"Property Child1Id is invalid, it doesn't reference a Child1 Id: {child2.Child1Id}.")));

}
public static Validation<Error, Child2> ValidatorsChild2(Child2 child2, Parent parent)
{
    var v1 = NonEmpty((Child2 c) => c.Id)(child2);
    var v2 = ShouldStartWithVowel((Child2 c) => c.Name)(child2);
    var v3 = ValidateIds(child2, parent);       
    return v1 | v2 | v3;
}

public static Seq<Error> CollectErrors<T>(IEnumerable<Validation<Error, T>> list)
{
    //var errors = list.Map(s => Validators(s)).Filter(v => v.IsFail).SelectMany(value => value.FailToSeq()).ToList();//Map(v => v.FailToSeq().Head).ToList();

    var errors = list.Bind(v => v.Match(Fail: errs => Some(errs), Succ: _ => None)).Bind(x => x).ToSeq(); //.Sequence();
    //return errors.Count() == 0 ? Validation<Error, List<String>>.Success(list) : Validation<Error, List<String>>.Fail(errors);
    return errors;
}

public static Validation<Error, Parent> ValidatorsParent(Parent parent)
{
  // Is there a better way to do this?
  var children1Errors = CollectErrors(parent.Children1.Map(c => ValidatorsChild1(c)));
  var children2Errors = CollectErrors(parent.Children2.Map(c => ValidatorsChild2(c, parent)));

  return children1Errors.Count == 0 && children2Errors.Count == 0 
    ? Validation<Error, Parent>.Success(parent)
    : Validation<Error, Parent>.Fail(children1Errors.Concat(children2Errors));
}

/*
{
    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();
};
*/

Overall I got it working, however, I get the feeling it can be improved.

Thanks

TysonMN commented 5 years ago

Add an example on how to use the ValidationContext and ValidationUnitContext.

Can you add links to those?

TysonMN commented 5 years ago

It would nice to also show an example where a validation is invoked only if a previous validation passed

Use monadic bind to achieve that behavior. Such an example already exists. See this line in the validation tests to which you linked.

TysonMN commented 5 years ago

After adding these usings...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using LanguageExt;
using static LanguageExt.Prelude;

...I still can't get your code to compile. What are those to calls to Dump()?

TysonMN commented 5 years ago

It would nice to also show an example where a validation is invoked only if a previous validation passed

Use monadic bind to achieve that behavior. Such an example already exists. See this line in the validation tests to which you linked.

In fact, you did this here

public static Validation<Error, string> Validators(string str) => NonEmpty(str).Bind(x => StartsWithLetterDigit(str));
TysonMN commented 5 years ago

Your code contains two validation examples. I have finished going through the first one, the one that starts with

ValidateList(new List<string> {"", "1", "", "a", "200"})

I think there is a key idea that you are missing. The purpose of validation is to guard the creation of strong types.

In the validation tests in Language Ext to which you linked, there is a type called CreditCard. Its constructor accepts any two strings and any two ints. However, having a CreditCard instance is not the same as having an instance of Tuple<string, string, int, int>. The difference is that the only call to the constructor of CreditCard went through many validation steps.

The type safety of CreditCard is very good. It could be improved a bit using the smart constructor pattern. You can read more about this idea in Functional Programming in C# by Enrico Buonanno. He first mentions the idea in section 3.4.5 and then elaborates on it in section 8.5.1 in the context of validation.

Alternatively, see below how the types I created have private constructors and factory methods involving validation.

using System.Collections.Generic;
using LanguageExt;
using static LanguageExt.Prelude;

public class Example {
  public static void Main() {
    var strings = new List<string> { "", "1", "", "a", "200" };
    var validatedStrings = strings
      .Map(Validate)
      .Map(v => v.Match(s => s.Value, errors => string.Join(", ", errors))) // just to more easily see what is inside the Validation<,> instances
      .ToSeq();
  }

  public static Validation<Error, AlphaStartingString> Validate(string s) => s
    .Apply(NonnullString.New)
    .Bind(NonemptyString.New)
    .Bind(AlphaStartingString.New);

  public sealed class NonnullString : Record<NonnullString> {
    public string Value { get; }
    private NonnullString(string value) => Value = value;
    public static Validation<Error, NonnullString> New(string value) => Optional(value)
      .Map(s => new NonnullString(s))
      .ToValidation(Error.New("Nonnull string is required"));
    public static implicit operator string(NonnullString self) => self.Value;
  }

  public sealed class NonemptyString : Record<NonemptyString> {
    public string Value { get; }
    private NonemptyString(string value) => Value = value;
    public static Validation<Error, NonemptyString> New(NonnullString nonnull) => Some(nonnull)
      .Filter(s => !string.IsNullOrEmpty(s))
      .Map(s => new NonemptyString(s))
      .ToValidation(Error.New("Nonempty string is required"));
    public static implicit operator string(NonemptyString self) => self.Value;
  }

  public sealed class AlphaStartingString : Record<AlphaStartingString> {
    public string Value { get; }
    private AlphaStartingString(string value) => Value = value;
    public static Validation<Error, AlphaStartingString> New(NonemptyString nonempty) => Some(nonempty)
      .Filter(s => s.Value[0].Apply(char.IsLetter))
      .Map(s => new AlphaStartingString(s))
      .ToValidation(Error.New($"{nonempty.Value} doesn't start with a letter"));
    public static implicit operator string(AlphaStartingString self) => self.Value;
  }

  public class Error : NewType<Error, string> {
    public Error(string e) : base(e) { }
  }

}
TysonMN commented 5 years ago

Add an example on how to use the ValidationContext and ValidationUnitContext.

Can you add links to those?

I found ValidationContext and ValidationUnitContext.

The purpose of ValidationContext (respectively ValidationUnitContext) is just to pass the two arguments to Match (respectively Match) one at a time.

For example, instead of

public static void Main() {
  var strings = new List<string> { "", "1", "", "a", "200" };
  var validatedStrings = strings
    .Map(Validate)
    .Map(v => v
      .Match(s => s.Value, errors => string.Join(", ", errors))) // just to more easily see what is inside the validation instances
    .ToSeq();
}

you can write

public static void Main() {
  var strings = new List<string> { "", "1", "", "a", "200" };
  var validatedStrings = strings
    .Map(Validate)
    .Map(v => v
      .Succ(s => s.Value)
      .Fail(errors => string.Join(", ", errors))) // just to more easily see what is inside the validation instances
    .ToSeq();
}
TysonMN commented 5 years ago

For your second validation example, the one that starts with

ValidatorsParent(parent)

do you think you can improve it based on my review of your first example?

costa100 commented 5 years ago

Thank you for taking the time to review the code and for the comments!

Re: Dump() it is a function implemented in linqpad. I used linqpad to test the code. The using blocks are added in a separate dialog in linqpad, that's why they are missing from the code - sorry for the confusion.

Yes, I started to read the Functional Programming in C# book. Just a side comment, I wish he used your library instead of writing his own code - it makes it harder when you want to adopt the techniques and use the FP style, the concepts are similar but the code is not exactly the same.

I think there is a key idea that you are missing. The purpose of validation is to guard the creation of strong types.

You make a very good point. Currently, my code is mostly imperative, however, I was looking at the validation classes to implement validation for configuration classes that receive values from the app.config file. My thought was to gradually introduce the FP style in the code. Validation is good start, I think.

do you think you can improve it based on my review of your first example?

Yes, your examples, NonnullString, NonemptyString etc. are very good. I can take it from here.

Thanks again