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

Request to Include cats.data.Coproduct and cats.free.Inject #671

Merged
merged 20 commits into from
Nov 19, 2015
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
217 changes: 217 additions & 0 deletions core/src/main/scala/cats/data/Coproduct.scala
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 {
Copy link
Contributor

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* traits private[data]? That will make them consistent with the work done in #612.

Copy link
Contributor Author

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


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
}

1 change: 1 addition & 0 deletions core/src/main/scala/cats/syntax/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ trait AllSyntax
with TraverseSyntax
with XorSyntax
with ValidatedSyntax
with CoproductSyntax
13 changes: 13 additions & 0 deletions core/src/main/scala/cats/syntax/coproduct.scala
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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: let's go ahead and make this class final to be consistent with other Ops classes.

def leftc[G[_]]: Coproduct[F, G, A] = Coproduct.leftc(a)
def rightc[G[_]]: Coproduct[G, F, A] = Coproduct.rightc(a)
}
114 changes: 114 additions & 0 deletions docs/src/main/tut/freemonad.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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))
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this method be added to Cats?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 Free. So I'd be inclined to say YES but I'm not sure where it would belong. Maybe someone in the more theoretical side of CT among us may know?

Copy link
Contributor

Choose a reason for hiding this comment

The 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 Free companion object as Free.inject? It might also be helpful to follow the PartiallyApplied pattern we have in a couple other places to allow you to do something like Free.inject[G](fa) since the F and A type parameters could be inferred in that case. Just a thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just realized this same method is already part of Inject:

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 Free?
Currently is being used like so...

Inject.inject[T, Test1Algebra, Int](Test1(1))

Also @ceedubs can you point me to an example elsewhere where the PartiallyApplied pattern is in use? Were you referring to something like this

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @fthomas!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ceedubs @fthomas Do you think we can use the PartiallyApplied trick given Free.inject[App](Test1(x)) would not be able to infer TestAlgebra1? Test1 is an F[_] but our Coproduct composition depends on TestAlgebra1. I can take it as far as inferring A in Test1[A] with that trick but not sure how would I go about auto inferring the parent sealed trait in the ADT.
This would fail not finding a Inject[X, Test1] implicit when really the one we are supposed to get is Inject[X, Test1Algebra].

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Free.inject[F, G] added. Unfortunately we need two params because the Coproduct you lift to is a parent of the type you pass to Free.inject which can't be inferred automatically as far as I know. If interested there is some more in depth discussion around it here https://gitter.im/non/cats?at=564cd3fa7ae518a71d2f9f98


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
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this method live within Cats?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as the discussed lift above. I'd love to see it included but not sure where it belongs. This is needed to create interpreters and it's used in the same way as the Coproduct type alias to stack up multiple algebras. CoProduct builds the Tree of the coproduct definition and or builds the same tree but with the interpreters. They mirror each other

Copy link
Contributor

Choose a reason for hiding this comment

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

I could see this living in either the Coproduct companion object or as a concrete method in the NaturalTransformation trait. I think that it might result in better inference and slightly nicer syntax to put it into NaturalTransformation, so I'm inclined to suggest that if nobody objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1 on adding it to NaturalTransformation @non @adelbertc thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, promoted to NaturalTransformation.or and included test.


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
Expand Down
Loading