-
-
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
Add Traverse1 typeclass (#1577) #1611
Changes from 25 commits
5452d01
96d79d2
4a38545
3e9afe3
7ff5e1e
2e2e7ac
82f4537
ab40d79
11e8a5d
6945a26
17d8d62
b1e115b
796ebb4
f437a98
647df06
1f69c30
caac5f7
aa5d6aa
590ba54
dfbc064
9093a53
5c49d9f
0cc4531
a2bbee4
8e9656b
f214c64
9ed10ea
7f55edc
02c25d5
50cc4c0
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,89 @@ | ||
package cats | ||
|
||
import simulacrum.typeclass | ||
|
||
/** | ||
* Traverse1, also known as Traversable1. | ||
* | ||
* `Traverse1` is like a non-empty `Traverse`. In addition to the traverse and sequence | ||
* methods it provides traverse1 and sequence1 methods which require an `Apply` instance instead of `Applicative`. | ||
*/ | ||
@typeclass trait Traverse1[F[_]] extends Traverse[F] with Reducible[F] { | ||
|
||
/** | ||
* Given a function which returns a G effect, thread this effect | ||
* through the running of this function on all the values in F, | ||
* returning an F[B] in a G context. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> import cats.data.NonEmptyList | ||
* scala> def countWords(words: List[String]): Map[String, Int] = words.groupBy(identity).mapValues(_.length) | ||
* scala> NonEmptyList.of(List("How", "do", "you", "fly"), List("What", "do", "you", "do")).traverse1(countWords) | ||
* res0: Map[String,cats.data.NonEmptyList[Int]] = Map(do -> NonEmptyList(1, 2), you -> NonEmptyList(1, 1)) | ||
* }}} | ||
*/ | ||
def traverse1[G[_]: Apply, A, B](fa: F[A])(f: A => G[B]): G[F[B]] | ||
|
||
/** | ||
* Thread all the G effects through the F structure to invert the | ||
* structure from F[G[A]] to G[F[A]]. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> import cats.data.NonEmptyList | ||
* scala> val x = NonEmptyList.of(Map("do" -> 1, "you" -> 1), Map("do" -> 2, "you" -> 1)) | ||
* scala> val y = NonEmptyList.of(Map("How" -> 3, "do" -> 1, "you" -> 1), Map[String,Int]()) | ||
* scala> x.sequence1 | ||
* res0: Map[String,NonEmptyList[Int]] = Map(do -> NonEmptyList(1, 2), you -> NonEmptyList(1, 1)) | ||
* scala> y.sequence1 | ||
* res1: Map[String,NonEmptyList[Int]] = Map() | ||
* }}} | ||
*/ | ||
def sequence1[G[_]: Apply, A](fga: F[G[A]]): G[F[A]] = | ||
traverse1(fga)(identity) | ||
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. This method is untested. |
||
|
||
|
||
/** | ||
* A traverse1 followed by flattening the inner result. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> import cats.data.NonEmptyList | ||
* scala> def countWords(words: List[String]): Map[String, Int] = words.groupBy(identity).mapValues(_.length) | ||
* scala> val x = NonEmptyList.of(List("How", "do", "you", "fly"), List("What", "do", "you", "do")) | ||
* scala> x.flatTraverse1(_.groupByNel(identity)) | ||
* res0: Map[String,cats.data.NonEmptyList[String]] = Map(do -> NonEmptyList(do, do, do), you -> NonEmptyList(you, you)) | ||
* }}} | ||
*/ | ||
def flatTraverse1[G[_], A, B](fa: F[A])(f: A => G[F[B]])(implicit G: Apply[G], F: FlatMap[F]): G[F[B]] = | ||
G.map(traverse1(fa)(f))(F.flatten) | ||
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. Ditto (untested) for this, and |
||
|
||
/** | ||
* Thread all the G effects through the F structure and flatten to invert the | ||
* structure from F[G[F[A]]] to G[F[A]]. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> import cats.data.NonEmptyList | ||
* scala> val x = NonEmptyList.of(Map(0 ->NonEmptyList.of(1, 2)), Map(0 -> NonEmptyList.of(3))) | ||
* scala> val y: NonEmptyList[Map[Int, NonEmptyList[Int]]] = NonEmptyList.of(Map(), Map(1 -> NonEmptyList.of(3))) | ||
* scala> x.flatSequence1 | ||
* res0: Map[Int,cats.data.NonEmptyList[Int]] = Map(0 -> NonEmptyList(1, 2, 3)) | ||
* scala> y.flatSequence1 | ||
* res1: Map[Int,cats.data.NonEmptyList[Int]] = Map() | ||
* }}} | ||
*/ | ||
def flatSequence1[G[_], A](fgfa: F[G[F[A]]])(implicit G: Apply[G], F: FlatMap[F]): G[F[A]] = | ||
G.map(traverse1(fgfa)(identity))(F.flatten) | ||
|
||
override def traverse[G[_] : Applicative, A, B](fa: F[A])(f: (A) => G[B]): G[F[B]] = | ||
traverse1(fa)(f) | ||
|
||
|
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,10 @@ final case class OneAnd[F[_], A](head: A, tail: F[A]) { | |
def forall(p: A => Boolean)(implicit F: Foldable[F]): Boolean = | ||
p(head) && F.forall(tail)(p) | ||
|
||
|
||
def reduceLeft(f: (A, A) => A)(implicit F: Foldable[F]): A = | ||
F.foldLeft(tail, head)(f) | ||
|
||
/** | ||
* Left-associative fold on the structure using f. | ||
*/ | ||
|
@@ -94,7 +98,7 @@ final case class OneAnd[F[_], A](head: A, tail: F[A]) { | |
s"OneAnd(${A.show(head)}, ${FA.show(tail)})" | ||
} | ||
|
||
private[data] sealed trait OneAndInstances extends OneAndLowPriority2 { | ||
private[data] sealed trait OneAndInstances extends OneAndLowPriority3 { | ||
|
||
implicit def catsDataEqForOneAnd[A, F[_]](implicit A: Eq[A], FA: Eq[F[A]]): Eq[OneAnd[F, A]] = | ||
new Eq[OneAnd[F, A]]{ | ||
|
@@ -221,4 +225,22 @@ private[data] trait OneAndLowPriority2 extends OneAndLowPriority1 { | |
} | ||
} | ||
|
||
private[data] trait OneAndLowPriority3 extends OneAndLowPriority2 { | ||
implicit def catsDataTraverse1ForOneAnd[F[_]](implicit F: Traverse[F], F2: MonadCombine[F]): Traverse1[OneAnd[F, ?]] = | ||
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. Can we override This implementation using 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. Great idea! |
||
new NonEmptyReducible[OneAnd[F, ?], F] with Traverse1[OneAnd[F, ?]] { | ||
def traverse1[G[_], A, B](fa: OneAnd[F, A])(f: (A) => G[B])(implicit G: Apply[G]): G[OneAnd[F, B]] = { | ||
import cats.syntax.cartesian._ | ||
|
||
fa.map(a => Apply[G].map(f(a))(OneAnd(_, F2.empty[B])))(F) | ||
.reduceLeft(((acc, a) => (acc |@| a).map((x: OneAnd[F, B], y: OneAnd[F, B]) => x.combine(y)))) | ||
} | ||
|
||
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. Can you add the implementation of |
||
override def traverse[G[_], A, B](fa: OneAnd[F, A])(f: (A) => G[B])(implicit G: Applicative[G]): G[OneAnd[F, B]] = { | ||
G.map2Eval(f(fa.head), Always(F.traverse(fa.tail)(f)))(OneAnd(_, _)).value | ||
} | ||
|
||
def split[A](fa: OneAnd[F, A]): (A, F[A]) = (fa.head, fa.tail) | ||
} | ||
} | ||
|
||
object OneAnd extends OneAndInstances |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package cats | ||
package syntax | ||
|
||
trait Traverse1Syntax extends Traverse1.ToTraverse1Ops |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package cats.laws | ||
|
||
|
||
import cats.{Apply, Id, Semigroup, Traverse1} | ||
import cats.data.{Const, Nested} | ||
import cats.syntax.traverse1._ | ||
import cats.syntax.reducible._ | ||
|
||
trait Traverse1Laws[F[_]] extends TraverseLaws[F] with ReducibleLaws[F] { | ||
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. None of the laws and test were actually hit. We need to add one law test in a data type that has a vanilla |
||
implicit override def F: Traverse1[F] | ||
|
||
def traverse1Identity[A, B](fa: F[A], f: A => B): IsEq[F[B]] = { | ||
fa.traverse1[Id, B](f) <-> F.map(fa)(f) | ||
} | ||
|
||
def traverse1SequentialComposition[A, B, C, M[_], N[_]]( | ||
fa: F[A], | ||
f: A => M[B], | ||
g: B => N[C] | ||
)(implicit | ||
N: Apply[N], | ||
M: Apply[M] | ||
): IsEq[Nested[M, N, F[C]]] = { | ||
|
||
val lhs = Nested(M.map(fa.traverse1(f))(fb => fb.traverse1(g))) | ||
val rhs = fa.traverse1[Nested[M, N, ?], C](a => Nested(M.map(f(a))(g))) | ||
lhs <-> rhs | ||
} | ||
|
||
def traverse1ParallelComposition[A, B, M[_], N[_]]( | ||
fa: F[A], | ||
f: A => M[B], | ||
g: A => N[B] | ||
)(implicit | ||
N: Apply[N], | ||
M: Apply[M] | ||
): IsEq[(M[F[B]], N[F[B]])] = { | ||
type MN[Z] = (M[Z], N[Z]) | ||
implicit val MN = new Apply[MN] { | ||
def ap[X, Y](f: MN[X => Y])(fa: MN[X]): MN[Y] = { | ||
val (fam, fan) = fa | ||
val (fm, fn) = f | ||
(M.ap(fm)(fam), N.ap(fn)(fan)) | ||
} | ||
override def map[X, Y](fx: MN[X])(f: X => Y): MN[Y] = { | ||
val (mx, nx) = fx | ||
(M.map(mx)(f), N.map(nx)(f)) | ||
} | ||
override def product[X, Y](fx: MN[X], fy: MN[Y]): MN[(X, Y)] = { | ||
val (mx, nx) = fx | ||
val (my, ny) = fy | ||
(M.product(mx, my), N.product(nx, ny)) | ||
} | ||
} | ||
val lhs: MN[F[B]] = fa.traverse1[MN, B](a => (f(a), g(a))) | ||
val rhs: MN[F[B]] = (fa.traverse1(f), fa.traverse1(g)) | ||
lhs <-> rhs | ||
} | ||
|
||
def reduceMapDerived[A, B]( | ||
fa: F[A], | ||
f: A => B | ||
)(implicit B: Semigroup[B]): IsEq[B] = { | ||
val lhs: B = fa.traverse1[Const[B, ?], B](a => Const(f(a))).getConst | ||
val rhs: B = fa.reduceMap(f) | ||
lhs <-> rhs | ||
} | ||
} | ||
|
||
object Traverse1Laws { | ||
def apply[F[_]](implicit ev: Traverse1[F]): Traverse1Laws[F] = | ||
new Traverse1Laws[F] { def F: Traverse1[F] = ev } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package cats.laws.discipline | ||
|
||
|
||
import org.scalacheck.{Arbitrary, Cogen, Prop} | ||
import Prop.forAll | ||
import cats.{Applicative, Eq, Monoid, Traverse1} | ||
import cats.laws.Traverse1Laws | ||
|
||
|
||
trait Traverse1Tests[F[_]] extends TraverseTests[F] with ReducibleTests[F] { | ||
def laws: Traverse1Laws[F] | ||
|
||
def traverse1[G[_]: Applicative, A: Arbitrary, B: Arbitrary, C: Arbitrary, M: Arbitrary, X[_]: Applicative, Y[_]: Applicative](implicit | ||
ArbFA: Arbitrary[F[A]], | ||
ArbXB: Arbitrary[X[B]], | ||
ArbYB: Arbitrary[Y[B]], | ||
ArbYC: Arbitrary[Y[C]], | ||
ArbFB: Arbitrary[F[B]], | ||
ArbFGA: Arbitrary[F[G[A]]], | ||
ArbGB: Arbitrary[G[B]], | ||
CogenA: Cogen[A], | ||
CogenB: Cogen[B], | ||
CogenC: Cogen[C], | ||
CogenM: Cogen[M], | ||
M: Monoid[M], | ||
MB: Monoid[B], | ||
EqFA: Eq[F[A]], | ||
EqFC: Eq[F[C]], | ||
EqG: Eq[G[Unit]], | ||
EqM: Eq[M], | ||
EqA: Eq[A], | ||
EqB: Eq[B], | ||
EqXYFC: Eq[X[Y[F[C]]]], | ||
EqXFB: Eq[X[F[B]]], | ||
EqYFB: Eq[Y[F[B]]], | ||
EqOptionA: Eq[Option[A]] | ||
): RuleSet = { | ||
implicit def EqXFBYFB : Eq[(X[F[B]], Y[F[B]])] = new Eq[(X[F[B]], Y[F[B]])] { | ||
override def eqv(x: (X[F[B]], Y[F[B]]), y: (X[F[B]], Y[F[B]])): Boolean = | ||
EqXFB.eqv(x._1, y._1) && EqYFB.eqv(x._2, y._2) | ||
} | ||
new RuleSet { | ||
def name: String = "traverse1" | ||
def bases: Seq[(String, RuleSet)] = Nil | ||
def parents: Seq[RuleSet] = Seq(traverse[A, B, C, M, X, Y], reducible[G, A, B]) | ||
def props: Seq[(String, Prop)] = Seq( | ||
"traverse1 identity" -> forAll(laws.traverse1Identity[A, C] _), | ||
"traverse1 sequential composition" -> forAll(laws.traverse1SequentialComposition[A, B, C, X, Y] _), | ||
"traverse1 parallel composition" -> forAll(laws.traverse1ParallelComposition[A, B, X, Y] _), | ||
"traverse1 derive reduceMap" -> forAll(laws.reduceMapDerived[A, M] _) | ||
) | ||
} | ||
} | ||
} | ||
|
||
object Traverse1Tests { | ||
def apply[F[_]: Traverse1]: Traverse1Tests[F] = | ||
new Traverse1Tests[F] { def laws: Traverse1Laws[F] = Traverse1Laws[F] } | ||
} |
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.
Can we also add some brief scaladocs there?