CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-typelevel--cats-free-2-11

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.

Overview
Eval results
Files

yoneda-coyoneda.mddocs/

Yoneda and Coyoneda

Yoneda and Coyoneda lemmas provide performance optimization through map fusion and enable functor-like operations on types that aren't naturally functorial.

Yoneda

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.

Core API

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]

Factory Methods

def apply[F[_], A](fa: F[A])(implicit F: Functor[F]): Yoneda[F, A]

Type Class Instances

implicit def catsFreeFunctorForYoneda[F[_]]: Functor[Yoneda[F, ?]]

Coyoneda

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.

Core API

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]

Factory Methods

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]

Type Class Instances

implicit def catsFreeFunctorForCoyoneda[F[_]]: Functor[Coyoneda[F, ?]]

ContravariantCoyoneda

Free contravariant functor that enables contravariant operations on types without a Contravariant instance.

Core API

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]

Factory Methods

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]

Type Class Instances

implicit def catsFreeContravariantFunctorForContravariantCoyoneda[F[_]]: Contravariant[ContravariantCoyoneda[F, ?]]

Usage Examples

Yoneda for Map Fusion

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

Coyoneda for Non-Functor Types

// 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")

Performance Optimization Example

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)
}

ContravariantCoyoneda Example

// 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

Composition Patterns

// 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")
}

Real-world Application: Database Query Optimization

// 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 transformation

Performance Considerations

  • Yoneda: Best for map fusion when you have multiple consecutive maps
  • Coyoneda: Useful for types without Functor instances or when you want to defer functor operations
  • Map fusion: Both Yoneda and Coyoneda can eliminate intermediate data structures
  • Memory efficiency: Deferred operations reduce temporary object creation
  • Composition cost: The abstractions have some overhead, so use judiciously
  • Stack safety: Operations are stack-safe when the underlying functor is stack-safe

Install with Tessl CLI

npx tessl i tessl/maven-org-typelevel--cats-free-2-11

docs

cofree.md

free-applicative.md

free-monads.md

free-transformer.md

index.md

interpreters.md

trampoline.md

yoneda-coyoneda.md

tile.json