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

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)
}
}
}