Validation With Tagless Final
This article will discuss a validation framework with Tagless Final and demonstrate how the proposed validation framework can improve the quality of encoding.
Join the DZone community and get the full member experience.
Join For FreeIn one of our previous articles, Validation for Free in Scala, we discussed a validation framework by lifting validators to monad “for free”. In this article, we will discuss a validation framework with Tagless Final. Through examples, we demonstrate how the proposed validation framework can improve the quality of encoding.
Validation
Validation falls into two categories: Fail Fast and Error Accumulation. In Fail Fast scenario, validators return monadic validation results; a for-comprehension can be used to complete the validation. The following code returns the first error it encounters without executing the rest of the validation:
for { name <- UserValidator.validateName(name)
phoneNumber <- UserValidator.validatePhoneNumber(phone)
age <- UserValidator.validateAge(age)
} yield User(name, phoneNumber, age)
Note that as long as the wrapper of the validation result is monadic, this code always works. There are a number of monadic wrappers to wrap the validation result to represent the error case, for instance, None to represent the error if Option is the wrapper; Throwable to represent the error if Try is a wrapper, or customized error object to represent the error using Either.
In Error Accumulation, by using applicatives instead of monads, validation is carried out, and errors are accumulated even if intermediate evaluation fails, such that all the errors can be collected in one evaluation. However, Option, Try, or Either can only hold one result; they only work in fast fail scenarios but not in error accumulation validation.
Cats is a library that provides abstractions for functional programming in the Scala programming language. Cats provide a very effective wrapper called Validated for error accumulation validation. Errors can be modeled as ApplicativeError, and customized errors can be accumulated in Validated in a NonEmptyList.
In many cases, we do not want to commit to a container too early. Now the question is, how can we encode a validation that is agnostic to the container of the validation result?
Here comes Tagless Final.
Tagless Final
Tagless Final style is a method of embedding domain-specific languages (DSLs) in a typed functional host language. It is a functional pattern that allows to compose of a set of functions and ensures type safety. It is centered around interpreters; for instance, a validator of a validation framework is an interpreter in the Tagless Final context. A higher kind type F[_] is used to annotate the operators and the operands such that type checking is performed at compile time and evaluation maintains type safety at run time. F[_] is an abstraction of the wrapper in which the structure of the program is preserved. There is a rich literature on Tagless Final. In this article, we do not intend to elaborate on this coding pattern. Instead, we will discuss how Tagless Final can be used to improve our validation.
Our validation framework contains the following parts:
- A domain to describe the problem;
- Validation Algebra or DSL, an abstraction of the validation rules;
- An interpreter that implements the validation rules;
- A program to execute the validation rules.
Domain
Suppose a User has Name, Phone, Email, and Age attributes::
Our validation is to validate each attribute and report the following errors:
case class Name(value:String)
case class Phone(value:String)
case class Email(value:String)
case class Age(value:Int)
case class User(name: Name, phone:Phone, email:Email, age:Age)
sealed trait UserError
case object InvalidName extends UserError
case object InvalidAge extends UserError
case object InvalidPhoneNumber extends UserError
case object InvalidEmail extends UserError
Above, our validation is to validate each attribute and report the following errors.
Algebra
Our DSL has a simple operation:
trait UserValidator[F[_]] extends Validator[F, User]
{
def validate:F[User]
}
Where F[_] is the type constructor as a container of validation result.
Interpreter
The interpreter is a generic function that takes a user and returns the validation result in the container F[_]. The interpreter uses an applicative in cats that takes the result of validation results and wraps it in higher-kind type F.
def interpreter: User => F[User] = { user => {
(User.apply _).curried.pure[F] <*>
validateName(user.name) <*>
validatePhone(user.phone) <*>
validateEmail(user.email) <*>
validateAge(user.age)
}
Validation errors are wrapped in F[_] and modeled as ApplicativeError to support error accumulation. The interpreter can be easily extended in the Tagless Final solution, as we demonstrated to support Option, Try, Either, and Validated (Cats) as typeclasses.
Program
Finally, a program that supplies the user instance to the interpreter and executes it. The interpreter typeclass is implicitly imported into the scope such that various containers are tested. For instance:
import com.taglessfinal.validator.userValidatorOptionInterpreter
user1.validate.tap(println)
import com.taglessfinal.validator.userValidatorTryInterpreter
user1.validate.tap(println)
import com.taglessfinal.validator.userValidatorEitherInterpreter
user1.validate.tap(println)
import com.taglessfinal.validator.userValidatorValidatedInterpreter
user1.validate.tap(println)
The implementation of validating rules is trivial.
Summary
Tagless Final pattern, coupled with type classes, provides an elegant solution to validation framework implementation. The solution is type-safe, extensible, and agnostic to the container of validation results; the solution works both in a fail-fast fashion and in error accumulation. It can allow the project to avoid committing to validation scenarios too early and be flexible and extensible.
In this article, we discussed the specifics of the Validation and the Tagless Final pattern and discussed how Tagless Final can be used to implement an extensible validation DSL, and finally, provided an implementation of a validation framework in Tagless Final that supports four different containers.
Published at DZone with permission of Michael Wang. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments