Free monads and related constructs for functional programming in Scala, providing Free and FreeT monads, Free applicatives, Cofree comonads, and Yoneda lemmas for DSL embedding and performance optimization.
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 executionfoldMapInstall with Tessl CLI
npx tessl i tessl/maven-org-typelevel--cats-free-2-11