-
-
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
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1611 +/- ##
==========================================
+ Coverage 93.88% 93.92% +0.04%
==========================================
Files 246 249 +3
Lines 4088 4148 +60
Branches 154 155 +1
==========================================
+ Hits 3838 3896 +58
- Misses 250 252 +2
Continue to review full report at Codecov.
|
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.
Few comments mostly about testing, but otherwise LGTM.
def traverse1[G[_]: Apply, A, B](fa: F[A])(f: A => G[B]): G[F[B]] | ||
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
This method is untested.
traverse1(fga)(identity) | ||
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto (untested) for this, and flatSequence
, sequence
, traverse1U
, sequence1U
in the same class.
def traverse1U[A, GB](fa: F[A])(f: A => GB)(implicit G: Unapply[Apply, GB]): G.M[F[G.A]] = | ||
traverse1(fa)(G.subst.compose(f))(G.TC) | ||
|
||
def sequenceU1[GA](fga: F[GA])(implicit U: Unapply[Apply, GA]): U.M[F[U.A]] = |
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.
Should this perhaps be sequence1U
?
* This class can be used on any type where the first value (`A`) and | ||
* the "rest" of the values (`G[A]`) can be easily found. | ||
*/ | ||
abstract class NonEmptyTraverse1[F[_], G[_]](implicit G: Foldable[G] with Traverse[G]) extends Traverse1[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.
Tests?
new Traverse[OneAnd[F, ?]] { | ||
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 | ||
implicit def catsDataTraverseForOneAnd[F[_]](implicit F: Traverse[F], F2: MonadCombine[F]): Traverse1[OneAnd[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.
I'm curious; why doesn't this use NonEmptyTraverse1
?
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.
Hey, I'm not sure what exactly you mean by this comment, if you could clarify, that'd be great :)
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.
@LukaJCB I think that @edmundnoble is asking why the Traverse1
instance for OneAnd
isn't using the NonEmptyTraverse1
abstract class that you've submitted with this PR.
new NonEmptyReducible[NonEmptyList, List] with SemigroupK[NonEmptyList] with Comonad[NonEmptyList] | ||
with Traverse[NonEmptyList] with Monad[NonEmptyList] { | ||
with Traverse1[NonEmptyList] with Monad[NonEmptyList] { |
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.
This one as well, why not NonEmptyTraverse1
it up?
Thanks @LukaJCB! This is good stuff. If you would like to move forward with this, could you please address Edmund's review comments? Also I think that you should be able to implement a |
Yeah, I'm sorry I've been so inactive, have been quite busy the past few weeks, but I'll get to this in the next couple of days! :) |
import cats.data.NonEmptyList | ||
import simulacrum.typeclass | ||
|
||
@typeclass trait Traverse1[F[_]] extends Traverse[F] with Reducible[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?
|
||
|
||
|
||
def traverse1U[A, GB](fa: F[A])(f: A => GB)(implicit G: Unapply[Apply, GB]): G.M[F[G.A]] = |
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.
all unapplied versions are no longer needed
package syntax | ||
|
||
private[syntax] trait Traverse1Syntax1 { | ||
implicit def catsSyntaxUTraverse1[FA](fa: FA)(implicit U: Unapply[Traverse1, FA]): Traverse1.Ops[U.M, U.A] = |
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.
We don't need this unapply version anymore.
new NestedReducibleOps[F, G, A](fga) | ||
private[syntax] trait ReducibleSyntax1 { | ||
implicit def catsSyntaxUReducible[FA](fa: FA)(implicit U: Unapply[Reducible, FA]): Reducible.Ops[U.M, U.A] = | ||
new Reducible.Ops[U.M, U.A] { |
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.
we can use a doctest in the type class to cover this.
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 comment
The 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 Traverse1
instance and one for a data type that uses the NonEmptyTraverse1
.
* This class can be used on any type where the first value (`A`) and | ||
* the "rest" of the values (`G[A]`) can be easily found. | ||
*/ | ||
abstract class NonEmptyReducible[F[_], G[_]](implicit G: Foldable[G]) extends Reducible[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.
implicit *val* G
, otherwise this is a param the class is closing over and not a field, thus not serializable.
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.
Unfortunately adding val
didn't seem to fix the issue, I'll have to look into it further 🤔
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.
I left a few comments.
The two most important are the one about the traverse1
implementations and about NonEmptyTraverse1
.
override def sequence[G[_] : Applicative, A](fga: F[G[A]]): G[F[A]] = | ||
traverse1(fga)(identity) | ||
|
||
override def reduceMap[A, B](fa: F[A])(f: (A) => B)(implicit B: Semigroup[B]): B = |
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.
I think foldMap1
is implemented in Scalaz using traverse1
because that way Traverse1
implements Foldable1
as well. The primitive operations of Reducible
are reduceLeftTo
and reduceRightTo
.
I don't think it makes any sense for us to override reduceMap
here.
Relevant discussion about Traverse
implementing Foldable
: #107.
override def traverse[G[_] : Applicative, A, B](fa: F[A])(f: (A) => G[B]): G[F[B]] = | ||
traverse1(fa)(f) | ||
|
||
override def sequence[G[_] : Applicative, A](fga: F[G[A]]): G[F[A]] = |
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.
By overriding traverse
we already get this, not sure if we should explicitly override this?
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) | ||
|
||
def flatSequence[G[_], A](fgfa: F[G[F[A]]])(implicit G: Apply[G], F: FlatMap[F]): G[F[A]] = |
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.
Did you mean to call this flatSequence1
?
override def reduceMap[A, B](fa: F[A])(f: (A) => B)(implicit B: Semigroup[B]): B = | ||
reduceLeft(traverse1[Id, A, B](fa)(f))(B.combine) | ||
|
||
override def map[A, B](fa: F[A])(f: A => B): F[B] = |
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.
We get this by overriding traverse
as well.
* This class can be used on any type where the first value (`A`) and | ||
* the "rest" of the values (`G[A]`) can be easily found. | ||
*/ | ||
abstract class NonEmptyTraverse1[F[_], G[_]](implicit G: Traverse[G]) extends Traverse1[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.
I am not sure about this. This is just a copy of NonEmptyReducible
where the implicit is changed from Foldable[G]
to Traverse[G]
, but we are not actually using anything from Traverse
in this class, right?
I would drop this class and use new NonEmptyReducible[F, G] extends Traverse1[G]
in place of new NonEmptyTraverse1[F, G]
.
def traverse1[G[_] : Apply, A, B](as: NonEmptyList[A])(f: A => G[B]): G[NonEmptyList[B]] = { | ||
as.map(a => Apply[G].map(f(a))(NonEmptyList.of(_))) | ||
.reduceLeft((acc, a) => (acc |@| a).map( _ |+| _ )) | ||
} |
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.
This is not really the best implementation of traverse1
.
A better implementation :
def traverse1[G[_], A, B](nel: NonEmptyList[A])(f: A => G[B])(implicit G: Apply[G]): G[NonEmptyList[B]] =
Foldable[List].reduceRightToOption[A, G[List[B]]](nel.tail)(a => G.map(f(a))(_ :: Nil)) { (a, lglb) =>
G.map2Eval(f(a), lglb)(_ :: _)
}.map {
case None => G.map(f(nel.head))(NonEmptyList(_, Nil))
case Some(gtail) => G.map2(f(nel.head), gtail)(NonEmptyList(_, _))
}.value
Traverse[Vector].traverse
also uses prepending (+:
), so I think this should work for NonEmptyVector
as well. For OneAnd
we need to come up with something else if we want to improve that one as well.
A benchmark comparing your traverse1
to mine above and a copy of yours using NonEmptyList(_, Nil)
instead of NonEmptyList.of
.
[info] Benchmark Mode Cnt Score Error Units
[info] NelTraverseBench.traverse1noOf thrpt 10 10977.183 ± 240.703 ops/s
[info] NelTraverseBench.traverse1prime thrpt 10 89787.940 ± 849.006 ops/s
[info] NelTraverseBench.traverse1 thrpt 10 3929.498 ± 72.846 ops/s
The difference of just replacing NonEmptyList.of
is already quite big, so I am plugging #1707 here again.
Thanks for the quick changes @LukaJCB! |
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 | ||
} | ||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Can we override traverse
using the implementation originally used in Traverse
?
We probably want to override the traverse
for NonEmptyVector
as well using the previous implementation, come to think of it.
This implementation using MonadCombine
also means that you cannot get a Traverse[OneAnd[F, ?]]
instance anymore if F
has a Traverse
but no MonadCombine
.
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.
Great idea!
Sorry to ask you one more thing, but can you add a See:
We could also merge as is and do this in follow up PR. |
Don't be! I'm on mobile right now, but I'll do it in the morning tomorrow :) |
implicit def catsDataInvariantForNestedContravariant[F[_]: Invariant, G[_]: Contravariant]: Invariant[Nested[F, G, ?]] = | ||
new NestedInvariant[F, G] { | ||
val FG: Invariant[λ[α => F[G[α]]]] = Invariant[F].composeContravariant[G] | ||
} | ||
} | ||
|
||
private[data] sealed abstract class NestedInstances11 { |
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.
Not sure if making a new class here was the best thing to do, feedback very welcome :)
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.
I think you can put the catsDataTraverse1ForNested
inside NestedInstances
.
As you can never have a TraverseFilter
and a Traverse1
for a data type at the same time, I don't think that the composition Traverse
and TraverseFilter
versus the composition of Traverse1
and Traverse1
can be ambiguous.
Seems that the I think replacing the {
implicit val foldable = ListWrapper.foldable
checkAll("Nested[NonEmptyList, OneAnd[ListWrapper, ?], ?]", ReducibleTests[Nested[NonEmptyList, OneAnd[ListWrapper, ?], ?]].reducible[Option, Int, Int])
checkAll("Reducible[Nested[NonEmptyList, OneAnd[ListWrapper, ?], ?]]", SerializableTests.serializable(Reducible[Nested[NonEmptyList, OneAnd[ListWrapper, ?], ?]]))
} should fix this. |
What do we do in that case with the methodsuffix |
I think I would prefer |
I vote for going with |
We currently also have I suggest something along the lines of Edit: To be consistent this would require changing them in |
I like |
I don't think we will be able to come up with anything better than I think it is best to leave the |
def props: Seq[(String, Prop)] = Seq( | ||
"nonEmptyTraverse identity" -> forAll(laws.nonEmptyTraverseIdentity[A, C] _), | ||
"nonEmptyTraverse sequential composition" -> forAll(laws.traverseSequentialComposition[A, B, C, X, Y] _), | ||
"nonEmptyTraverse parallel composition" -> forAll(laws.traverseParallelComposition[A, B, X, Y] _), |
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.
should we add tests for laws.nonEmptyTraverseParallelComposition
and laws.nonEmptyTraverseSequentialComposition
?
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.
Yeah, not sure what happened there 😅
There's currently one last thing, in |
Resolves #1577. Looking for feedback on this one, not sure if there's an easier way to implement it, or if there's general things to improve.