Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs for Func and AppFunc #4378

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
13 changes: 12 additions & 1 deletion core/src/main/scala/cats/data/Func.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@ import cats.Contravariant
*/
sealed abstract class Func[F[_], A, B] { self =>
def run: A => F[B]

/**
* scala> val f = Func.func((x: Int) => List(x.toString))
* val f: cats.data.Func[List,Int,String] = ...
* scala> val g = f.map((x: String) => if (x=="0") None else Some(x))
* val g: cats.data.Func[List,Int,Option[String]] = ...
*/
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 (natural) transformation `f`.
*/
def mapK[G[_]](f: F ~> G): Func[G, A, B] =
Func.func(a => f(run(a)))
Expand Down Expand Up @@ -118,6 +125,10 @@ sealed private[data] trait FuncApplicative[F[_], C] extends Applicative[λ[α =>

/**
* An implementation of [[Func]] that's specialized to [[Applicative]].
*
* As seen in [[https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf The Essence of the Iterator Pattern]]
* the Applicative Functor "capture the essence of the ITERATOR pattern"
* allowing to traverse and compose Applicative Functors
*/
sealed abstract class AppFunc[F[_], A, B] extends Func[F, A, B] { self =>
def F: Applicative[F]
Expand Down
78 changes: 78 additions & 0 deletions docs/datatypes/func.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Func
Copy link
Contributor

Choose a reason for hiding this comment

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

A link to the scaladoc should be added now that #4401 is merged.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since AppFunc is so important here, maybe the title should be "Func and AppFunc"? Some of the typeclasses are done this way.


Func is a wrapper around a `run` function `a => F[B]` where F is a functor. Given that, the Func data type is equipped with the known `map` function, and a `mapK` function to apply natural transformations (from a `F` Func get an `G` Func).
UlisesTorrella marked this conversation as resolved.
Show resolved Hide resolved

The signature: `Func[F[_], A, B]` refers to an `F` functor, `A` source type and `B` target type (`F[B]`).

If you are familiar with `Kleisli` you can recognize it has a similar signature: `Kleisli[F[_], -A, B]` and `Func[F[_], A, B]`. Well yes, `Func` is a less restrictive data type that wraps around functors, and only provides basic methods `run`, `map`, and `mapK`, while `Kleisli` is strong enough to provide composition, flatMap, and more. We will see a more useful data type just next with `AppFunc`.
UlisesTorrella marked this conversation as resolved.
Show resolved Hide resolved

## Quick example

```scala mdoc:silent:nest
import cats.data.{ Func, AppFunc }
import cats._

val f: Func[List, Int, String] = Func.func((x: Int) => List(x.toString))

val g: Func[List, Int, Option[String]] = f.map((x: String) => if (x=="0") None else Some(x))

val optToList = new (Option ~> List) {
def apply[T](opt: Option[T]): List[T] =
opt.toList
}

// We transform the elements of List, of type
// Option[String] to List[String]
g.map(optToList(_))
// val res0: cats.data.Func[List,Int,List[String]] = ...
```



# AppFunc

AppFunc extends Func to wrap around a special type of functor: Applicative functors.
Copy link
Contributor

Choose a reason for hiding this comment

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

May as well link to the section for reference.

Suggested change
AppFunc extends Func to wrap around a special type of functor: Applicative functors.
AppFunc extends Func to wrap around a special type of functor: [Applicative] functors.


With applicative functors we can `compose`, form the `product`, and also `traverse` traversable functors

Signature: `AppFunc[F[_], A, B] extends Func[F, A, B]`

Now, for the reader familiar with `Kleisli`, we find an even more similar data type. `AppFunc` provides compositions of weaker constraint, allowing to compose `AppFunc[F[_], A, B]` with `AppFunc[G[_], C, A]`.
UlisesTorrella marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think these Kleisli call outs are going to help people trying to figure out what AppFunc is for. Have you considered having a comparison to Kleisli section at the end of the article?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey, yes actually I think at the beginning it was at the end but a suggestion was to move it to the beginning. Given that most doubts in the PR introducing the code were around "why do we need this if Kleisli is a thing"

Copy link
Contributor

Choose a reason for hiding this comment

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

Ha, I guess that's down to a matter of audience. For cats newcomers, Kleisli is probably at least as foreign as this.

## Composition

All of functional programming revolves around composing, and functors cannot be left behind. If we are working with multiple contexts we might want to compose them, for example: we want to `List` things, and discard some (`Option`).

To achieve this nested context behavior `AppFunc` uses the `Nested` datatype.
UlisesTorrella marked this conversation as resolved.
Show resolved Hide resolved

```scala mdoc:silent:nest
val AppFuncF: AppFunc[Option,Int,Int] = Func.appFunc((i: Int) => if (i==0) None else Some(i))

val AppFuncG: AppFunc[List,Int,Int] = Func.appFunc((o: Int) => {List(o+1)})
UlisesTorrella marked this conversation as resolved.
Show resolved Hide resolved
(AppFuncF andThen AppFuncG).run(1) //Nested(Some(List(2)))

(AppFuncF andThen AppFuncG).run(0) //Nested(None)
// same thing with compose

(AppFuncG compose AppFuncF)
```
## Product

Applicative functors, like monads, are closed under product. Cats models this data type as Tuple2k, allowing to form the product of two applicative functors (they can be different!) in one data type.
UlisesTorrella marked this conversation as resolved.
Show resolved Hide resolved

For further reading: [hearding cats](http://eed3si9n.com/herding-cats/combining-applicative.html#Product+of+applicative+functions)
UlisesTorrella marked this conversation as resolved.
Show resolved Hide resolved

```scala mdoc:silent:nest
(AppFuncF product AppFuncG).run(1)
```
## Traverse

This explained in the implementation of the Applicative trait: [Applicative - Traverse](https://typelevel.org/cats/typeclasses/applicative.html#traverse)

```scala mdoc:silent:nest
AppFuncF.traverse(List(1,2,3))
// val res14: Option[List[Int]] = Some(List(1, 2, 3))

AppFuncF.traverse(List(1,2,0))
//val res15: Option[List[Int]] = None
```