Generating types with build-in invariant preservation using C# source generators (2 / x) : Composition to form complex types
In the last post about "Generating types with build-in invariant preservation using C# source generators" I mentioned composing instances of Validated<T> in a smart way to form complex types that in turn also perverse their invariants. This post will go more into detail on how to do that.
Validated<T> captures the essence of wetter or not an instance of a type T is in a valid state or not. Validated<T> has 2 subtypes, namely Valid<T> and Invalid<T>. You will never guess what they represent ... Ok, you did. Valid<T> indeed means that whatever the state an instance of T is in, it is in a valid state. Invalid<T>, ... well I think you get it.
So how do we guarantee the instance of T is in a valid or invalid state? Well, by validating the input that leads to a state change. When programming in a functional style, which we are doing here, we don't want to let the state of an instance of T change at all after it has been created. So the place where we need to validate the input is when the instance will be constructed. When the validation fails, we will return an instance of Invalid<T> which holds one or more validation error messages explaining why the state was deemed invalid. The following code shows the same example (the generated code) as in the previous post:
public sealed partial record Name{ public static Validated<Name> Create(string value) => string.IsNullOrEmpty(value) ? Invalid<string>("The string may not be null or empty") : Valid(value);
public string Value { get; }
private Name(string value) { Value = value; }
public override string ToString() => Value.ToString(); public static implicit operator String(Name value) => value.Value;}
So now we have simple value types that protect the invariant, you probably wonder how that works when you want to compose these values into a more complex type? Let's start with a simple domain in which we want to make sure only adults can order an alcoholic drink. To make sure we are dealing with an adult, we want to capture this fact into the type system.
public static Beer Order(Adult adult) {}
[Validated<int, IsGreaterOrEqualTo18>]
public partial record Age{ }
[Validated<string, IsNotNullOrEmpty>]
public partial record FirstName { }
[Validated<string, IsNotNullOrEmpty>]
public partial record LastName{ }
public class IsGreaterOrEqualTo18 : Validity<int>
{
public static Validated<int> Validate(int value) =>
value < 18
? Invalid<int>("The value must be greater or equal to 18")
: Valid(value);
}
public sealed partial record Age{ public static Validated<Age> Create(Integer value) {
var result = from validated in global::Radix.Data.Integer.Validity.IsGreaterOrEqualTo18.Validate(value)
select new Age(validated);
return result;
}
public Integer Value { get; }
private Age(Integer value) { Value = value; }
public override string ToString() => Value.ToString(); public static implicit operator Integer(Age value) => value.Value;}
public record Adult{ private Adult(Age age, FirstName firstName, LastName lastName) {
Age = age;
FirstName = firstName;
LastName = lastName ;
}
private static Func<Age, FirstName, LastName, Adult> New => (age, firstName, lastName) => new Adult(age, firstName, lastName);
public static Validated<Adult> Create(int age, string firstName, string lastName) => Valid(New)
.Apply(Age.Create(age))
.Apply(FirstName.Create(firstName))
.Apply(LastName.Create(lastName))
public Age Age { get; }
public FirstName FirstName { get; }
public LastName LastName { get; }
}
Now, we will write the same code structure for all kinds of complex types like Adult which are composed out of Validated<T> 's... Hmmm..... So I should probably write a generator for this :)
When we create an instance of Adult, we actually get an instance of Validated<Adult>. So how can we work with the wrapped Adult instance? We use pattern matching. Valid<T> is a record, which comes with a deconstructor out of the box. To me the clearest way is to use a switch statement/expression:
switch (validatedAdult)
{
case Valid<Adult> (var adult): var beer = Order(adult); break;
case Invalid<Adult> error:
// handle the invalid case. error has a property Reasons that contains all validation error // message
break;
}
Comments
Post a Comment