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.
Yoneda and Coyoneda lemmas provide performance optimization through map fusion and enable functor-like operations on types that aren't naturally functorial.
The Yoneda lemma states that Yoneda[F, A] is isomorphic to F[A] for any functor F. It enables map fusion by storing function composition rather than applying maps immediately.
abstract class Yoneda[F[_], A] extends Serializable
// Essential operation
def apply[B](f: A => B): F[B]
// Conversion
def run: F[A]
def toCoyoneda: Coyoneda.Aux[F, A, A]
// Functor operations
def map[B](f: A => B): Yoneda[F, B]
def mapK[G[_]](f: F ~> G): Yoneda[G, A]def apply[F[_], A](fa: F[A])(implicit F: Functor[F]): Yoneda[F, A]implicit def catsFreeFunctorForYoneda[F[_]]: Functor[Yoneda[F, ?]]The dual view of the Yoneda lemma. Coyoneda provides a free functor for any type constructor, even those that don't have a Functor instance.
sealed abstract class Coyoneda[F[_], A] extends Serializable
// Type members
type Pivot
val fi: F[Pivot]
// Transformation function
def k: Pivot => A
// Conversion
def run(implicit F: Functor[F]): F[A]
def foldMap[G[_]](trans: F ~> G)(implicit G: Functor[G]): G[A]
def toYoneda(implicit F: Functor[F]): Yoneda[F, A]
// Functor operations (without requiring F to be a Functor)
def map[B](f: A => B): Coyoneda.Aux[F, B, Pivot]
def mapK[G[_]](f: F ~> G): Coyoneda.Aux[G, A, Pivot]type Aux[F[_], A, B] = Coyoneda[F, A] { type Pivot = B }
def lift[F[_], A](fa: F[A]): Coyoneda[F, A]
def apply[F[_], A, B](fa: F[A])(k0: A => B): Aux[F, B, A]implicit def catsFreeFunctorForCoyoneda[F[_]]: Functor[Coyoneda[F, ?]]Free contravariant functor that enables contravariant operations on types without a Contravariant instance.
sealed abstract class ContravariantCoyoneda[F[_], A] extends Serializable
// Type members
type Pivot
val fi: F[Pivot]
// Transformation function
def k: A => Pivot
// Conversion
def run(implicit F: Contravariant[F]): F[A]
def foldMap[G[_]](trans: F ~> G)(implicit G: Contravariant[G]): G[A]
// Contravariant operations
def contramap[B](f: B => A): ContravariantCoyoneda.Aux[F, B, Pivot]
def mapK[G[_]](f: F ~> G): ContravariantCoyoneda.Aux[G, A, Pivot]type Aux[F[_], A, B] = ContravariantCoyoneda[F, A] { type Pivot = B }
def lift[F[_], A](fa: F[A]): ContravariantCoyoneda[F, A]
def apply[F[_], A, B](fa: F[A])(k0: B => A): Aux[F, B, A]implicit def catsFreeContravariantFunctorForContravariantCoyoneda[F[_]]: Contravariant[ContravariantCoyoneda[F, ?]]import cats.implicits._
// Without Yoneda - multiple traversals
val list = List(1, 2, 3, 4, 5)
val result1 = list.map(_ + 1).map(_ * 2).map(_.toString)
// With Yoneda - single traversal
val yonedaList = Yoneda(list)
val result2 = yonedaList
.map(_ + 1)
.map(_ * 2)
.map(_.toString)
.run
// result1 and result2 are equivalent, but result2 is more efficient// A type that doesn't have a Functor instance
case class NoFunctor[A](value: A, metadata: String)
// Can't do this - NoFunctor doesn't have a Functor instance:
// val mapped = NoFunctor("hello", "meta").map(_.toUpperCase)
// But we can use Coyoneda:
val lifted = Coyoneda.lift(NoFunctor("hello", "meta"))
val mapped = lifted.map(_.toUpperCase)
// When we have an interpreter that can handle NoFunctor:
val interpreter = new (NoFunctor ~> Option) {
def apply[A](fa: NoFunctor[A]): Option[A] = Some(fa.value)
}
val result = mapped.foldMap(interpreter) // Some("HELLO")import cats.effect.IO
import cats.implicits._
// Expensive operation
def expensiveComputation[A](a: A): IO[A] = IO {
Thread.sleep(1000)
println(s"Processing: $a")
a
}
// Without optimization - multiple expensive calls
def inefficient(): IO[String] = {
expensiveComputation(42)
.map(_ + 1)
.map(_ * 2)
.map(_.toString)
}
// With Yoneda optimization - single expensive call
def efficient(): IO[String] = {
val yonedaIO = expensiveComputation(42).map(Yoneda.apply[IO, Int])
yonedaIO.flatMap(_.map(_ + 1).map(_ * 2).map(_.toString).run)
}
// Alternative using Coyoneda for better composition
def coyonedaOptimized(): IO[String] = {
val coyonedaIO = expensiveComputation(42).map(Coyoneda.lift)
coyonedaIO.flatMap(_.map(_ + 1).map(_ * 2).map(_.toString).run)
}// A type that needs contravariant mapping but doesn't have an instance
case class Predicate[A](test: A => Boolean)
// Using ContravariantCoyoneda to get contravariant operations
val stringPredicate = Predicate[String](_.length > 5)
val lifted = ContravariantCoyoneda.lift(stringPredicate)
// Contramap without needing Contravariant[Predicate]
val intPredicate = lifted.contramap[Int](_.toString)
// Define an interpreter
val predicateInterpreter = new (Predicate ~> Function1[String, ?]) {
def apply[A](fa: Predicate[A]): String => A = ??? // This would need proper implementation
}
// In practice, you'd run this when you have a proper Contravariant instance:
implicit val contravariantPredicate: Contravariant[Predicate] = new Contravariant[Predicate] {
def contramap[A, B](fa: Predicate[A])(f: B => A): Predicate[B] =
Predicate(b => fa.test(f(b)))
}
val directResult = intPredicate.run // Now we can run it// Composing Yoneda with other abstractions
def optimizedPipeline[F[_]: Functor, A](fa: F[A]): F[String] = {
Yoneda(fa)
.map(_.toString)
.map(_.toUpperCase)
.map(s => s"Result: $s")
.run
}
// Using Coyoneda for generic optimization
def genericOptimization[F[_], A, B](fa: F[A])(f: A => B): Coyoneda[F, B] = {
Coyoneda.lift(fa).map(f)
}
// Chain optimizations
def chainedOptimization[F[_], A](fa: F[A]): Coyoneda[F, String] = {
genericOptimization(fa, (x: A) => x.toString)
.map(_.toUpperCase)
.map(s => s"Final: $s")
}// Simulated database query type
case class Query[A](sql: String, mapper: ResultSet => A)
// We want to optimize multiple projections into a single query
val baseQuery = Query("SELECT id, name, age FROM users", resultSet =>
User(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getInt("age"))
)
// Using Coyoneda to defer and optimize projections
val optimizedQuery = Coyoneda.lift(baseQuery)
.map(_.name) // Project to name
.map(_.toUpperCase) // Transform name
.map(s => s"User: $s") // Format
// Later, when we have a database interpreter:
implicit val queryFunctor: Functor[Query] = new Functor[Query] {
def map[A, B](fa: Query[A])(f: A => B): Query[B] =
Query(fa.sql, fa.mapper.andThen(f))
}
val finalQuery = optimizedQuery.run
// This creates a single query with the composed transformationInstall with Tessl CLI
npx tessl i tessl/maven-org-typelevel--cats-free-2-11