@@ -5,10 +5,176 @@ section: "data"
55source : " https://github.com/non/cats/blob/master/core/src/main/scala/cats/free/FreeApplicative.scala"
66scaladoc : " #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
14180Deeper explanations can be found in this paper [ Free Applicative Functors by Paolo Capriotti] ( http://www.paolocapriotti.com/assets/applicative.pdf )
0 commit comments