You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('') and can be up to 35 characters long.
150 lines
4.7 KiB
150 lines
4.7 KiB
package green.thisfieldwas.embracingnondeterminism.data




import green.thisfieldwas.embracingnondeterminism.control.Applicative




/** A validation context representing an invalidation and absence of the desired


* term or validation and presence of the desired term.


*


* This context on the surface appears as a duplication of the `Either` context


* in that it's "one thing or the other", but there is a key difference between


* these two contexts: `Either`'s implementation of `Applicative` will short


* circuit on the first `Left` and hide the values contained in any secondary


* `Left`s. This means that if you use `Either` for validation and there are


* multiple, concurrent validation errors that you will only receive the error


* for the first one. This makes `Either` unsuitable for use as a validation


* context.


*


* `Validated` in contrast specializes its `Applicative` instance to require


* that its undesired case contain data with a `Semigroup` instance. This means


* that as undesired cases are encountered, their values are combined and


* propagated as a whole. In this way, you receive all validation errors.


*


* @tparam E


* The term representing the reason for invalidation.


* @tparam A


* The present term if valid.


*/


sealed trait Validated[+E, +A] {




/** Gets the instance of the invalid term, if it exists.


*


* @return


* The invalid instance.


* @throws NoSuchElementException


* if this is valid.


*/


def invalid: E




/** Gets the instance of the valid term, if it exists.


*


* @return


* The valid instance.


* @throws NoSuchElementException


* if this is invalid.


*/


def valid: A




/** Gets whether this is valid.


*


* @return


* True if valid.


*/


def isValid: Boolean = false




/** Gets whether this is invalid.


*


* @return


* False if invalid.


*/


def isInvalid: Boolean = false




/** Fold the `Validated` into a single value.


*


* @param invalidCase


* The function to use if `Invalid`.


* @param validCase


* The function to use if `Valid`.


* @tparam B


* The type to transform the contained `E` or `A` into.


* @return


* The new instance of `B`.


*/


def fold[B](invalidCase: E => B)(validCase: A => B): B


}




/** The valid case of the context containing a validated value.


*


* @param valid


* The valid value.


* @tparam A


* The present term if valid.


*/


case class Valid[+A](valid: A) extends Validated[Nothing, A] {




def invalid: Nothing = throw new NoSuchElementException("Valid.invalid")




override def isValid: Boolean = true




override def fold[B](invalidCase: Nothing => B)(validCase: A => B): B = validCase(valid)


}




/** The invalid case of the context, containing the reason for invalidation.


*


* @param invalid


* The reason for invalidation.


* @tparam E


* The term representing the reason for invalidation.


*/


case class Invalid[+E](invalid: E) extends Validated[E, Nothing] {




def valid: Nothing = throw new NoSuchElementException("Invalid.valid")




override def isInvalid: Boolean = true




override def fold[B](invalidCase: E => B)(validCase: Nothing => B): B = invalidCase(invalid)


}




object Validated {




import green.thisfieldwas.embracingnondeterminism.syntax.semigroup._




implicit def validatedApplicative[E: Semigroup]: Applicative[Validated[E, *]] = new Applicative[Validated[E, *]] {




/** Lifts the pure value of a computation into the context. This is an


* abstracted constructor for the `Applicative` and concretely for contexts


* `Option` it is `Some`, and for `Either` it is `Right`. It always creates


* a new context in the desired case.


*


* @param a


* The value to lift.


* @tparam A


* The type of the value.


* @return


* A new context in the desired case.


*/


override def pure[A](a: A): Validated[E, A] = Valid(a)




/** Pronounced "apply", this function takes an applicative functor of the


* form `F[A => B]` and applies it to the functor `F[A]`, giving the result


* of `F[B]`.


*


* @param ff


* The applicative functor, or lifted function.


* @param fa


* The argument context, or lifted argument.


* @tparam A


* The type of the argument.


* @tparam B


* The type of the result.


* @return


* The lifted result.


*/


override def ap[A, B](ff: Validated[E, A => B])(fa: Validated[E, A]): Validated[E, B] =


(ff, fa) match {


case (Valid(f), Valid(a)) => Valid(f(a))


case (Invalid(x), Invalid(y)) => Invalid(x + y) // propagate both undesired cases via Semigroup


case (Invalid(x), _) => Invalid(x)


case (_, Invalid(y)) => Invalid(y)


}


}


}


