Result
and the ?
operators stop the computation at the first error. While this makes sense for many use cases, there are better defaults for some use cases, like type-checking and form validation. Type checking and form validation shouldn't stop at the first error. Instead, in these use cases, the validation code should try to produce as many errors as possible after the first error.
Here is an example for a type checking signature.
fn type_check(term: Term) -> Result<Term, TypeError> {⋯}
To better support this use case, we can take inspiration from Haskell libraries that address this problem. We can introduce a new
enum Validation<T, E> { Failure(E), Success(T) }
This type is like Result<T, E>
except instead of stopping at the first error, it combines subsequent errors. The operation exemplifies this.
use Validation as V;
impl<T, E: Semigroup> Validation<T, E> {
pub fn zip_with<U, R>(
self,
other: Validation<U, E>,
f: impl FnOnce(T, U) -> R,
) -> Validation<R, E> {
match (self, other) {
(V::Failure(e0), V::Failure(e1)) => V::Failure(e0.combine(e1)),
(V::Success(_), V::Failure(e)) | (V::Failure(e), V::Success(_)) => V::Failure(e),
(V::Success(x), V::Success(y)) => V::Success(f(x, y)),
}
}
}
The difference between this and what one gets by doing something with the same intent with ?
is that it uses the Semigroup that the error type is equipped with an associative binary operation to merge the error information of both cases instead of forgetting the error information of the later failure by stopping before the second case has the opportunity to fail. Semigroup
represents a type that possesses an associative operation. For example, on Vec
-like types, a natural associative operation would be appending the contents of the right-hand side to the left-hand side.
/// (|x, y, z| x.combine(y).combine(z)) == (|x, y, z| x.combine(y.combine(z)))
trait Semigroup {
fn combine(self, other: Self) -> Self;
// Some other methods with default implementations for performance (&mut versions, etc)
}
There could be a macro that transforms something like validate!(f(x,y,z))
to x.zip_with(y, |x,y| (x,y)).zip_with(z, |(x, y), z| f(x, y, z))
. The validate!()
macro could allow arbitrary function application expressions and operator expressions.