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

Add NonEmptySet #2143

merged 12 commits into from
Mar 14, 2018

Conversation

LukaJCB
Copy link
Member

@LukaJCB LukaJCB commented Jan 8, 2018

Pretty much the same situation as #2141


import scala.collection.immutable._

final class NonEmptySet[A] private (val set: SortedSet[A]) extends AnyVal {
Copy link
Contributor

Choose a reason for hiding this comment

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

AnyVal boxing can lead to a lot of reboxing when this is used in a generic context, which it would be in cats code since any of the typeclasses use it in generic context. I think we don't actually want AnyVal.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point! Man, we really need opaque types :(

def find(f: A ⇒ Boolean): Option[A] = set.find(f)
def collect[B](pf: PartialFunction[A, B]): Set[B] = set.collect(pf)
def filter(p: A ⇒ Boolean): SortedSet[A] = set.filter(p)
def filterNot(p: A ⇒ Boolean): SortedSet[A] = filter(t => !p(t))
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 we can also add concatMap[B](A => SortedSet[B]): SortedSet[B] and I think it would be useful. We can also do concatMap[B](A => NonEmptySet[B]): NonEmptySet[B]

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 no doubt useful.


import scala.collection.immutable._

final class NonEmptySet[A] private (val set: SortedSet[A]) extends AnyVal {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should this be NonEmptySortedSet? In the future we might want a Hash-based Set so just using Set can be ambiguous.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, yeah in theory this is true, but that's quite a long name, we could just have NonEmptySet and NonEmptyHashSet maybe?

def reduce[AA >: A](implicit S: Semigroup[AA]): AA =
S.combineAllOption(set).get

def toSet: SortedSet[A] = set
Copy link
Contributor

Choose a reason for hiding this comment

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

what about toSortedSet?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

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

Choose a reason for hiding this comment

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

do we need this one vs of? I also like singleton or something better.

Copy link
Member Author

Choose a reason for hiding this comment

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

I took the name from NonEmptyList.one, but I have no preference wrt naming.

Copy link
Contributor

Choose a reason for hiding this comment

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

I vote for consistent API

implicit def catsDataShowForNonEmptySet[A](implicit A: Show[A]): Show[NonEmptySet[A]] =
Show.show[NonEmptySet[A]](_.show)

implicit def catsDataBandForNonEmptySet[A]: Band[NonEmptySet[A]] = new Band[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 is a semilattice isn't it (it is commutative, no?) Am I missing something?

checkAll("NonEmptySet[String]", BandTests[NonEmptySet[String]].band)
checkAll("NonEmptySet[String]", EqTests[NonEmptySet[String]].eqv)


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 test nes.forall { v => Order[Int].lteq(nes.head, v) }

@codecov-io
Copy link

codecov-io commented Jan 8, 2018

Codecov Report

Merging #2143 into master will increase coverage by 0.1%.
The diff coverage is 97.4%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #2143     +/-   ##
=========================================
+ Coverage   94.66%   94.76%   +0.1%     
=========================================
  Files         328      331      +3     
  Lines        5548     5659    +111     
  Branches      213      208      -5     
=========================================
+ Hits         5252     5363    +111     
  Misses        296      296
Impacted Files Coverage Δ
core/src/main/scala/cats/data/package.scala 87.5% <ø> (ø) ⬆️
...rc/main/scala/cats/laws/discipline/Arbitrary.scala 100% <100%> (+1.28%) ⬆️
core/src/main/scala/cats/data/NonEmptySet.scala 97.29% <97.29%> (ø)
core/src/main/scala/cats/data/WriterT.scala 91.37% <0%> (-1.76%) ⬇️
.../src/main/scala/cats/syntax/applicativeError.scala 100% <0%> (ø) ⬆️
core/src/main/scala/cats/syntax/option.scala 100% <0%> (ø) ⬆️
core/src/main/scala/cats/syntax/TrySyntax.scala 100% <0%> (ø)
...c/main/scala/cats/free/ContravariantCoyoneda.scala 100% <0%> (ø)
core/src/main/scala/cats/syntax/either.scala 99.16% <0%> (ø) ⬆️
... and 3 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 d228e95...b709773. Read the comment docs.

@LukaJCB LukaJCB added this to the 1.1 milestone Jan 15, 2018
@LukaJCB
Copy link
Member Author

LukaJCB commented Jan 16, 2018

Maybe we should implement these with newtypes similar to what Alex did here: https://github.com/typelevel/cats-effect/pull/115/files#diff-c01f002766a28f49dfe20d6b1f0afd50R300

Thoughts?

kailuowang
kailuowang previously approved these changes Jan 29, 2018
Copy link
Contributor

@kailuowang kailuowang left a comment

Choose a reason for hiding this comment

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

LGTM

@kailuowang
Copy link
Contributor

kailuowang commented Jan 29, 2018

I am also fine with renaming it to NonEmptySortedSet, it's a long name, but not a very high cost to avoid potential confusion.


final class NonEmptySet[A] private (val set: SortedSet[A]) {

private implicit def ordering: Ordering[A] = set.ordering
Copy link
Member Author

Choose a reason for hiding this comment

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

These should be vals

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.


import scala.collection.immutable._

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.

kailuowang
kailuowang previously approved these changes Feb 8, 2018
@LukaJCB
Copy link
Member Author

LukaJCB commented Feb 9, 2018

@johnynek Care to re-review? :)



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 🙈

package cats
package data

trait Newtype { self =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add some documentation that motivates this trait?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, good idea :)

@@ -0,0 +1,410 @@
/*
* Copyright (c) 2018 Luka Jacobowitz
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason that this file has this header but Newtype.scala doesn't?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope, just copy and paste, Kai suggested we try to standardize on something in a future PR, possibly with sbt-header or something like that :)

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

kailuowang
kailuowang previously approved these changes Mar 5, 2018
Copy link
Contributor

@kailuowang kailuowang left a comment

Choose a reason for hiding this comment

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

on build green

@kailuowang
Copy link
Contributor

@ceedubs any other issues?

@kailuowang
Copy link
Contributor

@johnynek were all your feedback addressed?

* 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).

@LukaJCB
Copy link
Member Author

LukaJCB commented Mar 11, 2018

I removed the context bounds @ceedubs :)

Copy link
Contributor

@ceedubs ceedubs left a comment

Choose a reason for hiding this comment

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

Thanks a bunch @LukaJCB. Just one remaining small question and then this LGTM.

* Helper trait for `newtype`s. These allow you to create a zero-allocation wrapper around a specific type.
* Similar to `AnyVal` value classes, but never have any runtime overhead.
*/
trait Newtype { self =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense for this to be a public trait if Base and Tag are both private[data]?

Copy link
Member Author

Choose a reason for hiding this comment

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

Debatable IMO, but to me those two are implementation details, you're never actually going to want to access these, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh right I guess that in order to create your own type that uses Newtype you don't actually have to set Base and Tag, right?

I guess I'm partially hesitant to expose Newtype publicly because there are a lot of approaches to newtypes in the scala world and I'm not sure whether we want to take on the burden of keeping this one around and stable in cats. Especially since it's an internal implementation detail that isn't really used as an abstraction and is trivial enough that people could easily copy/paste it if they wanted it. I'd be inclined to make it private for now and if it works out well and people want it to be exposed publicly it's easier to open it up later than to make it private later. Admittedly I'm sometimes overly paranoid about this sort of thing...

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd be okay with making them private :)

@kailuowang kailuowang merged commit 3503cea into typelevel:master Mar 14, 2018
@LukaJCB LukaJCB deleted the add-nonemptyset branch March 14, 2018 14:51
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.

6 participants