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

Add Traverse1 typeclass (#1577) #1611

Merged
merged 30 commits into from
Jun 19, 2017
Merged

Add Traverse1 typeclass (#1577) #1611

merged 30 commits into from
Jun 19, 2017

Conversation

LukaJCB
Copy link
Member

@LukaJCB LukaJCB commented Apr 18, 2017

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.

@codecov-io
Copy link

codecov-io commented Apr 22, 2017

Codecov Report

Merging #1611 into master will increase coverage by 0.04%.
The diff coverage is 95.31%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
core/src/main/scala/cats/syntax/reducible.scala 100% <ø> (ø) ⬆️
core/src/main/scala/cats/Reducible.scala 94.91% <ø> (ø) ⬆️
core/src/main/scala/cats/data/NonEmptyVector.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/package.scala 100% <100%> (ø) ⬆️
...in/scala/cats/laws/discipline/ReducibleTests.scala 100% <100%> (ø) ⬆️
...a/cats/laws/discipline/NonEmptyTraverseTests.scala 100% <100%> (ø)
core/src/main/scala/cats/NonEmptyTraverse.scala 100% <100%> (ø)
core/src/main/scala/cats/data/OneAnd.scala 98.36% <100%> (+0.14%) ⬆️
core/src/main/scala/cats/data/Nested.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/Composed.scala 96.15% <100%> (+0.15%) ⬆️
... and 8 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1931c93...50cc4c0. Read the comment docs.

Copy link
Contributor

@edmundnoble edmundnoble left a 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)
Copy link
Contributor

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

@edmundnoble edmundnoble Apr 29, 2017

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]] =
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 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] {
Copy link
Contributor

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, ?]] =
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 curious; why doesn't this use NonEmptyTraverse1?

Copy link
Member Author

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 :)

Copy link
Contributor

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

@edmundnoble edmundnoble Apr 29, 2017

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?

@ceedubs
Copy link
Contributor

ceedubs commented May 13, 2017

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 compose similar to the one defined for Traverse.

@LukaJCB
Copy link
Member Author

LukaJCB commented May 14, 2017

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

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

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

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

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

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

@edmundnoble edmundnoble May 18, 2017

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.

Copy link
Member Author

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 🤔

Copy link
Collaborator

@peterneyens peterneyens left a 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 =
Copy link
Collaborator

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]] =
Copy link
Collaborator

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]] =
Copy link
Collaborator

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] =
Copy link
Collaborator

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] {
Copy link
Collaborator

@peterneyens peterneyens May 28, 2017

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( _ |+| _ ))
}
Copy link
Collaborator

@peterneyens peterneyens May 28, 2017

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.

@peterneyens
Copy link
Collaborator

Thanks for the quick changes @LukaJCB!
I think you may need to fix the conflicts in the typeclasses.md file before travis runs again.

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, ?]] =
Copy link
Collaborator

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Great idea!

@peterneyens
Copy link
Collaborator

Sorry to ask you one more thing, but can you add a Traverse1#compose method (to create a Traverse1[λ[α => F[G[α]]]] instance), like @ceedubs asked here?

See:

We could also merge as is and do this in follow up PR.

@LukaJCB
Copy link
Member Author

LukaJCB commented May 28, 2017

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 {
Copy link
Member Author

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 :)

Copy link
Collaborator

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.

@peterneyens
Copy link
Collaborator

Seems that the Reducible[Nested[NonEmptyList, NonEmptyVector, ?]] instance isn't tested any more after we moved the Traverse1 instance.

I think replacing the ReducibleTests[Nested[NonEmptyList, NonEmptyVector, ?]] by

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

@kailuowang kailuowang mentioned this pull request May 29, 2017
26 tasks
kailuowang
kailuowang previously approved these changes May 30, 2017
@kailuowang kailuowang dismissed their stale review May 30, 2017 14:41

pending rename

@kailuowang
Copy link
Contributor

I agree that Traverse1 probably is not a good enough name. I can live with NonEmptyTraverse as suggested by @LukaJCB here

@peterneyens
Copy link
Collaborator

What do we do in that case with the methodsuffix 1?
Appending NonEmpty becomes a bit unwieldy (traverseNonEmpty, flatSequenceNonEmpty, ...).

@LukaJCB
Copy link
Member Author

LukaJCB commented May 30, 2017

I think I would prefer traverseNonEmpty, but yeah, your point still holds, nonEmptyFlatSequence is quite a bit. Can't think of anything better right now, but I'll try to brainstorm later.

@kailuowang
Copy link
Contributor

I vote for going with NonEmpty, it's not the best, but better than 1

@LukaJCB
Copy link
Member Author

LukaJCB commented Jun 8, 2017

We currently also have traverse1_ and sequence1_ in reducible. I suggest renaming them both and since we'd be breaking compatibility anyway, I also suggest we find better names for these two. I realize the underscore comes from the Haskell equivalent, but I do not believe it conveys its intent.

I suggest something along the lines of traverseVoid or traverseToUnit.

Edit: To be consistent this would require changing them in Foldable as well , which might be too much of a breaking change. Could also create another issue for this.

@andyscott
Copy link
Contributor

I like NonEmpty as a prefix. It's consistent with the naming for NonEmptyList. Additionally, I prefer nonEmpty as a prefix for the associated methods.

@peterneyens
Copy link
Collaborator

I don't think we will be able to come up with anything better than NonEmptyTraverse and nonEmptyTraverse / nonEmptySequence, so 👍.

I think it is best to leave the _ suffix in this PR and create a new issue for a potential rename.

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] _),
Copy link
Contributor

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 ?

Copy link
Member Author

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 😅

@LukaJCB
Copy link
Member Author

LukaJCB commented Jun 12, 2017

There's currently one last thing, in Reducible we have the intercalate1 method. As with traverse1_ and sequence1_, I'm going to rename this to nonEmptyIntercalate if no one objects.

@kailuowang kailuowang merged commit afb8a18 into typelevel:master Jun 19, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants