ValidationM

The validation effect allows for the distinction between valid and invalid values in a program, accumulating the validation errors when executing it.

The validation effect, like state, supports parameterization to any type remaining type safe throughout the program declaration.

There needs to be implicit evidence of cats.mtl.MonadState[M[_], List[E]] for any runtime M[_] where E is the type of the validation error due to the constraints placed by this effect.

The validation effect comes with three basic operations valid, invalid, and errors. Apart from these, it includes a couple of combinators for accumulating errors: fromEither and fromValidatedNel.

import freestyle.free._
import freestyle.free.implicits._
import freestyle.free.effects.validation
import cats.data.State
import cats.implicits._
import cats.mtl.implicits._


sealed trait ValidationError
case class NotValid(explanation: String) extends ValidationError

val vl = validation[ValidationError]
import vl.implicits._

type ValidationResult[A] = State[List[ValidationError], A]

valid

valid lifts a valid value to the program without accumulating any errors:

import cats.instances.list._
// import cats.instances.list._

def programValid[F[_]: vl.ValidationM] =
  for {
    a <- FreeS.pure(1)
    b <- vl.ValidationM[F].valid(1)
    c <- FreeS.pure(1)
  } yield a + b + c
// programValid: [F[_]](implicit evidence$1: vl.ValidationM[F])cats.free.Free[[β$0$]cats.free.FreeApplicative[F,β$0$],Int]

programValid[vl.ValidationM.Op].interpret[ValidationResult].runEmpty
// res0: cats.Eval[(List[ValidationError], Int)] = cats.Eval$$anon$9@3dd199f6

invalid

invalid accumulates a validation error:

def programInvalid[F[_]: vl.ValidationM] =
  for {
    a <- FreeS.pure(1)
    _ <- vl.ValidationM[F].invalid(NotValid("oh no"))
    b <- FreeS.pure(1)
  } yield a + b
// programInvalid: [F[_]](implicit evidence$1: vl.ValidationM[F])cats.free.Free[[β$0$]cats.free.FreeApplicative[F,β$0$],Int]

programInvalid[vl.ValidationM.Op].interpret[ValidationResult].runEmpty
// res1: cats.Eval[(List[ValidationError], Int)] = cats.Eval$$anon$9@c8ca5c7

errors

errors allows you to inspect the accumulated errors so far:

def programErrors[F[_]: vl.ValidationM] =
  for {
    _ <- vl.ValidationM[F].invalid(NotValid("oh no"))
    errs <- vl.ValidationM[F].errors
    _ <- vl.ValidationM[F].invalid(NotValid("this won't be in errs"))
  } yield errs
// programErrors: [F[_]](implicit evidence$1: vl.ValidationM[F])cats.free.Free[[β$0$]cats.free.FreeApplicative[F,β$0$],List[ValidationError]]

programErrors[vl.ValidationM.Op].interpret[ValidationResult].runEmpty
// res2: cats.Eval[(List[ValidationError], List[ValidationError])] = cats.Eval$$anon$9@91f80b9

fromEither

We can interleave Either[ValidationError, ?] values in the program. If they have errors on the left side they will be accumulated:

def programFromEither[F[_]: vl.ValidationM] =
  for {
    _ <- vl.ValidationM[F].fromEither(Left(NotValid("oh no")) : Either[ValidationError, Int])
	a <- vl.ValidationM[F].fromEither(Right(42) : Either[ValidationError, Int])
  } yield a
// programFromEither: [F[_]](implicit evidence$1: vl.ValidationM[F])cats.free.Free[[β$0$]cats.free.FreeApplicative[F,β$0$],scala.util.Either[ValidationError,Int]]

programFromEither[vl.ValidationM.Op].interpret[ValidationResult].runEmpty
// res3: cats.Eval[(List[ValidationError], scala.util.Either[ValidationError,Int])] = cats.Eval$$anon$9@6c37c941

fromValidatedNel

We can interleave ValidatedNel[ValidationError, ?] values in the program. If they have errors in the invalid case they will be accumulated:

import cats.data.{Validated, ValidatedNel, NonEmptyList}
// import cats.data.{Validated, ValidatedNel, NonEmptyList}

def programFromValidatedNel[F[_]: vl.ValidationM] =
  for {
    a <- vl.ValidationM[F].fromValidatedNel(
	   Validated.Valid(42)
	)
	_ <- vl.ValidationM[F].fromValidatedNel(
	    Validated.invalidNel[ValidationError, Unit](NotValid("oh no"))
    )
	_ <- vl.ValidationM[F].fromValidatedNel(
	    Validated.invalidNel[ValidationError, Unit](NotValid("another error!"))
    )
  } yield a
// programFromValidatedNel: [F[_]](implicit evidence$1: vl.ValidationM[F])cats.free.Free[[β$0$]cats.free.FreeApplicative[F,β$0$],cats.data.Validated[cats.data.NonEmptyList[ValidationError],Int]]

programFromValidatedNel[vl.ValidationM.Op].interpret[ValidationResult].runEmpty
// res4: cats.Eval[(List[ValidationError], cats.data.Validated[cats.data.NonEmptyList[ValidationError],Int])] = cats.Eval$$anon$9@32fc6963

Syntax

By importing the validation effect implicits, a couple of methods are available for lifting valid and invalid values to our program: liftValid and liftInvalid.

def programSyntax[F[_]: vl.ValidationM] =
  for {
    a <- 42.liftValid
	_ <- NotValid("oh no").liftInvalid
	_ <- NotValid("another error!").liftInvalid
  } yield a
// programSyntax: [F[_]](implicit evidence$1: vl.ValidationM[F])cats.free.Free[[β$0$]cats.free.FreeApplicative[F,β$0$],Int]

programSyntax[vl.ValidationM.Op].interpret[ValidationResult].runEmpty
// res5: cats.Eval[(List[ValidationError], Int)] = cats.Eval$$anon$9@454d7c51