Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 156 additions & 5 deletions core/src/main/scala/cats/data/Func.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 the same input into both Int and Double simultaneously
* scala> val combined: Func[Option, String, (Int, Double)] = parseInt.product(double)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really parse and double the input, it parses it into both Int and Double simultaneously.

* 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)))
Expand All @@ -44,15 +97,40 @@ 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] {
def run: A => F[B] = run0
}

/**
* 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] {
Expand Down Expand Up @@ -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
Expand All @@ -132,21 +245,59 @@ 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) =>
Nested(g.F.map(g.run(c))(self.run))
}
}

/**
* 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)
}
Expand Down
1 change: 1 addition & 0 deletions docs/datatypes/directory.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ laika.navigationOrder = [
eval.md
freeapplicative.md
freemonad.md
func.md
functionk.md
id.md
ior.md
Expand Down
159 changes: 159 additions & 0 deletions docs/datatypes/func.md
Original file line number Diff line number Diff line change
@@ -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
Loading