Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions core/src/main/scala/cats/TraverseFilter.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package cats

import cats.data.State
import simulacrum.{noop, typeclass}

import scala.annotation.implicitNotFound
import scala.collection.immutable.{HashSet, TreeSet}

/**
* `TraverseFilter`, also known as `Witherable`, represents list-like structures
Expand Down Expand Up @@ -85,6 +88,32 @@ trait TraverseFilter[F[_]] extends FunctorFilter[F] {

override def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B] =
traverseFilter[Id, A, B](fa)(f)

/**
* Removes duplicate elements from a list, keeping only the first occurrence.
*/
def ordDistinct[A](fa: F[A])(implicit O: Order[A]): F[A] = {
implicit val ord: Ordering[A] = O.toOrdering

traverseFilter[State[TreeSet[A], *], A, A](fa)(a =>
State(alreadyIn => if (alreadyIn(a)) (alreadyIn, None) else (alreadyIn + a, Some(a)))
)
.run(TreeSet.empty)
.value
._2
}

/**
* Removes duplicate elements from a list, keeping only the first occurrence.
* This is usually faster than ordDistinct, especially for things that have a slow comparion (like String).
*/
def hashDistinct[A](fa: F[A])(implicit H: Hash[A]): F[A] =
traverseFilter[State[HashSet[A], *], A, A](fa)(a =>
State(alreadyIn => if (alreadyIn(a)) (alreadyIn, None) else (alreadyIn + a, Some(a)))
)
.run(HashSet.empty)
.value
._2
Comment on lines +110 to +116

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@takayahilton unfortunately it is not going to work the way it is supposed to.

The passed Hash typeclass is not used here since HashSet from Scala library relies on the Java's hashCode instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(linking to discussions in #4147 and #4185 just for reference)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@satorg note that the law I mentioned in #4147 (comment) effectively makes it okay, I think. At least at the time the PR was merged.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah, I see... Although I'm not getting it – if it is strictly required by the law for Hash to produce the same value as the universal hash does, then why would Hash be useful for at all?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You're right, it doesn't really make sense 😕 see discussion on the topic in #4118 (comment).

The problem is, it's difficult to fix this backwards-compatibly. Because there may be code (such as this PR) that relies on this "law" ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should fix this. This "law" precludes many Hash implementations on opaque types.

}

object TraverseFilter {
Expand Down Expand Up @@ -119,6 +148,8 @@ object TraverseFilter {
typeClassInstance.filterA[G, A](self)(f)(G)
def traverseEither[G[_], B, C](f: A => G[Either[C, B]])(g: (A, C) => G[Unit])(implicit G: Monad[G]): G[F[B]] =
typeClassInstance.traverseEither[G, A, B, C](self)(f)(g)(G)
def ordDistinct(implicit O: Order[A]): F[A] = typeClassInstance.ordDistinct(self)
def hashDistinct(implicit H: Hash[A]): F[A] = typeClassInstance.hashDistinct(self)
}
trait AllOps[F[_], A] extends Ops[F, A] with FunctorFilter.AllOps[F, A] {
type TypeClassType <: TraverseFilter[F]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,4 @@ trait ScalaVersionSpecificTraverseSuite { self: TraverseSuiteAdditional =>

class TraverseLazyListSuite extends TraverseSuite[LazyList]("LazyList")
class TraverseLazyListSuiteUnderlying extends TraverseSuite.Underlying[LazyList]("LazyList")
class TraverseFilterLazyListSuite extends TraverseFilterSuite[LazyList]("LazyList")
43 changes: 43 additions & 0 deletions tests/src/test/scala/cats/tests/TraverseFilterSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cats.tests

import cats.data.Chain
import cats.instances.all._
import cats.laws.discipline.arbitrary.catsLawsArbitraryForChain
import cats.syntax.eq._
import cats.syntax.foldable._
import cats.syntax.traverseFilter._
import cats.{Traverse, TraverseFilter}
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

import scala.collection.immutable.Queue

abstract class TraverseFilterSuite[F[_]: TraverseFilter](name: String)(implicit
ArbFInt: Arbitrary[F[Int]],
ArbFString: Arbitrary[F[String]]
) extends CatsSuite {

implicit def T: Traverse[F] = implicitly[TraverseFilter[F]].traverse

test(s"TraverseFilter[$name].ordDistinct") {
forAll { (fa: F[Int]) =>
fa.ordDistinct.toList === fa.toList.distinct
}
}

test(s"TraverseFilter[$name].hashDistinct") {
forAll { (fa: F[String]) =>
fa.hashDistinct.toList === fa.toList.distinct
}
}
}

class TraverseFilterListSuite extends TraverseFilterSuite[List]("list")

class TraverseFilterVectorSuite extends TraverseFilterSuite[Vector]("vector")

class TraverseFilterChainSuite extends TraverseFilterSuite[Chain]("chain")

class TraverseFilterQueueSuite extends TraverseFilterSuite[Queue]("queue")

class TraverseFilterStreamSuite extends TraverseFilterSuite[Stream]("stream")