Applicative validation in Scala

James Earl Douglas

March 30, 2016

Objective

Sanitize potentially invalid data.

Example

case class User(name: String, email: String, phone: String)
val james = User(null, "james@", "Banana") // Oops!

Teaser

We'll do it with Validation and its applicative functor:

sealed trait Validation[E,A]
case class Success[E,A](value: A) extends Validation[E,A]
case class Failure[E,A](value: E) extends Validation[E,A]

new Applicative[Validation[E,_]] {
  def map[A,B](fa: Validation[E,A])(f: A => B): Validation[E,B] = ...
  def ap[A,B](ff: Validation[E,A => B])(fa: Validation[E,A]): Validation[E,B] = ...
}

Outline

Functors

A functor converts a function A => B to a function F[A] => F[B].

F[A] ~~~~~~~~► F[B]
 │              ▲
 └──► A => B ───┘

See the functor laws.

F[_] is a type constructor

F[_] is parameterized by a single type variable.

There is never a bare instance of F, but only of F[A] for some type A.

Familiar examples of F[_]:

Functor type class

trait Functor[F[_]] {
  def map[A,B](fa: F[A])(f: A => B): F[B]
}

Functor[Option]

implicit val optionFunctor: Functor[Option] =
  new Functor[Option] {
    def map[A,B](fa: Option[A])(f: A => B): Option[B] =
      fa match {
        case Some(a) => Some(f(a))
        case None    => None
      }
  }

optionFunctor.map(Option(6))({ x: Int => x * 7 }) // Some(42)

FunctorOps enrichment

Sometimes* it's impractical to use the function map(fa)(f).

We can pimp a map method on to the fa value.

implicit class FunctorOps[A,F[_]:Functor](fa: F[A]) {
  def map[B](f: A => B): F[B] =
    implicitly[Functor[F]].map(fa)(f)
}
Option(6) map { x: Int => x * 7 } // Some(42)

* e.g. for-comprehensions, infix notation, OO-style

FlippedFunctorOps enrichment

Other times*, it's better to start with a function f and map it over an argument fa.

implicit class FlippedFunctorOps[A,B](f: A => B) {
  def <%>[F[_]:Functor](fa: F[A]): F[B] =
    implicitly[Functor[F]].map(fa)(f)
}

{ x: Int => x * 7 } <%> Option(6) // Some(42)

* It can be easier to read prefix-like notation.

Applicatives

An applicative converts a value F[A => B] to a function F[A] => F[B].

F[A => B] ~~~~► F[B]
    │            ▲
    └──► F[A] ───┘

See the applicative functor laws.

Applicative type class

trait Applicative[F[_]] extends Functor[F] {
  def ap[A,B](ff: F[A => B])(fa: F[A]): F[B]
}

Applicative[Option]

implicit val optionApplicative: Applicative[Option] =
  new Applicative[Option] {
    def map[A,B](fa: Option[A])(f: A => B): Option[B] =
      optionFunctor.map(fa)(f)
    def ap[A,B](ff: Option[A => B])(fa: Option[A]): Option[B] =
      ff match {
        case Some(f) =>
          fa match {
            case Some(a) => Some(f(a))
            case None    => None
          }
        case None => None
      }
  }

optionApplicative.ap(Option(6))(Option({ x: Int => x * 7 })) // Some(42)

ApplicativeOps enrichment

As before, it can be impractical to use the function ap(ff)(fa).

We can pimp an ap method on to the lifted function ff.

implicit class ApplicativeOps[A,F[_]:Applicative](fa: F[A]) {
  def ap[B](ff: F[A => B]): F[B] =
    implicitly[Applicative[F]].ap(ff)(fa)
}

Option(6) ap Option({ x: Int => x * 7 }) // Some(42)

There's an ap for that

ap converts a value F[A => B] to F[A] => F[B].

What about lifted functions of higher arity, like F[A => B => C]?

B, or not a B

That is the question.

The B in F[A => B] is a type variable.

In practice B can have any type, even a function like Y => Z.

n-ary functions

Let's simplify F[A => B => C] to F[A => β].

val fabc = ...          // F[A => B => C]
val fa   = ...          // F[A]
val fb   = ...          // F[B]

val fbc  = ap(fabc)(fa) // F[A => β] => F[A] => F[β]
val fc   = ap(fbc)(fb)  // F[B => C] => F[B] => F[C]

3-ary ApplicativeOps example

Option(2) ap (
  Option(3) ap (
    Option({ x: Int => y: Int => x * y * 7 })
  )
) // Some(42)

Handling non-curried n-ary functions

Easy; curry them!

Option(2) ap (
  Option(3) ap (
    Option({ (x: Int, y: Int) => x * y * 7 }.curried)
  )
) // Some(42)

FlippedApplicativeOps

As before, sometimes* it's better to start with a lifted function ff and apply it to a lifted argument fa.

implicit class FlippedApplicativeOps[
  A,B,F[_]:Applicative](ff: F[A => B]) {
    def <*>(fa: F[A]): F[B] =
      implicitly[Applicative[F]].ap(ff)(fa)
  }

Option({ x: Int => x * 7 }) <*> Option(6) // Some(42)

* It can be easier to read prefix-like notation.

Validation

sealed trait Validation[E,A]
case class Success[E,A](value: A) extends Validation[E,A]
case class Failure[E,A](value: E) extends Validation[E,A]

So, pretty much the same thing as Either[E,A].

Aside: Semigroup

A semigroup lets us concatenate stuff.

trait Semigroup[A] {
  def append(x: A)(y: A): A
}

Semigroup[List[A]]

We'll need a way to collect errors. Let's use lists of strings.

implicit def listSemigroup[A]: Semigroup[List[A]] =
  new Semigroup[List[A]] {
    def append(x: List[A])(y: List[A]): List[A] = x ++ y
  }

Applicative[Validation[E,_]]

Validation[E,_] is our F[_].

implicit def validationApplicative[E:Semigroup]: Applicative[
  ({type λ[α] = Validation[E,α]})#λ] =
    new Applicative[({type λ[α] = Validation[E,α]})#λ] {
      ...
    }

sbt's kind-projector plugin can clean this up as Validation[E,?]

Applicative[Validation[E,_]]#map

def map[A,B](fa: Validation[E,A])(f: A => B): Validation[E,B] =
  fa match {
    case Success(a)  => Success(f(a))
    case Failure(es) => Failure(es)
  }

Applicative[Validation[E,_]]#ap

def ap[A,B](ff: Validation[E,A => B])
           (fa: Validation[E,A]): Validation[E,B] =
  fa match {
    case Success(a) =>
      ff match {
        case Success(f)  => Success(f(a))
        case Failure(es) => Failure(es)
      }
    case Failure(es) =>
      ff match {
        case Success(f)   => Failure(es)
        case Failure(es2) => val se = implicitly[Semigroup[E]]
                             Failure(se.append(es2)(es))
      }
  }

Stepping through map and ap

case class User(name: String, email: String, phone: String)
scala> val newUser = (User.apply _).curried
newUser: String => (String => (String => User)) =
  <function1>
scala> val a = Validation.applicative[List[String]]
a: Applicative[[α]Validation[List[String],α]] =
  Validation$$anon$1@133c92b
scala> a.map(Success("James"))(newUser)
res0: Validation[List[String],String => (String => User)] =
  Success(<function1>)
scala> a.ap(res0)(Success("james@earldouglas.com"))
res1: Validation[List[String],String => User] =
  Success(<function1>)
scala> a.ap(res1)(Success("555-123-4567"))
res2: Validation[List[String],User] =
  Success(User(James,james@earldouglas.com,555-123-4567))

Regex parsing

type Parsed[A] = Validation[List[String], A]

def parseR(n: String, x: String, r: Regex): Parsed[String] =
  r.findFirstIn(x) match {
    case Some(_) => Success(x)
    case _       => Failure(List(s"""invalid ${n}: "${x}""""))
  }

parseR("name", "James", """\w+(\s\w+)+""".r)
// Failure(List(invalid name: "James"))

parseR("name", "James Douglas", """\w+(\s\w+)+""".r)
// Success("James Douglas")

Parsing with ap

case class User(name: String, email: String, phone: String)

def parse(name: String, email: String, phone: String): Parsed[User] =
  parseR("name", name, """\w+(\s\w+)*""".r).ap(
    parseR("email", email, """[^@]+@[^@]+""".r).ap(
      parseR("phone", phone, """\d{3}-\d{3}-\d{4}""".r).map(
        (User.apply _).curried
      )
    )
  )

parse("James", "james", "555-JAMES-42")
// List(invalid phone: "555-JAMES-42",
//      invalid email: "james")

parse("James", "james@earldouglas.com", "555-123-4567"):
// User(James,james@earldouglas.com,555-123-4567)

Flipped parsing with <%> and <*>

def parse(name: String, email: String, phone: String): Parsed[User] =
  (User.apply _).curried <%>
    parseR("name",  name,  """\w+(\s\w+)*""".r) <*>
    parseR("email", email, """[^@]+@[^@]+""".r) <*>
    parseR("phone", phone, """\d{3}-\d{3}-\d{4}""".r)

parse("James", "james", "555-JAMES-42")
// List(invalid email: "james",
//      invalid phone: "555-JAMES-42")

parse("James", "james@earldouglas.com", "555-123-4567")
// User(James,james@earldouglas.com,555-123-4567)

Using Scalaz

Using Scalaz is pretty much the same as what we've seen so far.

Scalaz library

build.sbt:

libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.1.0"

Scalaz imports

Functor and Applicative type classes:

import scalaz.Functor
import scalaz.Applicative

Validation data type:

import scalaz.Validation
import scalaz.Success
import scalaz.Failure

Semigroup[List] instance:

import scalaz.std.list.listMonoid

Using Cats

Again, using Cats is pretty much the same as what we've seen so far.

Cats library

build.sbt:

libraryDependencies += "org.typelevel" %% "cats" % "0.4.1"

Cats imports

Functor and Applicative type classes:

import cats.Functor
import cats.Applicative

Validated data type:

import cats.data.Validated
import cats.data.Validated.Valid
import cats.data.Validated.Invalid

Semigroup[List] instance:

import cats.implicits.listAlgebra

References