Req 1. Reject invalid input.
A ⇒ Boolean.blah(dataWasValid: Boolean, hacksEnabled: Boolean,
killTheHostages: Boolean) the compiler isn't going to help you if you
get the arguments wrong somewhere. Have you ever had a bug where you
used the ID of one data object in place of another because they were
both longs? Did you hear about the NASA mission that failed because of
mixed metric numbers (eg. miles and kilometers) being
indistinguishable?A ⇒ Validity.sealed trait Validity case object Valid extends Validity case object Invalid extends Validity type Validator[A] = A => Validity
val usernameV: Validator[String] = {
val p = "^[a-z]+$".r.pattern
s => if (p.matcher(s).matches) Valid else Invalid
}
def example(u: String): Unit =
usernameV(u) match {
case Valid => println("Fake-saving username.")
case Invalid => println("Invalid username.")
}
Effect, to be a function that neither takes input nor
output.scalaz.effect.IO is a decent catch-all for effects.)type Effect = () => Unit
def fakeSave: Effect = () => println("Fake save")
Either[A,B] which can be inhabited by either Left[A] or
Right[B] and we'll use this to return either an effect or failure
reason.def example(u: String): Either[String, Effect] =
usernameV(u) match {
case Valid => Right(fakeSave)
case Invalid => Left("Invalid username.")
}
Req 2. Explain why input is invalid.
Invalid case.case class Invalid(e: String) extends Validity
val usernameV: Validator[String] = {
val p = "^[a-z]+$".r.pattern
- s => if (p.matcher(s).matches) Valid else Invalid
+ s => if (p.matcher(s).matches) Valid else
+ Invalid("Username must be 1 or more lowercase letters.")
}
def example(u: String): Either[String, Effect] =
usernameV(u) match {
case Valid => Right(fakeSave)
- case Invalid => Left("Invalid username.")
+ case Invalid(e) => Left(e)
}
Req 3. Share reusable rules between validators.
8 = 5 + 1 + 2
number + number = numbernumber + number = number | null | InvalidArgumentException- You know you don't have to remember to check for nulls, catch any exceptions, worry about our internal AddService™ being online.
- As long as the overall order of elements is preserved, you needn't care about the order in which groups are composed. i.e. we know that a+b+c+d+e will safely yield the same result if we batch up execution of (a+b) and (c+d+e) then add their results last. And parenthesis support is already provided by the programming language.
- If ever forced into composition by some code path and you can opt out by specifying the 0 because we know that 0+x and x+0 are the same as x. No need to overload methods or whatnot.
- Identity: The 0 is what we call an identity element. 0+x = x = x+0
- Associativity: That's the ability to add/remove parentheses without changing the result.
- Closure: Always returns a result of the same type, no RuntimeExceptions, no nulls.
Validator.+ for composition and ensure it preserves the associativity
property, and Validator.id for identity (also called zero).case class Validator[A](f: A => Validity) {
@inline final def apply(a: A) = f(a)
def +(v: Validator[A]) = Validator[A](a =>
apply(a) match {
case Valid => v(a)
case e@ Invalid(_) => e
})
}
object Validator {
def id[A] = Validator[A](_ => Valid)
}
pred and regex, then use them to create our validatorsobject Validator {
def pred[A](f: A => Boolean, err: => String) =
Validator[A](a => if (f(a)) Valid else Invalid(err))
def regex(r: java.util.regex.Pattern, err: => String) =
pred[String](a => r.matcher(a).matches, err)
}
val nonEmpty = Validator.pred[String](_.nonEmpty, "must be empty")
val lowercase = Validator.regex("^[a-z]*$".r.pattern, "must be lowercase")
val usernameV = nonEmpty + lowercase
def buildErrorMessage(field: String, err: String) = s"$field $err"
def example(u: String): Either[String, Effect] =
usernameV(u) match {
case Valid => Right(fakeSave)
case Invalid(e) => Left(buildErrorMessage("Username", e))
}
Req 4. Explain all the reasons for rejection.
Invalid
appropriately...case class Invalid(e1: String, en: List[String]) extends Validity
case class Validator[A](f: A => Validity) {
@inline final def apply(a: A) = f(a)
def +(v: Validator[A]) = Validator[A](a =>
- apply(a) match {
- case Valid => v(a)
- case e@ Invalid(_) => e
- })
+ (apply(a), v(a)) match {
+ case (Valid , Valid ) => Valid
+ case (Valid , e@ Invalid(_,_)) => e
+ case (e@ Invalid(_,_), Valid ) => e
+ case (Invalid(e1,en) , Invalid(e2,em) ) => Invalid(e1, en ::: e2 :: em)
+ })
}
object Validator {
def pred[A](f: A => Boolean, err: => String) =
- Validator[A](a => if (f(a)) Valid else Invalid(err))
+ Validator[A](a => if (f(a)) Valid else Invalid(err, Nil))
}
-def buildErrorMessage(field: String, err: String) = s"$field $err"
+def buildErrorMessage(field: String, h: String, t: List[String]): String = t match {
+ case Nil => s"$field $h"
+ case _ => (h :: t).zipWithIndex.map{case (e,i) => s"${i+1}) $e"}.mkString(s"$field ", ", ", ".")
+}
def example(u: String): Either[String, Effect] =
usernameV(u) match {
case Valid => Right(fakeSave)
- case Invalid(e) => Left(buildErrorMessage("Username", e))
+ case Invalid(h, t) => Left(buildErrorMessage("Username", h, t))
}
NonEmptyList[A] is a better replacement for
A, List[A] like I've done in Invalid. The same thing can also be
achieved by OneAnd[List, A]. In fact OneAnd is a good way to have
compiler-enforced non-emptiness.)Req 5. Omit mutually-exclusive or redundant error messages.
andIfSuccessful so that
we can say nonEmpty andIfSuccessful containsNumber to indicate
a validator that will only check for numbers if data isn't empty.Validator:def andIfSuccessful(v: Validator[A]) = Validator[A](a =>
apply(a) match {
case Valid => v(a)
case e@ Invalid(_,_) => e
})
Conclusion
It's not how I would've approached code years back in my OO/Java era, nor is it like any of the code I came across written by others in that job. As an experiment I started fulfilling these requirements in Java the way old me used to code and there was a loooot of wasted code between requirements. I'd get all annoyed at each new step, so much so that I didn't even bother finishing. On the contrary, I enjoyed writing the FP.
Right, what conclusions can we draw?
FP is simple. Each validation is a single function in a wrapper.