From 17424b4e721b049404dae1004260ba1015e42312 Mon Sep 17 00:00:00 2001 From: Mustafa Senoglu Date: Sun, 2 Aug 2026 22:46:32 +0300 Subject: [PATCH 1/2] docs: improve Func and AppFunc documentation The existing scaladoc for Func was only 2 lines and linked to a 25-page Haskell paper. This adds comprehensive documentation explaining: - What Func is and how it differs from Kleisli - The distinction between sequential (Kleisli) and parallel (Func) composition - Usage examples with AppFunc (product, compose, andThen, traverse) - A new website documentation page (docs/datatypes/func.md) - Type class instances overview Closes #2650 --- core/src/main/scala/cats/data/Func.scala | 161 ++++++++++++++++++++++- docs/datatypes/directory.conf | 1 + docs/datatypes/func.md | 159 ++++++++++++++++++++++ 3 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 docs/datatypes/func.md diff --git a/core/src/main/scala/cats/data/Func.scala b/core/src/main/scala/cats/data/Func.scala index 52e8190487..eadf95525f 100644 --- a/core/src/main/scala/cats/data/Func.scala +++ b/core/src/main/scala/cats/data/Func.scala @@ -25,17 +25,70 @@ package data import cats.Contravariant /** - * [[Func]] is a function `A => F[B]`. + * [[Func]] is a function `A => F[B]`, where `F` is a functor. * - * See: [[https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf The Essence of the Iterator Pattern]] + * `Func` is similar to [[Kleisli]], but with weaker requirements. While `Kleisli` requires `FlatMap` (or `Monad`) + * for sequential composition, `Func` only requires `Functor` and supports parallel (applicative) composition via + * [[AppFunc]]. This makes `Func` useful when you want to compose effects in parallel rather than sequentially. + * + * '''Kleisli vs Func''' + * + * `Kleisli` is for ''sequential'' composition: `a => F[B]` andThen `b => F[C]` requires the output of the first + * to feed into the second, and needs `FlatMap` to chain them. `Func` is for ''parallel'' composition: given + * `a => F[B]` and `a => F[C]`, we can combine them into `a => F[(B, C)]` using only `Applicative#product`. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> import cats.syntax.all._ + * + * scala> val parseInt: Func[Option, String, Int] = Func.func(s => scala.util.Try(s.toInt).toOption) + * scala> val double: Func[Option, String, Double] = Func.func(s => scala.util.Try(s.toDouble).toOption) + * + * scala> // Compose in parallel: parse and double the same input independently + * scala> val combined: Func[Option, String, (Int, Double)] = parseInt.product(double) + * scala> combined.run("42") + * res0: Option[(Int, Double)] = Some((42,42.0)) + * }}} + * + * For more powerful composition (compose, andThen, traverse), see [[AppFunc]]. + * + * See also: [[https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf The Essence of the Iterator Pattern]] */ sealed abstract class Func[F[_], A, B] { self => def run: A => F[B] + + /** + * Lift a function `B => C` over this `Func`, producing a `Func[F, A, C]`. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> import cats.syntax.all._ + * + * scala> val f: Func[Option, Int, Int] = Func.func(i => Some(i + 1)) + * scala> f.map(_ * 10).run(5) + * res0: Option[Int] = Some(60) + * }}} + */ def map[C](f: B => C)(implicit FF: Functor[F]): Func[F, A, C] = Func.func(a => FF.map(self.run(a))(f)) /** - * Modify the context `F` using transformation `f`. + * Modify the context `F` using a natural transformation `f: F ~> G`. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> import cats.arrow.FunctionK + * scala> val f: Func[List, Int, Int] = Func.func(i => List(i + 1)) + * scala> val toOption: List ~> Option = FunctionK.from { + * | case Nil => None + * | case h :: _ => Some(h) + * | } + * scala> f.mapK(toOption).run(5) + * res0: Option[Int] = Some(6) + * }}} */ def mapK[G[_]](f: F ~> G): Func[G, A, B] = Func.func(a => f(run(a))) @@ -44,7 +97,15 @@ sealed abstract class Func[F[_], A, B] { self => object Func extends FuncInstances { /** - * function `A => F[B]`. + * Create a `Func` from a function `A => F[B]`. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> val f: Func[Option, String, Int] = Func.func(s => scala.util.Try(s.toInt).toOption) + * scala> f.run("42") + * res0: Option[Int] = Some(42) + * }}} */ def func[F[_], A, B](run0: A => F[B]): Func[F, A, B] = new Func[F, A, B] { @@ -52,7 +113,24 @@ object Func extends FuncInstances { } /** - * applicative function. + * Create an [[AppFunc]] (applicative function) from a function `A => F[B]`. + * + * `AppFunc` supports parallel composition via `product`, `compose`, `andThen`, and `traverse`. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> import cats.syntax.all._ + * + * scala> val parse: AppFunc[Option, String, Int] = + * | Func.appFunc(s => scala.util.Try(s.toInt).toOption) + * scala> val double: AppFunc[Option, String, Double] = + * | Func.appFunc(s => scala.util.Try(s.toDouble * 2).toOption) + * + * scala> val combined = parse.product(double) + * scala> combined.run("21") + * res0: Option[(Int, Double)] = Some((21,42.0)) + * }}} */ def appFunc[F[_], A, B](run0: A => F[B])(implicit FF: Applicative[F]): AppFunc[F, A, B] = new AppFunc[F, A, B] { @@ -120,10 +198,45 @@ sealed private[data] trait FuncApplicative[F[_], C] extends FuncApply[F, C] with /** * An implementation of [[Func]] that's specialized to [[Applicative]]. + * + * `AppFunc` is the more powerful version of [[Func]], requiring an `Applicative` instance for `F`. + * It supports parallel composition via `product`, `compose`, `andThen`, and `traverse`. + * + * While [[Kleisli]] is for ''sequential'' composition (requires `FlatMap`/`Monad`), `AppFunc` is for + * ''parallel'' composition (requires only `Applicative`). This means `AppFunc` can compose effects + * that don't depend on each other, running them in parallel. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> import cats.syntax.all._ + * + * scala> val validateName: AppFunc[Either[String, *], Config, String] = + * | Func.appFunc(c => if (c.name.nonEmpty) Right(c.name) else Left("Name required")) + * scala> val validateAge: AppFunc[Either[String, *], Config, Int] = + * | Func.appFunc(c => if (c.age > 0) Right(c.age) else Left("Invalid age")) + * + * scala> // Combine validators: both must pass + * scala> val combined = validateName.product(validateAge) + * }}} */ sealed abstract class AppFunc[F[_], A, B] extends Func[F, A, B] { self => def F: Applicative[F] + /** + * Combine this `AppFunc` with another in parallel, producing a tuple of results. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> import cats.syntax.all._ + * + * scala> val f: AppFunc[Option, Int, Int] = Func.appFunc(i => Some(i + 1)) + * scala> val g: AppFunc[Option, Int, String] = Func.appFunc(i => Some(i.toString)) + * scala> f.product(g).run(42) + * res0: Option[(Int, String)] = Some((43,"42")) + * }}} + */ def product[G[_]](g: AppFunc[G, A, B]): AppFunc[λ[α => Tuple2K[F, G, α]], A, B] = { implicit val FF: Applicative[F] = self.F implicit val GG: Applicative[G] = g.F @@ -132,6 +245,23 @@ sealed abstract class AppFunc[F[_], A, B] extends Func[F, A, B] { self => } } + /** + * Compose this `AppFunc` with another, where this function's input is the other's output. + * + * The resulting function's context is `Nested[G, F, *]`. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> import cats.syntax.all._ + * + * scala> val f: AppFunc[Option, Int, Int] = Func.appFunc(i => Some(i + 1)) + * scala> val g: AppFunc[Option, String, Int] = Func.appFunc(s => scala.util.Try(s.toInt).toOption) + * scala> val composed = f.compose(g) + * scala> composed.run("42") + * res0: Nested[Option, Option, Int] = Nested(Some(Some(43))) + * }}} + */ def compose[G[_], C](g: AppFunc[G, C, A]): AppFunc[Nested[G, F, *], C, B] = { implicit val gfApplicative: Applicative[Nested[G, F, *]] = Nested.catsDataApplicativeForNested[G, F](using g.F, F) Func.appFunc[Nested[G, F, *], C, B] { (c: C) => @@ -139,14 +269,35 @@ sealed abstract class AppFunc[F[_], A, B] extends Func[F, A, B] { self => } } + /** + * Compose this `AppFunc` with another, where the other function's input is this function's output. + * + * This is the opposite direction of `compose`. + */ def andThen[G[_], C](g: AppFunc[G, B, C]): AppFunc[Nested[F, G, *], A, C] = g.compose(self) + /** + * Lift a function `B => C` over this `AppFunc`, producing an `AppFunc[F, A, C]`. + */ def map[C](f: B => C): AppFunc[F, A, C] = { implicit val FF: Applicative[F] = self.F Func.appFunc(a => F.map(self.run(a))(f)) } + /** + * Apply this function to each element of a traversable structure. + * + * Example: + * {{{ + * scala> import cats.data._ + * scala> import cats.syntax.all._ + * + * scala> val f: AppFunc[Option, Int, Int] = Func.appFunc(i => Some(i * 2)) + * scala> f.traverse(List(1, 2, 3)) + * res0: Option[List[Int]] = Some(List(2, 4, 6)) + * }}} + */ def traverse[G[_]](ga: G[A])(implicit GG: Traverse[G]): F[G[B]] = GG.traverse(ga)(self.run)(using F) } diff --git a/docs/datatypes/directory.conf b/docs/datatypes/directory.conf index b44c76219c..7af49cca2e 100644 --- a/docs/datatypes/directory.conf +++ b/docs/datatypes/directory.conf @@ -8,6 +8,7 @@ laika.navigationOrder = [ eval.md freeapplicative.md freemonad.md + func.md functionk.md id.md ior.md diff --git a/docs/datatypes/func.md b/docs/datatypes/func.md new file mode 100644 index 0000000000..6f82bc6b4c --- /dev/null +++ b/docs/datatypes/func.md @@ -0,0 +1,159 @@ +# Func + +API Documentation: @:api(cats.data.Func) + +`Func` is a function `A => F[B]`, where `F` is a functor. It is similar to `Kleisli`, but with weaker requirements: while `Kleisli` requires `FlatMap` (or `Monad`) for sequential composition, `Func` only requires `Functor` and supports parallel (applicative) composition via `AppFunc`. + +## Kleisli vs Func + +The key difference is in how they compose: + +- **Kleisli** is for ''sequential'' composition: `A => F[B]` followed by `B => F[C]` requires the output of the first to feed into the second, and needs `FlatMap` to chain them. +- **Func** is for ''parallel'' composition: given `A => F[B]` and `A => F[C]`, we can combine them into `A => F[(B, C)]` using only `Applicative#product`. + +This is useful when you have independent effects that don't depend on each other's results. + +```scala mdoc:silent +import cats.data._ +import cats.syntax.all._ + +val parseInt: Func[Option, String, Int] = + Func.func(s => scala.util.Try(s.toInt).toOption) + +val parseDouble: Func[Option, String, Double] = + Func.func(s => scala.util.Try(s.toDouble).toOption) + +// Compose in parallel: parse as Int and Double independently +val combined: Func[Option, String, (Int, Double)] = + parseInt.product(parseDouble) + +combined.run("42") // Some((42, 42.0)) +``` + +## Func + +At its core, `Func[F[_], A, B]` wraps a function `A => F[B]`. Depending on the properties of `F[_]`, we can do different things: + +```scala mdoc:silent +val f: Func[Option, Int, Int] = Func.func(i => Some(i + 1)) + +// map only requires Functor +f.map(_ * 10).run(5) // Some(60) +``` + +### Methods + +``` +Method | Constraint on `F[_]` +--------- | ------------------- +map | Functor +mapK | (none - uses natural transformation) +product | Applicative (via AppFunc) +``` + +## AppFunc + +`AppFunc[F[_], A, B]` is the more powerful version of `Func`, requiring an `Applicative` instance for `F`. It supports parallel composition via `product`, `compose`, `andThen`, and `traverse`. + +```scala mdoc:silent +val validateName: AppFunc[Either[String, *], Config, String] = + Func.appFunc(c => if (c.name.nonEmpty) Right(c.name) else Left("Name required")) + +val validateAge: AppFunc[Either[String, *], Config, Int] = + Func.appFunc(c => if (c.age > 0) Right(c.age) else Left("Invalid age")) + +// Both validators run independently +val combined = validateName.product(validateAge) +``` + +Note: the example above uses kind-projector syntax (`Either[String, *]`). + +### Composition + +`AppFunc` supports several composition patterns: + +#### product + +Combine two `AppFunc`s that share the same input type, producing a tuple of results: + +```scala mdoc:silent +val f: AppFunc[Option, Int, Int] = Func.appFunc(i => Some(i + 1)) +val g: AppFunc[Option, Int, String] = Func.appFunc(i => Some(i.toString)) + +f.product(g).run(42) // Some((43, "42")) +``` + +#### compose and andThen + +Compose two `AppFunc`s where the output of one feeds into the other: + +```scala mdoc:silent +val parse: AppFunc[Option, String, Int] = + Func.appFunc(s => scala.util.Try(s.toInt).toOption) + +val double: AppFunc[Option, Int, Int] = + Func.appFunc(i => Some(i * 2)) + +// andThen: parse first, then double +val parseThenDouble: AppFunc[Nested[Option, Option, *], String, Int] = + parse.andThen(double) + +parseThenDouble.run("21") // Nested(Some(Some(42))) +``` + +#### traverse + +Apply an `AppFunc` to each element of a traversable structure: + +```scala mdoc:silent +val parse: AppFunc[Option, String, Int] = + Func.appFunc(s => scala.util.Try(s.toInt).toOption) + +parse.traverse(List("1", "2", "3")) // Some(List(1, 2, 3)) +parse.traverse(List("1", "x", "3")) // None +``` + +### Methods + +``` +Method | Constraint on `F[_]` +--------- | ------------------- +map | (none - Applicative already implies Functor) +product | Applicative +compose | Applicative +andThen | Applicative +traverse | Applicative +``` + +## Type class instances + +`Func[F, C, *]` has the following type class instances depending on what `F[_]` has: + +``` +Type class | Constraint on `F[_]` +-------------- | ------------------- +Functor | Functor +Apply | Apply +Applicative | Applicative +``` + +`Func[F, *, C]` (contravariant in the input type) has: + +``` +Type class | Constraint on `F[_]` +-------------- | ------------------- +Contravariant | Contravariant +``` + +## When to use Func vs Kleisli + +- Use **Kleisli** when you need sequential composition: the output of one function feeds into the next, and you need `flatMap`. +- Use **Func/AppFunc** when you have independent effects that can run in parallel: you only need `Applicative`, not `Monad`. + +In practice, `Kleisli` is more commonly used because most real-world effects are sequential. However, `Func`/`AppFunc` can be more efficient for parallel validation, parallel data fetching, or any scenario where independent effects can be composed without dependencies. + +## Further reading + +- [The Essence of the Iterator Pattern](https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf) - the paper that inspired `Func` +- @:api(cats.data.Kleisli) - the sequential counterpart +- @:api(cats.data.Nested) - used in `compose`/`andThen` return types From bbc8a2a4fa53d61d6397737e683bb281b977278b Mon Sep 17 00:00:00 2001 From: mmustafasenoglu Date: Mon, 3 Aug 2026 10:37:44 +0300 Subject: [PATCH 2/2] docs: fix example comment wording in Func scaladoc --- core/src/main/scala/cats/data/Func.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/data/Func.scala b/core/src/main/scala/cats/data/Func.scala index eadf95525f..cb64529cf3 100644 --- a/core/src/main/scala/cats/data/Func.scala +++ b/core/src/main/scala/cats/data/Func.scala @@ -45,7 +45,7 @@ import cats.Contravariant * scala> val parseInt: Func[Option, String, Int] = Func.func(s => scala.util.Try(s.toInt).toOption) * scala> val double: Func[Option, String, Double] = Func.func(s => scala.util.Try(s.toDouble).toOption) * - * scala> // Compose in parallel: parse and double the same input independently + * scala> // Compose in parallel: parse the same input into both Int and Double simultaneously * scala> val combined: Func[Option, String, (Int, Double)] = parseInt.product(double) * scala> combined.run("42") * res0: Option[(Int, Double)] = Some((42,42.0))