Free applicative functors enable building parallel, analyzable computations that don't require the sequential binding power of monads, allowing for better optimization and parallelization.
sealed abstract class FreeApplicative[F[_], A] extends Product with Serializable
// Construction
def pure[F[_], A](a: A): FreeApplicative[F, A]
def lift[F[_], A](fa: F[A]): FreeApplicative[F, A]
// Applicative Operations
def ap[B](b: FreeApplicative[F, A => B]): FreeApplicative[F, B]
def map[B](f: A => B): FreeApplicative[F, B]
def map2[B, C](fb: FreeApplicative[F, B])(f: (A, B) => C): FreeApplicative[F, C]
// Execution and Analysis
def foldMap[G[_]](f: F ~> G)(implicit G: Applicative[G]): G[A]// Natural transformations
def compile[F[_], G[_]](f: FunctionK[F, G]): FunctionK[FreeApplicative[F, ?], FreeApplicative[G, ?]]
def foldMap[F[_], G[_]: Applicative](f: FunctionK[F, G]): FunctionK[FreeApplicative[F, ?], G]
def analyze[F[_], M: Monoid](f: FunctionK[F, λ[α => M]]): FunctionK[FreeApplicative[F, ?], λ[α => M]]implicit def catsFreeApplicativeForFreeApplicative[F[_]]: Applicative[FreeApplicative[F, ?]]import cats.data.{NonEmptyList, Validated}
import cats.implicits._
sealed trait ValidationA[A]
case class CheckNotEmpty(field: String, value: String) extends ValidationA[String]
case class CheckMinLength(field: String, value: String, min: Int) extends ValidationA[String]
case class CheckEmail(field: String, value: String) extends ValidationA[String]
type Validation[A] = FreeApplicative[ValidationA, A]
def checkNotEmpty(field: String, value: String): Validation[String] =
FreeApplicative.lift(CheckNotEmpty(field, value))
def checkMinLength(field: String, value: String, min: Int): Validation[String] =
FreeApplicative.lift(CheckMinLength(field, value, min))
def checkEmail(field: String, value: String): Validation[String] =
FreeApplicative.lift(CheckEmail(field, value))case class User(name: String, email: String, password: String)
def validateUser(name: String, email: String, password: String): Validation[User] = {
val validName = checkNotEmpty("name", name)
val validEmail = (checkNotEmpty("email", email), checkEmail("email", email)).mapN((_, email) => email)
val validPassword = (checkNotEmpty("password", password), checkMinLength("password", password, 8)).mapN((_, pwd) => pwd)
(validName, validEmail, validPassword).mapN(User.apply)
}import cats.arrow.FunctionK
type ValidationResult[A] = Validated[NonEmptyList[String], A]
val validationInterpreter = new FunctionK[ValidationA, ValidationResult] {
def apply[A](fa: ValidationA[A]): ValidationResult[A] = fa match {
case CheckNotEmpty(field, value) =>
if (value.nonEmpty) Validated.valid(value)
else Validated.invalid(NonEmptyList.one(s"$field cannot be empty"))
case CheckMinLength(field, value, min) =>
if (value.length >= min) Validated.valid(value)
else Validated.invalid(NonEmptyList.one(s"$field must be at least $min characters"))
case CheckEmail(field, value) =>
if (value.contains("@")) Validated.valid(value)
else Validated.invalid(NonEmptyList.one(s"$field must be a valid email"))
}
}
// Execute validation
val userValidation = validateUser("Alice", "alice@example.com", "secret123")
val result: ValidationResult[User] = userValidation.foldMap(validationInterpreter)// Analyze validation structure
val analyzeInterpreter = new FunctionK[ValidationA, λ[α => List[String]]] {
def apply[A](fa: ValidationA[A]): List[String] = fa match {
case CheckNotEmpty(field, _) => List(s"empty-check:$field")
case CheckMinLength(field, _, min) => List(s"length-check:$field:$min")
case CheckEmail(field, _) => List(s"email-check:$field")
}
}
val checks: List[String] = userValidation.analyze(analyzeInterpreter)
// Result: List("empty-check:name", "empty-check:email", "email-check:email", "empty-check:password", "length-check:password:8")import cats.effect.IO
import cats.Parallel
// Interpreter that supports parallel execution
val asyncValidationInterpreter = new FunctionK[ValidationA, IO] {
def apply[A](fa: ValidationA[A]): IO[A] = fa match {
case CheckNotEmpty(field, value) =>
IO {
Thread.sleep(100) // Simulate async work
if (value.nonEmpty) value else throw new RuntimeException(s"$field is empty")
}
case CheckMinLength(field, value, min) =>
IO {
Thread.sleep(100) // Simulate async work
if (value.length >= min) value else throw new RuntimeException(s"$field too short")
}
case CheckEmail(field, value) =>
IO {
Thread.sleep(100) // Simulate async work
if (value.contains("@")) value else throw new RuntimeException(s"$field invalid email")
}
}
}
// This will execute validations in parallel when possible
val asyncResult: IO[User] = userValidation.foldMap(asyncValidationInterpreter)// More complex example with multiple field types
sealed trait FormA[A]
case class ValidateRequired[T](field: String, value: Option[T]) extends FormA[T]
case class ValidateRange(field: String, value: Int, min: Int, max: Int) extends FormA[Int]
case class ValidatePattern(field: String, value: String, pattern: String) extends FormA[String]
type Form[A] = FreeApplicative[FormA, A]
def required[T](field: String, value: Option[T]): Form[T] =
FreeApplicative.lift(ValidateRequired(field, value))
def range(field: String, value: Int, min: Int, max: Int): Form[Int] =
FreeApplicative.lift(ValidateRange(field, value, min, max))
def pattern(field: String, value: String, regex: String): Form[String] =
FreeApplicative.lift(ValidatePattern(field, value, regex))
case class Registration(name: String, age: Int, email: String, phone: String)
def validateRegistration(
name: Option[String],
age: Option[Int],
email: Option[String],
phone: Option[String]
): Form[Registration] = {
val validName = required("name", name)
val validAge = required("age", age).ap(range("age", age.getOrElse(0), 13, 120).map(Function.const))
val validEmail = required("email", email).ap(pattern("email", email.getOrElse(""), ".*@.*").map(Function.const))
val validPhone = required("phone", phone).ap(pattern("phone", phone.getOrElse(""), "\\d{10}").map(Function.const))
(validName, validAge, validEmail, validPhone).mapN(Registration.apply)
}analyze to inspect and optimize computation structure before executionfoldMap