Updated on 2021-06-19: published a new post about result builders for this validation.
A program that takes any input from the outside world must validate it. In a project I worked on, I encountered a problem with the code that validates a received response and I didn’t know why validation failed because the logs only said “Rejected response X because it’s invalid”. The problem is that the validation function just returned a
Bool, which doesn’t carry any extra information as I show in this post.
This article is about a general idea of how to get more information from various processes in your program, in this case, from validation. It describes only the first steps and can be extended further.
The repository with the sample code is at https://github.com/eunikolsky/LightweightValidation.
Say we have a sample response that we need to validate:
1 2 3 4 5 6 7 8 9
The most basic validation is a function taking a response and returning a
Bool, here separated into logical, independent steps:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
That’s not a good design, as there are various problems with it:
- you can’t distinguish between the original and validated responses in terms of types, so there is always a possibility of logic errors when you accidentally use the original response when you meant only validated ones;
- it returns a plain
Boolwhich tells you nothing about why it failed.
We’ll tackle only the second point in this post.
What we need is a type that could be either a valid result or an error, which is a job for a sum type:
1 2 3 4 5 6 7
In the first step, the
error case doesn’t have any actual messages. Don’t worry, we’’ll add those shortly.
If we implement
&& for our use case, then the logic in the validation functions stays the same:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
&& combinator contains the crucial logic for the validator: if and only if both validation results are successful, then the result is successful; an error otherwise. It propagates errors from the lower-level validators (
validateUserName) to the higher-level ones (
validate) so that we don’t have to remember to check the results manually.
SimpleValidationResult is now isomorphic to a regular
See more details in the commit
Allowing specific errors
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Bool.V converter now provides some placeholder validation error so that the
ResponseValidator code doesn’t have to change. We’ll fix it in the next step. The
&& implementation is updated to correctly combine the errors from both validators.
Supplying the correct validation errors
Here we introduce the
<?> operator (instead of
Bool.V) so that our validator checks look nice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Tidying up the errors
A small last step is to make the validator code a bit nicer and not have to wrap the errors in
1 2 3 4 5 6 7 8 9 10
In our case of simple
StringErrors, this is done by conforming the type to
This test verifies the errors accumulation behavior:
1 2 3 4 5 6 7
Finally we can compare the contents of the validation functions and see how they have changed:
1 2 3 4 5 6 7 8 9 10
The logic has stayed exactly the same, we just added the explanations with a nice API! I like the result.
The result at the last step works and is much better than the original basic implementation. But it’s still a very limited implementation for the sake of simplicity in this post. Further improvements are definitely possible.
Depending on the requirements, instead of using a simple
StringError it could be preferable to use an error type specific for your validator. Then the client code could check which exact requirements failed and do something specific instead of simply showing the error strings.
&& as it is implemented now has a very specific use case. In fact, the
apply) operator from the
Applicative typeclass (interface) is the generic version of this and
&& can be implemented in terms of it.
V.error case currently holds an array of errors:
[E]; the code is forced to know that it’s an array even though the only operation it uses it the concatenation (
.error(e1 + e2) in
&&). In a more general case, it can be any
Semigroup of errors, because a semigroup is a typeclass that only defines an associative operation
<> to combine two values into one.
Extended validation implementation in the “PureScript by Example” book: https://leanpub.com/purescript/read#leanpub-auto-applicative-validation.