Skip to content

Commit 48286f2

Browse files
committed
Merge pull request #720 from adelbertc/free-applicative-doc
More tut for FreeApplicative
2 parents b8be77b + fd06d23 commit 48286f2

1 file changed

Lines changed: 170 additions & 4 deletions

File tree

docs/src/main/tut/freeapplicative.md

Lines changed: 170 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,176 @@ section: "data"
55
source: "https://github.com/non/cats/blob/master/core/src/main/scala/cats/free/FreeApplicative.scala"
66
scaladoc: "#cats.free.FreeApplicative"
77
---
8-
# Free Applicative Functor
8+
# Free Applicative
99

10-
Applicative functors are a generalization of monads allowing expressing effectful computations in a pure functional way.
10+
`FreeApplicative`s are similar to `Free` (monads) in that they provide a nice way to represent
11+
computations as data and are useful for building embedded DSLs (EDSLs). However, they differ
12+
from `Free` in that the kinds of operations they support are limited, much like the distinction
13+
between `Applicative` and `Monad`.
1114

12-
Free Applicative functor is the counterpart of FreeMonads for Applicative.
13-
Free Monads is a construction that is left adjoint to a forgetful functor from the category of Monads
15+
## Example
16+
Consider building an EDSL for validating strings - to keep things simple we'll just have
17+
a way to check a string is at least a certain size and to ensure the string contains numbers.
18+
19+
```tut:silent
20+
sealed abstract class ValidationOp[A]
21+
case class Size(size: Int) extends ValidationOp[Boolean]
22+
case object HasNumber extends ValidationOp[Boolean]
23+
```
24+
25+
Much like the `Free` monad tutorial, we use smart constructors to lift our algebra into the `FreeApplicative`.
26+
27+
```tut:silent
28+
import cats.free.FreeApplicative
29+
import cats.free.FreeApplicative.lift
30+
31+
type Validation[A] = FreeApplicative[ValidationOp, A]
32+
33+
def size(size: Int): Validation[Boolean] = lift(Size(size))
34+
35+
val hasNumber: Validation[Boolean] = lift(HasNumber)
36+
```
37+
38+
Because a `FreeApplicative` only supports the operations of `Applicative`, we do not get the nicety
39+
of a for-comprehension. We can however still use `Applicative` syntax provided by Cats.
40+
41+
```tut:silent
42+
import cats.syntax.apply._
43+
44+
val prog: Validation[Boolean] = (size(5) |@| hasNumber).map { case (l, r) => l && r}
45+
```
46+
47+
As it stands, our program is just an instance of a data structure - nothing has happened
48+
at this point. To make our program useful we need to interpret it.
49+
50+
```tut:silent
51+
import cats.Id
52+
import cats.arrow.NaturalTransformation
53+
import cats.std.function._
54+
55+
val compiler =
56+
new NaturalTransformation[ValidationOp, String => ?] {
57+
def apply[A](fa: ValidationOp[A]): String => A =
58+
str =>
59+
fa match {
60+
case Size(size) => str.size >= size
61+
case HasNumber => str.exists(c => "0123456789".contains(c))
62+
}
63+
}
64+
```
65+
66+
```tut
67+
val validator = prog.foldMap[String => ?](compiler)
68+
validator("1234")
69+
validator("12345")
70+
```
71+
72+
## Differences from `Free`
73+
So far everything we've been doing has been not much different from `Free` - we've built
74+
an algebra and interpreted it. However, there are some things `FreeApplicative` can do that
75+
`Free` cannot.
76+
77+
Recall a key distinction between the type classes `Applicative` and `Monad` - `Applicative`
78+
captures the idea of independent computations, whereas `Monad` captures that of dependent
79+
computations. Put differently `Applicative`s cannot branch based on the value of an existing/prior
80+
computation. Therefore when using `Applicative`s, we must hand in all our data in one go.
81+
82+
In the context of `FreeApplicative`s, we can leverage this static knowledge in our interpreter.
83+
84+
### Parallelism
85+
Because we have everything we need up front and know there can be no branching, we can easily
86+
write a validator that validates in parallel.
87+
88+
```tut:silent
89+
import cats.data.Kleisli
90+
import cats.std.future._
91+
import scala.concurrent.Future
92+
import scala.concurrent.ExecutionContext.Implicits.global
93+
94+
// recall Kleisli[Future, String, A] is the same as String => Future[A]
95+
type ParValidator[A] = Kleisli[Future, String, A]
96+
97+
val parCompiler =
98+
new NaturalTransformation[ValidationOp, ParValidator] {
99+
def apply[A](fa: ValidationOp[A]): ParValidator[A] =
100+
Kleisli { str =>
101+
fa match {
102+
case Size(size) => Future { str.size >= size }
103+
case HasNumber => Future { str.exists(c => "0123456789".contains(c)) }
104+
}
105+
}
106+
}
107+
108+
val parValidation = prog.foldMap[ParValidator](parCompiler)
109+
```
110+
111+
### Logging
112+
We can also write an interpreter that simply creates a list of strings indicating the filters that
113+
have been used - this could be useful for logging purposes. Note that we need not actually evaluate
114+
the rules against a string for this, we simply need to map each rule to some identifier. Therefore
115+
we can completely ignore the return type of the operation and return just a `List[String]` - the
116+
`Const` data type is useful for this.
117+
118+
```tut:silent
119+
import cats.data.Const
120+
import cats.std.list._
121+
122+
type Log[A] = Const[List[String], A]
123+
124+
val logCompiler =
125+
new NaturalTransformation[ValidationOp, Log] {
126+
def apply[A](fa: ValidationOp[A]): Log[A] =
127+
fa match {
128+
case Size(size) => Const(List(s"size >= $size"))
129+
case HasNumber => Const(List("has number"))
130+
}
131+
}
132+
133+
def logValidation[A](validation: Validation[A]): List[String] =
134+
validation.foldMap[Log](logCompiler).getConst
135+
```
136+
137+
```tut
138+
logValidation(prog)
139+
logValidation(size(5) *> hasNumber *> size(10))
140+
logValidation((hasNumber |@| size(3)).map(_ || _))
141+
```
142+
143+
### Why not both?
144+
It is perhaps more plausible and useful to have both the actual validation function and the logging
145+
strings. While we could easily compile our program twice, once for each interpreter as we have above,
146+
we could also do it in one go - this would avoid multiple traversals of the same structure.
147+
148+
Another useful property `Applicative`s have over `Monad`s is that given two `Applicative`s `F[_]` and
149+
`G[_]`, their product `type FG[A] = (F[A], G[A])` is also an `Applicative`. This is not true in the general
150+
case for monads.
151+
152+
Therefore, we can write an interpreter that uses the product of the `ParValidator` and `Log` `Applicative`s
153+
to interpret our program in one go.
154+
155+
```tut:silent
156+
import cats.data.Prod
157+
158+
type ValidateAndLog[A] = Prod[ParValidator, Log, A]
159+
160+
val prodCompiler =
161+
new NaturalTransformation[ValidationOp, ValidateAndLog] {
162+
def apply[A](fa: ValidationOp[A]): ValidateAndLog[A] = {
163+
fa match {
164+
case Size(size) =>
165+
val f: ParValidator[Boolean] = Kleisli(str => Future { str.size >= size })
166+
val l: Log[Boolean] = Const(List(s"size > $size"))
167+
Prod[ParValidator, Log, Boolean](f, l)
168+
case HasNumber =>
169+
val f: ParValidator[Boolean] = Kleisli(str => Future(str.exists(c => "0123456789".contains(c))))
170+
val l: Log[Boolean] = Const(List("has number"))
171+
Prod[ParValidator, Log, Boolean](f, l)
172+
}
173+
}
174+
}
175+
176+
val prodValidation = prog.foldMap[ValidateAndLog](prodCompiler)
177+
```
178+
179+
## References
14180
Deeper explanations can be found in this paper [Free Applicative Functors by Paolo Capriotti](http://www.paolocapriotti.com/assets/applicative.pdf)

0 commit comments

Comments
 (0)