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 NonEmptySet #2143

Merged
merged 12 commits into from
Mar 14, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
140 changes: 73 additions & 67 deletions core/src/main/scala/cats/data/NonEmptySet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,54 @@ import cats.{Always, Eq, Eval, Foldable, Later, Now, Reducible, SemigroupK, Show

import scala.collection.immutable._

final class NonEmptySet[A] private (val set: SortedSet[A]) {
trait Newtype { self =>
Copy link
Contributor

@andyscott andyscott Feb 5, 2018

Choose a reason for hiding this comment

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

If it's staying public (or even package private) then let's move NewType to a new file.

private[data] type Base
private[data] trait Tag extends Any
private[cats] type Type[A] <: Base with Tag
}

private[data] object NonEmptySetImpl extends NonEmptySetInstances with Newtype {
Copy link
Contributor

Choose a reason for hiding this comment

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

shall we override the toString as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

We can't really do that, since we can't actually add methods on the type, but only through implicit extension methods. This means that it will always call the toString of the underlying type, we can of course use show though.


private[cats] def create[A](s: SortedSet[A]): Type[A] =
s.asInstanceOf[Type[A]]

private[cats] def unwrap[A](s: Type[A]): SortedSet[A] =
s.asInstanceOf[SortedSet[A]]


def fromSet[A: Order](as: SortedSet[A]): Option[NonEmptySet[A]] =
Copy link
Contributor

Choose a reason for hiding this comment

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

This takes an implicit Order[A] but then doesn't actually use it, does it? That seems misleading to me.

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 that's true, it assumes there's an Ordering for A, maybe best to get rid of it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the last bit that prevents this PR being merged?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's done :)

if (as.nonEmpty) Option(create(as)) else None

def fromSetUnsafe[A: Order](set: SortedSet[A]): NonEmptySet[A] =
if (set.nonEmpty) create(set)
else throw new IllegalArgumentException("Cannot create NonEmptySet from empty set")


def of[A: Order](a: A, as: A*): NonEmptySet[A] =
create(SortedSet(a)(Order[A].toOrdering) ++ SortedSet(as: _*)(Order[A].toOrdering) + a)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this adding a to the set twice? (Initializing with SortedSet(a) and then calling + a.)

Copy link
Contributor

Choose a reason for hiding this comment

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

This is making two separate calls to toOrdering. If the ordering is needed twice, a slight optimization would be to store the result of toOrdering. Also, on a more minor note, the Order[A] instance is being summoned twice, so if the implicit instance is defined by a def this will result in unnecessary allocations.

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, you're absolutely right, this is a really stupid mistake, not sure how it slipped through, probably refactored it one to many times 🙈

def apply[A: Order](head: A, tail: SortedSet[A]): NonEmptySet[A] = create(SortedSet(head)(Order[A].toOrdering) ++ tail)
def one[A: Order](a: A): NonEmptySet[A] = create(SortedSet(a)(Order[A].toOrdering))

implicit def catsNonEmptySetOps[A](value: NonEmptySet[A]): NonEmptySetOps[A] =
new NonEmptySetOps(value)
}


private[data] sealed class NonEmptySetOps[A](val value: NonEmptySetImpl.Type[A]) {

private implicit val ordering: Ordering[A] = toSortedSet.ordering
private implicit val order: Order[A] = Order.fromOrdering

/**
* Converts this set to a `SortedSet`
*/
def toSortedSet: SortedSet[A] = NonEmptySetImpl.unwrap(value)

private implicit def ordering: Ordering[A] = set.ordering
private implicit def order: Order[A] = Order.fromOrdering

/**
* Adds an element to this set, returning a new `NonEmptySet`
* {{{
* scala> import cats.data.NonEmptySet
* scala> import cats.implicits._
* scala> val nes = NonEmptySet.of(1, 2, 4, 5)
* scala> nes + 3
* res0: cats.data.NonEmptySet[Int] = NonEmptyTreeSet(1, 2, 3, 4, 5)
* }}}
*/
def +(a: A): NonEmptySet[A] = new NonEmptySet(set + a)
def add(a: A): NonEmptySet[A] = NonEmptySet.create(toSortedSet + a)

/**
* Alias for [[union]]
Expand All @@ -47,7 +79,7 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* scala> import cats.implicits._
* scala> val nes = NonEmptySet.of(1, 2, 4, 5)
* scala> nes ++ NonEmptySet.of(1, 2, 7)
* res0: cats.data.NonEmptySet[Int] = NonEmptyTreeSet(1, 2, 4, 5, 7)
* res0: cats.data.NonEmptySet[Int] = TreeSet(1, 2, 4, 5, 7)
* }}}
*/
def ++(as: NonEmptySet[A]): NonEmptySet[A] = union(as)
Expand All @@ -59,7 +91,7 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* scala> import cats.implicits._
* scala> val nes = NonEmptySet.of(1, 2, 4, 5)
* scala> nes | NonEmptySet.of(1, 2, 7)
* res0: cats.data.NonEmptySet[Int] = NonEmptyTreeSet(1, 2, 4, 5, 7)
* res0: cats.data.NonEmptySet[Int] = TreeSet(1, 2, 4, 5, 7)
* }}}
*/
def | (as: NonEmptySet[A]): NonEmptySet[A] = union(as)
Expand Down Expand Up @@ -104,13 +136,13 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
/**
* Removes a key from this set, returning a new `SortedSet`.
*/
def -(a: A): SortedSet[A] = set - a
def -(a: A): SortedSet[A] = toSortedSet - a

/**
* Applies f to all the elements
*/
def map[B: Order](f: A ⇒ B): NonEmptySet[B] =
new NonEmptySet(SortedSet(set.map(f).to: _*)(Order[B].toOrdering))
NonEmptySetImpl.create(SortedSet(toSortedSet.map(f).to: _*)(Order[B].toOrdering))

/**
* Converts this set to a `NonEmptyList`.
Expand All @@ -122,22 +154,22 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* res0: cats.data.NonEmptyList[Int] = NonEmptyList(1, 2, 3, 4, 5)
* }}}
*/
def toNonEmptyList: NonEmptyList[A] = NonEmptyList.fromListUnsafe(set.toList)
def toNonEmptyList: NonEmptyList[A] = NonEmptyList.fromListUnsafe(toSortedSet.toList)

/**
* Returns the first element of this set.
*/
def head: A = set.head
def head: A = toSortedSet.head

/**
* Returns all but the first element of this set.
*/
def tail: SortedSet[A] = set.tail
def tail: SortedSet[A] = toSortedSet.tail

/**
* Returns the last element of this set.
*/
def last: A = set.last
def last: A = toSortedSet.last

/**
* Alias for [[contains]]
Expand All @@ -156,55 +188,50 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
/**
* Tests if some element is contained in this set.
*/
def contains(a: A): Boolean = set(a)
def contains(a: A): Boolean = toSortedSet(a)

/**
* Computes the difference of this set and another set.
*/
def diff(as: NonEmptySet[A]): SortedSet[A] = set -- as.set
def diff(as: NonEmptySet[A]): SortedSet[A] = toSortedSet -- as.toSortedSet

/**
* Computes the union between this NES and another NES.
*/
def union(as: NonEmptySet[A]): NonEmptySet[A] = new NonEmptySet(set ++ as.set)
def union(as: NonEmptySet[A]): NonEmptySet[A] = NonEmptySetImpl.create(toSortedSet ++ as.toSortedSet)

/**
* Computes the intersection between this set and another set.
*/
def intersect(as: NonEmptySet[A]): SortedSet[A] = set.filter(as.apply)

/**
* Returns the number of elements in this set.
*/
def size: Int = set.size
def intersect(as: NonEmptySet[A]): SortedSet[A] = toSortedSet.filter(as.apply)

/**
* Tests whether a predicate holds for all elements of this set.
*/
def forall(p: A ⇒ Boolean): Boolean = set.forall(p)
def forall(p: A ⇒ Boolean): Boolean = toSortedSet.forall(p)

/**
* Tests whether a predicate holds for at least one element of this set.
*/
def exists(f: A ⇒ Boolean): Boolean = set.exists(f)
def exists(f: A ⇒ Boolean): Boolean = toSortedSet.exists(f)

/**
* Returns the first value that matches the given predicate.
*/
def find(f: A ⇒ Boolean): Option[A] = set.find(f)
def find(f: A ⇒ Boolean): Option[A] = toSortedSet.find(f)

/**
* Returns a new `SortedSet` containing all elements where the result of `pf` is defined.
*/
def collect[B: Order](pf: PartialFunction[A, B]): SortedSet[B] = {
implicit val ordering = Order[B].toOrdering
Copy link
Contributor

Choose a reason for hiding this comment

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

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 there are some other places that this applies to this PR. I don't necessarily consider this a blocker, but I'd like us to at least decide what we want to do about this before we publish a release that has the name of the implicit parameter fixed (I think that the auto-generated one will be something like ev1).

set.collect(pf)
toSortedSet.collect(pf)
}

/**
* Filters all elements of this set that do not satisfy the given predicate.
*/
def filter(p: A ⇒ Boolean): SortedSet[A] = set.filter(p)
def filter(p: A ⇒ Boolean): SortedSet[A] = toSortedSet.filter(p)

/**
* Filters all elements of this set that satisfy the given predicate.
Expand All @@ -216,19 +243,19 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* Left-associative fold using f.
*/
def foldLeft[B](b: B)(f: (B, A) => B): B =
set.foldLeft(b)(f)
toSortedSet.foldLeft(b)(f)

/**
* Right-associative fold using f.
*/
def foldRight[B](lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
Foldable[SortedSet].foldRight(set, lb)(f)
Foldable[SortedSet].foldRight(toSortedSet, lb)(f)

/**
* Left-associative reduce using f.
*/
def reduceLeft(f: (A, A) => A): A =
set.reduceLeft(f)
toSortedSet.reduceLeft(f)

/**
* Apply `f` to the "initial element" of this set and lazily combine it
Expand Down Expand Up @@ -260,7 +287,7 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* Reduce using the Semigroup of A
*/
def reduce[AA >: A](implicit S: Semigroup[AA]): AA =
S.combineAllOption(set).get
S.combineAllOption(toSortedSet).get

/**
* Map a function over all the elements of this set and concatenate the resulting sets into one.
Expand All @@ -269,20 +296,15 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* scala> import cats.implicits._
* scala> val nes = NonEmptySet.of(1, 2, 3)
* scala> nes.concatMap(n => NonEmptySet.of(n, n * 4, n * 5))
* res0: cats.data.NonEmptySet[Int] = NonEmptyTreeSet(1, 2, 3, 4, 5, 8, 10, 12, 15)
* res0: cats.data.NonEmptySet[Int] = TreeSet(1, 2, 3, 4, 5, 8, 10, 12, 15)
* }}}
*/
def concatMap[B: Order](f: A => NonEmptySet[B]): NonEmptySet[B] = {
implicit val ordering = Order[B].toOrdering
new NonEmptySet(set.flatMap(f andThen (_.set)))
NonEmptySetImpl.create(toSortedSet.flatMap(f andThen (_.toSortedSet)))
}


/**
* Converts this set to a `SortedSet`
*/
def toSortedSet: SortedSet[A] = set

/**
* Typesafe stringification method.
*
Expand All @@ -291,7 +313,7 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* universal .toString method.
*/
def show(implicit A: Show[A]): String =
s"NonEmpty${Show[SortedSet[A]].show(set)}"
s"NonEmpty${Show[SortedSet[A]].show(toSortedSet)}"

/**
* Typesafe equality operator.
Expand All @@ -302,14 +324,12 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* universal equality provided by .equals.
*/
def ===(that: NonEmptySet[A]): Boolean =
Eq[SortedSet[A]].eqv(set, that.toSortedSet)
Eq[SortedSet[A]].eqv(toSortedSet, that.toSortedSet)

/**
* Alias for [[size]]
* Returns the number of elements in this set.
*/
def length: Int = size

override def toString: String = s"NonEmpty${set.toString}"
def length: Int = toSortedSet.size

/**
* Zips this `NonEmptySet` with another `NonEmptySet` and applies a function for each pair of elements.
Expand All @@ -320,17 +340,17 @@ final class NonEmptySet[A] private (val set: SortedSet[A]) {
* scala> val as = NonEmptySet.of(1, 2, 3)
* scala> val bs = NonEmptySet.of("A", "B", "C")
* scala> as.zipWith(bs)(_ + _)
* res0: cats.data.NonEmptySet[String] = NonEmptyTreeSet(1A, 2B, 3C)
* res0: cats.data.NonEmptySet[String] = TreeSet(1A, 2B, 3C)
* }}}
*/
def zipWith[B, C: Order](b: NonEmptySet[B])(f: (A, B) => C): NonEmptySet[C] =
new NonEmptySet(SortedSet((set, b.toSortedSet).zipped.map(f).to: _*)(Order[C].toOrdering))
NonEmptySetImpl.create(SortedSet((toSortedSet, b.toSortedSet).zipped.map(f).to: _*)(Order[C].toOrdering))

/**
* Zips this `NonEmptySet` with its index.
*/
def zipWithIndex: NonEmptySet[(A, Int)] =
new NonEmptySet(set.zipWithIndex)
NonEmptySetImpl.create(toSortedSet.zipWithIndex)
}

private[data] sealed abstract class NonEmptySetInstances {
Expand Down Expand Up @@ -393,17 +413,3 @@ private[data] sealed abstract class NonEmptySetInstances {
}
}

object NonEmptySet extends NonEmptySetInstances {
def fromSet[A: Order](as: SortedSet[A]): Option[NonEmptySet[A]] =
if (as.nonEmpty) Option(new NonEmptySet(as)) else None

def fromSetUnsafe[A: Order](set: SortedSet[A]): NonEmptySet[A] =
if (set.nonEmpty) new NonEmptySet(set)
else throw new IllegalArgumentException("Cannot create NonEmptySet from empty set")


def of[A: Order](a: A, as: A*): NonEmptySet[A] =
new NonEmptySet(SortedSet(a)(Order[A].toOrdering) ++ SortedSet(as: _*)(Order[A].toOrdering) + a)
def apply[A: Order](head: A, tail: SortedSet[A]): NonEmptySet[A] = new NonEmptySet(SortedSet(head)(Order[A].toOrdering) ++ tail)
def one[A: Order](a: A): NonEmptySet[A] = new NonEmptySet(SortedSet(a)(Order[A].toOrdering))
}
3 changes: 3 additions & 0 deletions core/src/main/scala/cats/data/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ package object data {
def NonEmptyStream[A](head: A, tail: A*): NonEmptyStream[A] =
OneAnd(head, tail.toStream)

type NonEmptySet[A] = NonEmptySetImpl.Type[A]
val NonEmptySet = NonEmptySetImpl

type ReaderT[F[_], A, B] = Kleisli[F, A, B]
val ReaderT = Kleisli

Expand Down
5 changes: 2 additions & 3 deletions tests/src/test/scala/cats/tests/NonEmptySetSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class NonEmptySetSuite extends CatsSuite {

test("+ consistent with Set") {
forAll { (nes: NonEmptySet[Int], i: Int) =>
(nes + i).toSortedSet should === (nes.toSortedSet + i)
(nes add i).toSortedSet should === (nes.toSortedSet + i)
}
}

Expand All @@ -209,9 +209,8 @@ class NonEmptySetSuite extends CatsSuite {
}
}

test("NonEmptySet#size and length is consistent with Set#size") {
test("NonEmptySet#length is consistent with Set#size") {
forAll { nes: NonEmptySet[Int] =>
nes.size should === (nes.toSortedSet.size)
nes.length should === (nes.toSortedSet.size)
}
}
Expand Down