-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Request to Include cats.data.Coproduct
and cats.free.Inject
#671
Changes from 10 commits
08bc6ad
df4d72a
09cba75
9bec85d
2dc6690
414c942
8f5a679
9450dc0
1781b5f
ecac4d4
e9fa262
77ccebd
2c38d37
fa81101
4f06759
f50c15b
d0efd8d
1b40683
743b75f
5ea5313
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
package cats | ||
package data | ||
|
||
import cats.functor.Contravariant | ||
|
||
/** `F` on the left and `G` on the right of [[Xor]]. | ||
* | ||
* @param run The underlying [[Xor]]. */ | ||
final case class Coproduct[F[_], G[_], A](run: F[A] Xor G[A]) { | ||
|
||
import Coproduct._ | ||
|
||
def map[B](f: A => B)(implicit F: Functor[F], G: Functor[G]): Coproduct[F, G, B] = | ||
Coproduct(run.bimap(F.lift(f), G.lift(f))) | ||
|
||
def coflatMap[B](f: Coproduct[F, G, A] => B)(implicit F: CoflatMap[F], G: CoflatMap[G]): Coproduct[F, G, B] = | ||
Coproduct( | ||
run.bimap(a => F.coflatMap(a)(x => f(leftc(x))), a => G.coflatMap(a)(x => f(rightc(x)))) | ||
) | ||
|
||
def duplicate(implicit F: CoflatMap[F], G: CoflatMap[G]): Coproduct[F, G, Coproduct[F, G, A]] = | ||
Coproduct(run.bimap( | ||
x => F.coflatMap(x)(a => leftc(a)) | ||
, x => G.coflatMap(x)(a => rightc(a))) | ||
) | ||
|
||
def extract(implicit F: Comonad[F], G: Comonad[G]): A = | ||
run.fold(F.extract, G.extract) | ||
|
||
def contramap[B](f: B => A)(implicit F: Contravariant[F], G: Contravariant[G]): Coproduct[F, G, B] = | ||
Coproduct(run.bimap(F.contramap(_)(f), G.contramap(_)(f))) | ||
|
||
def foldRight[B](z: Eval[B])(f: (A, Eval[B]) => Eval[B])(implicit F: Foldable[F], G: Foldable[G]): Eval[B] = | ||
run.fold(a => F.foldRight(a, z)(f), a => G.foldRight(a, z)(f)) | ||
|
||
def foldLeft[B](z: B)(f: (B, A) => B)(implicit F: Foldable[F], G: Foldable[G]): B = | ||
run.fold(a => F.foldLeft(a, z)(f), a => G.foldLeft(a, z)(f)) | ||
|
||
def foldMap[B](f: A => B)(implicit F: Foldable[F], G: Foldable[G], M: Monoid[B]): B = | ||
run.fold(F.foldMap(_)(f), G.foldMap(_)(f)) | ||
|
||
def traverse[X[_], B](g: A => X[B])(implicit F: Traverse[F], G: Traverse[G], A: Applicative[X]): X[Coproduct[F, G, B]] = | ||
run.fold( | ||
x => A.map(F.traverse(x)(g))(leftc(_)) | ||
, x => A.map(G.traverse(x)(g))(rightc(_)) | ||
) | ||
|
||
def isLeft: Boolean = | ||
run.isLeft | ||
|
||
def isRight: Boolean = | ||
run.isRight | ||
|
||
def swap: Coproduct[G, F, A] = | ||
Coproduct(run.swap) | ||
|
||
def toValidated: Validated[F[A], G[A]] = | ||
run.toValidated | ||
|
||
} | ||
|
||
object Coproduct extends CoproductInstances { | ||
|
||
def leftc[F[_], G[_], A](x: F[A]): Coproduct[F, G, A] = | ||
Coproduct(Xor.left(x)) | ||
|
||
def rightc[F[_], G[_], A](x: G[A]): Coproduct[F, G, A] = | ||
Coproduct(Xor.right(x)) | ||
|
||
final class CoproductLeft[G[_]] private[Coproduct] { | ||
def apply[F[_], A](fa: F[A]): Coproduct[F, G, A] = Coproduct(Xor.left(fa)) | ||
} | ||
|
||
final class CoproductRight[F[_]] private[Coproduct] { | ||
def apply[G[_], A](ga: G[A]): Coproduct[F, G, A] = Coproduct(Xor.right(ga)) | ||
} | ||
|
||
def left[G[_]]: CoproductLeft[G] = new CoproductLeft[G] | ||
|
||
def right[F[_]]: CoproductRight[F] = new CoproductRight[F] | ||
|
||
} | ||
|
||
sealed abstract class CoproductInstances3 { | ||
|
||
implicit def coproductEq[F[_], G[_], A](implicit E: Eq[F[A] Xor G[A]]): Eq[Coproduct[F, G, A]] = | ||
Eq.by(_.run) | ||
|
||
implicit def coproductFunctor[F[_], G[_]](implicit F0: Functor[F], G0: Functor[G]): Functor[Coproduct[F, G, ?]] = | ||
new CoproductFunctor[F, G] { | ||
implicit def F: Functor[F] = F0 | ||
|
||
implicit def G: Functor[G] = G0 | ||
} | ||
|
||
implicit def coproductFoldable[F[_], G[_]](implicit F0: Foldable[F], G0: Foldable[G]): Foldable[Coproduct[F, G, ?]] = | ||
new CoproductFoldable[F, G] { | ||
implicit def F: Foldable[F] = F0 | ||
|
||
implicit def G: Foldable[G] = G0 | ||
} | ||
} | ||
|
||
sealed abstract class CoproductInstances2 extends CoproductInstances3 { | ||
|
||
implicit def coproductContravariant[F[_], G[_]](implicit F0: Contravariant[F], G0: Contravariant[G]): Contravariant[Coproduct[F, G, ?]] = | ||
new CoproductContravariant[F, G] { | ||
implicit def F: Contravariant[F] = F0 | ||
|
||
implicit def G: Contravariant[G] = G0 | ||
} | ||
} | ||
|
||
sealed abstract class CoproductInstances1 extends CoproductInstances2 { | ||
implicit def coproductCoflatMap[F[_], G[_]](implicit F0: CoflatMap[F], G0: CoflatMap[G]): CoflatMap[Coproduct[F, G, ?]] = | ||
new CoproductCoflatMap[F, G] { | ||
implicit def F: CoflatMap[F] = F0 | ||
|
||
implicit def G: CoflatMap[G] = G0 | ||
} | ||
} | ||
|
||
sealed abstract class CoproductInstances0 extends CoproductInstances1 { | ||
implicit def coproductTraverse[F[_], G[_]](implicit F0: Traverse[F], G0: Traverse[G]): Traverse[Coproduct[F, G, ?]] = | ||
new CoproductTraverse[F, G] { | ||
implicit def F: Traverse[F] = F0 | ||
|
||
implicit def G: Traverse[G] = G0 | ||
} | ||
} | ||
|
||
sealed abstract class CoproductInstances extends CoproductInstances0 { | ||
|
||
implicit def coproductComonad[F[_], G[_]](implicit F0: Comonad[F], G0: Comonad[G]): Comonad[Coproduct[F, G, ?]] = | ||
new CoproductComonad[F, G] { | ||
implicit def F: Comonad[F] = F0 | ||
|
||
implicit def G: Comonad[G] = G0 | ||
} | ||
} | ||
|
||
private[data] trait CoproductFunctor[F[_], G[_]] extends Functor[Coproduct[F, G, ?]] { | ||
implicit def F: Functor[F] | ||
|
||
implicit def G: Functor[G] | ||
|
||
def map[A, B](a: Coproduct[F, G, A])(f: A => B): Coproduct[F, G, B] = | ||
a map f | ||
} | ||
|
||
private[data] trait CoproductContravariant[F[_], G[_]] extends Contravariant[Coproduct[F, G, ?]] { | ||
implicit def F: Contravariant[F] | ||
|
||
implicit def G: Contravariant[G] | ||
|
||
def contramap[A, B](a: Coproduct[F, G, A])(f: B => A): Coproduct[F, G, B] = | ||
a contramap f | ||
} | ||
|
||
private[data] trait CoproductFoldable[F[_], G[_]] extends Foldable[Coproduct[F, G, ?]] { | ||
implicit def F: Foldable[F] | ||
|
||
implicit def G: Foldable[G] | ||
|
||
def foldRight[A, B](fa: Coproduct[F, G, A], z: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = | ||
fa.foldRight(z)(f) | ||
|
||
def foldLeft[A, B](fa: Coproduct[F, G, A], z: B)(f: (B, A) => B): B = | ||
fa.foldLeft(z)(f) | ||
|
||
override def foldMap[A, B](fa: Coproduct[F, G, A])(f: A => B)(implicit M: Monoid[B]): B = | ||
fa foldMap f | ||
} | ||
|
||
private[data] trait CoproductTraverse[F[_], G[_]] extends CoproductFoldable[F, G] with Traverse[Coproduct[F, G, ?]] { | ||
implicit def F: Traverse[F] | ||
|
||
implicit def G: Traverse[G] | ||
|
||
override def map[A, B](a: Coproduct[F, G, A])(f: A => B): Coproduct[F, G, B] = | ||
a map f | ||
|
||
override def traverse[X[_] : Applicative, A, B](fa: Coproduct[F, G, A])(f: A => X[B]): X[Coproduct[F, G, B]] = | ||
fa traverse f | ||
} | ||
|
||
private[data] trait CoproductCoflatMap[F[_], G[_]] extends CoflatMap[Coproduct[F, G, ?]] { | ||
implicit def F: CoflatMap[F] | ||
|
||
implicit def G: CoflatMap[G] | ||
|
||
def map[A, B](a: Coproduct[F, G, A])(f: A => B): Coproduct[F, G, B] = | ||
a map f | ||
|
||
def coflatMap[A, B](a: Coproduct[F, G, A])(f: Coproduct[F, G, A] => B): Coproduct[F, G, B] = | ||
a coflatMap f | ||
|
||
} | ||
|
||
private[data] trait CoproductComonad[F[_], G[_]] extends Comonad[Coproduct[F, G, ?]] { | ||
implicit def F: Comonad[F] | ||
|
||
implicit def G: Comonad[G] | ||
|
||
def map[A, B](a: Coproduct[F, G, A])(f: A => B): Coproduct[F, G, B] = | ||
a map f | ||
|
||
def extract[A](p: Coproduct[F, G, A]): A = | ||
p.extract | ||
|
||
def coflatMap[A, B](a: Coproduct[F, G, A])(f: Coproduct[F, G, A] => B): Coproduct[F, G, B] = | ||
a coflatMap f | ||
|
||
def duplicate[A](a: Coproduct[F, G, A]): Coproduct[F, G, Coproduct[F, G, A]] = | ||
a.duplicate | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,3 +32,4 @@ trait AllSyntax | |
with TraverseSyntax | ||
with XorSyntax | ||
with ValidatedSyntax | ||
with CoproductSyntax |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package cats | ||
package syntax | ||
|
||
import cats.data.Coproduct | ||
|
||
trait CoproductSyntax { | ||
implicit def coproductSyntax[F[_], A](a: F[A]): CoproductOps[F, A] = new CoproductOps(a) | ||
} | ||
|
||
class CoproductOps[F[_], A](val a: F[A]) extends AnyVal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: let's go ahead and make this class |
||
def leftc[G[_]]: Coproduct[F, G, A] = Coproduct.leftc(a) | ||
def rightc[G[_]]: Coproduct[G, F, A] = Coproduct.rightc(a) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -348,6 +348,120 @@ it's not too hard to get around.) | |
val result: Map[String, Int] = compilePure(program, Map.empty) | ||
``` | ||
|
||
## Composing Free monads ADTs. | ||
|
||
Real world applications often time combine different algebras. | ||
The `Inject` typeclass described by Swierstra in [Data types à la carte](http://www.staff.science.uu.nl/~swier004/Publications/DataTypesALaCarte.pdf) | ||
let us compose different algebras in the context of `Free`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor typo: "lets us" |
||
|
||
Let's see a trivial example of unrelated ADT's getting composed as a `Coproduct` that conform a more complex program. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should "conform" here be "can form" or just "form"? |
||
|
||
```tut | ||
import cats.arrow.NaturalTransformation | ||
import cats.data.{Xor, Coproduct} | ||
import cats.free.{Inject, Free} | ||
import cats.{Id, ~>} | ||
import scala.collection.mutable.ListBuffer | ||
``` | ||
|
||
```tut | ||
/* Handles user interaction */ | ||
sealed trait Interact[A] | ||
case class Ask(prompt: String) extends Interact[String] | ||
case class Tell(msg: String) extends Interact[Unit] | ||
|
||
/* Represents persistence operations */ | ||
sealed trait DataOp[A] | ||
case class AddCat(a: String) extends DataOp[Unit] | ||
case class GetAllCats() extends DataOp[List[String]] | ||
``` | ||
|
||
Once the ADTs are defined we can formally state that a `Free` program is the Coproduct of it's Algebras. | ||
|
||
```tut | ||
type CatsApp[A] = Coproduct[DataOp, Interact, A] | ||
``` | ||
|
||
In order to take advantage of monadic composition we use smart constructors to lift our Algebra to the `Free` context. | ||
|
||
```tut | ||
def lift[F[_], G[_], A](fa: F[A])(implicit I: Inject[F, G]): Free[G, A] = | ||
Free.liftF(I.inj(fa)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this method be added to Cats? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As a user, this is something I use all the time and it's a the core of the Smart Constructors pattern to lift ADTs to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm using absolutely zero CT here, but what if we added it to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll be happy to add it there then if everyone else thinks it makes sense. @non @adelbertc thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's do it! I'm happy to do it in this PR, or another, whichever you feel like. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just realized this same method is already part of def inject[F[_], G[_], A](ga: G[Free[F, A]])(implicit I: Inject[G, F]): Free[F, A] =
Free.liftF(I.inj(ga)) flatMap identity Inference improvements apart, does it make sense to have it also on Inject.inject[T, Test1Algebra, Int](Test1(1)) Also @ceedubs can you point me to an example elsewhere where the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @raulraja WRT There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @fthomas! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ceedubs @fthomas Do you think we can use the final class FreeInjectPartiallyApplied[G[_]] private[free] {
def apply[F[_], A](fa: F[A])(implicit I : Inject[F, G]): Free[G, A] =
Free.liftF(I.inj(fa))
}
def inject[G[_]]: FreeInjectPartiallyApplied[G] = new FreeInjectPartiallyApplied There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
class Interacts[F[_]](implicit I: Inject[Interact, F]) { | ||
def tell(msg: String): Free[F, Unit] = lift(Tell(msg)) | ||
def ask(prompt: String): Free[F, String] = lift(Ask(prompt)) | ||
} | ||
|
||
object Interacts { | ||
implicit def interacts[F[_]](implicit I: Inject[Interact, F]): Interacts[F] = new Interacts[F] | ||
} | ||
|
||
class DataSource[F[_]](implicit I: Inject[DataOp, F]) { | ||
def addCat(a: String): Free[F, Unit] = lift[DataOp, F, Unit](AddCat(a)) | ||
def getAllCats: Free[F, List[String]] = lift[DataOp, F, List[String]](GetAllCats()) | ||
} | ||
|
||
object DataSource { | ||
implicit def dataSource[F[_]](implicit I: Inject[DataOp, F]): DataSource[F] = new DataSource[F] | ||
} | ||
``` | ||
|
||
ADTs are now easily composed and trivially intertwined inside monadic contexts. | ||
|
||
```tut | ||
def program(implicit I : Interacts[CatsApp], D : DataSource[CatsApp]) = { | ||
|
||
import I._, D._ | ||
|
||
for { | ||
cat <- ask("What's the kitty's name") | ||
_ <- addCat(cat) | ||
cats <- getAllCats | ||
_ <- tell(cats.toString) | ||
} yield () | ||
} | ||
``` | ||
|
||
Finally we write one interpreter per ADT and combine them with a `NaturalTransformation` to `Coproduct` so when they can be | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can drop "when" from this sentence. |
||
compiled and applied to our `Free` program. | ||
|
||
```scala | ||
object ConsoleCatsInterpreter extends (Interact ~> Id) { | ||
def apply[A](i: Interact[A]) = i match { | ||
case Ask(prompt) => | ||
println(prompt) | ||
scala.io.StdIn.readLine() | ||
case Tell(msg) => | ||
println(msg) | ||
} | ||
} | ||
|
||
object InMemoryDatasourceInterpreter extends (DataOp ~> Id) { | ||
|
||
private[this] val memDataSet = new ListBuffer[String] | ||
|
||
def apply[A](fa: DataOp[A]) = fa match { | ||
case AddCat(a) => memDataSet.append(a); () | ||
case GetAllCats() => memDataSet.toList | ||
} | ||
} | ||
|
||
def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): Coproduct[F, G, ?] ~> H = | ||
new NaturalTransformation[Coproduct[F, G, ?], H] { | ||
def apply[A](fa: Coproduct[F, G, A]): H[A] = fa.run match { | ||
case Xor.Left(ff) => f(ff) | ||
case Xor.Right(gg) => g(gg) | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this method live within Cats? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as the discussed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could see this living in either the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 on adding it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, promoted to |
||
|
||
val interpreter: CatsApp ~> Id = or(InMemoryDatasourceInterpreter, ConsoleCatsInterpreter) | ||
|
||
import DataSource._, Interacts._ | ||
|
||
val evaled = program.foldMap(interpreter) | ||
``` | ||
|
||
## <a name="what-is-free-in-theory"></a>For the curious ones: what is Free in theory? | ||
|
||
Mathematically-speaking, a *free monad* (at least in the programming | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the minor details, but could you make the
CodproductInstances*
traitsprivate[data]
? That will make them consistent with the work done in #612.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries, I want to get it right so I understand what the expectations are for future contributions.:) Added those access modifiers also on
InjectInstances