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.
Cofree comonads are the dual of Free monads, representing infinite data structures with context-dependent computation. They're useful for modeling streams, environments, and other structures where you need to extract values from context.
final case class Cofree[S[_], A](head: A, tail: Eval[S[Cofree[S, A]]])
// Construction
def unfold[F[_], A](a: A)(f: A => F[A])(implicit F: Functor[F]): Cofree[F, A]
def ana[F[_], A, B](a: A)(coalg: A => F[A], f: A => B)(implicit F: Functor[F]): Cofree[F, B]
def anaEval[F[_], A, B](a: A)(coalg: A => Eval[F[A]], f: A => B)(implicit F: Functor[F]): Cofree[F, B]
// Access
def head: A
def tail: Eval[S[Cofree[S, A]]]
def tailForced: S[Cofree[S, A]]
def extract: A // Alias for head
// Comonadic Operations
def coflatMap[B](f: Cofree[S, A] => B)(implicit S: Functor[S]): Cofree[S, B]
def coflatten(implicit S: Functor[S]): Cofree[S, Cofree[S, A]]
// Transformations
def map[B](f: A => B)(implicit S: Functor[S]): Cofree[S, B]
def transform[B](f: A => B, g: Cofree[S, A] => Cofree[S, B])(implicit S: Functor[S]): Cofree[S, B]
def mapBranchingRoot(nat: S ~> S)(implicit S: Functor[S]): Cofree[S, A]
def mapBranchingS[T[_]](nat: S ~> T)(implicit S: Functor[S]): Cofree[T, A]
def mapBranchingT[T[_]](nat: S ~> T)(implicit T: Functor[T]): Cofree[T, A]
// Evaluation
def forceTail: Cofree[S, A]
def forceAll(implicit S: Functor[S]): Cofree[S, A]// Catamorphisms
def cata[F[_], A, B](cof: Cofree[F, A])(folder: (A, F[B]) => Eval[B])(implicit F: Traverse[F]): Eval[B]
def cataM[F[_], M[_], A, B](cof: Cofree[F, A])(folder: (A, F[B]) => M[B])(inclusion: Eval ~> M)(implicit F: Traverse[F], M: Monad[M]): M[B]implicit def catsFreeComonadForCofree[S[_]: Functor]: Comonad[Cofree[S, ?]]
implicit def catsFreeTraverseForCofree[S[_]: Traverse]: Traverse[Cofree[S, ?]]import cats.implicits._
// Create an infinite stream of integers
val naturals: Cofree[Option, Int] = Cofree.unfold(1)(n => Some(n + 1))
// Access elements
val first = naturals.head // 1
val second = naturals.tail.value.map(_.head).getOrElse(0) // 2
// Map over the stream
val doubled: Cofree[Option, Int] = naturals.map(_ * 2)
val firstDoubled = doubled.head // 2// Rose tree structure
type RoseTree[A] = Cofree[List, A]
// Create a tree
val tree: RoseTree[String] = Cofree(
"root",
Eval.now(List(
Cofree("child1", Eval.now(List(
Cofree("grandchild1", Eval.now(Nil)),
Cofree("grandchild2", Eval.now(Nil))
))),
Cofree("child2", Eval.now(Nil))
))
)
// Extract the root
val root = tree.extract // "root"
// Map over all nodes
val uppercased: RoseTree[String] = tree.map(_.toUpperCase)// Configuration environment
case class Config(baseUrl: String, timeout: Int, retries: Int)
// Environment stack - each level can have different config
type Environment[A] = Cofree[List, (Config, A)]
def withConfig[A](config: Config, value: A, children: List[Environment[A]]): Environment[A] =
Cofree((config, value), Eval.now(children))
// Create nested environment
val env: Environment[String] = withConfig(
Config("http://api.example.com", 5000, 3),
"api",
List(
withConfig(
Config("http://api.example.com", 1000, 1),
"fast-endpoint",
Nil
),
withConfig(
Config("http://api.example.com", 10000, 5),
"slow-endpoint",
Nil
)
)
)
// Extract configuration at any level
def getConfig[A](env: Environment[A]): Config = env.extract._1
def getValue[A](env: Environment[A]): A = env.extract._2
val rootConfig = getConfig(env) // Config("http://api.example.com", 5000, 3)
val rootValue = getValue(env) // "api"// Binary tree functor
sealed trait BinaryF[A]
case class Node[A](left: A, right: A) extends BinaryF[A]
case class Leaf[A]() extends BinaryF[A]
implicit val binaryFunctor: Functor[BinaryF] = new Functor[BinaryF] {
def map[A, B](fa: BinaryF[A])(f: A => B): BinaryF[B] = fa match {
case Node(left, right) => Node(f(left), f(right))
case Leaf() => Leaf()
}
}
type BinaryTree[A] = Cofree[BinaryF, A]
// Create a binary tree
def node[A](value: A, left: BinaryTree[A], right: BinaryTree[A]): BinaryTree[A] =
Cofree(value, Eval.now(Node(left, right)))
def leaf[A](value: A): BinaryTree[A] =
Cofree(value, Eval.now(Leaf()))
val binaryTree: BinaryTree[Int] = node(
1,
node(2, leaf(4), leaf(5)),
node(3, leaf(6), leaf(7))
)// Sum all values in a binary tree
def sumTree(tree: BinaryTree[Int]): Int = {
val folder: (Int, BinaryF[Int]) => Eval[Int] = {
case (value, Node(left, right)) => Eval.now(value + left + right)
case (value, Leaf()) => Eval.now(value)
}
Cofree.cata(tree)(folder).value
}
val total = sumTree(binaryTree) // 28// Navigate through a Cofree structure
def navigate[S[_]: Functor, A](cofree: Cofree[S, A]): NavigationContext[S, A] = {
NavigationContext(cofree, Nil)
}
case class NavigationContext[S[_], A](
current: Cofree[S, A],
breadcrumbs: List[S[Cofree[S, A]]]
) {
def extract: A = current.extract
def down(selector: S[Cofree[S, A]] => Option[Cofree[S, A]])(implicit S: Functor[S]): Option[NavigationContext[S, A]] = {
selector(current.tailForced).map { next =>
NavigationContext(next, current.tailForced :: breadcrumbs)
}
}
def up: Option[NavigationContext[S, A]] = breadcrumbs.headOption.map { parent =>
NavigationContext(Cofree(current.extract, Eval.now(parent)), breadcrumbs.tail)
}
}
// Example usage with List functor (non-deterministic choice)
val listCofree: Cofree[List, String] = Cofree("root", Eval.now(List(
Cofree("child1", Eval.now(Nil)),
Cofree("child2", Eval.now(Nil))
)))
val nav = navigate(listCofree)
val maybeChild = nav.down(_.headOption) // Navigate to first childimport cats.effect.IO
// Create an effectful stream
def effectfulStream[A](initial: A)(next: A => IO[Option[A]]): IO[Cofree[Option, A]] = {
def go(current: A): IO[Cofree[Option, A]] = {
next(current).flatMap {
case Some(nextVal) => go(nextVal).map(tail => Cofree(current, Eval.now(Some(tail))))
case None => IO.pure(Cofree(current, Eval.now(None)))
}
}
go(initial)
}
// Fibonacci stream
val fibStream: IO[Cofree[Option, (BigInt, BigInt)]] = effectfulStream((BigInt(0), BigInt(1))) {
case (a, b) => IO.pure(Some((b, a + b)))
}
// Take first n elements
def take[A](n: Int, cofree: Cofree[Option, A]): List[A] = {
if (n <= 0) Nil
else {
cofree.extract :: (cofree.tailForced match {
case Some(tail) => take(n - 1, tail)
case None => Nil
})
}
}Eval for lazy evaluation of the tail structureforceTail only when you need immediate evaluationforceAll forces the entire structure - use carefully with infinite structuresInstall with Tessl CLI
npx tessl i tessl/maven-org-typelevel--cats-free-2-11