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