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

Make NonEmptyVector a value class. #1204 #1212

Merged
merged 3 commits into from
Jul 28, 2016
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
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ lazy val disciplineDependencies = Seq(
lazy val testingDependencies = Seq(
libraryDependencies += "org.typelevel" %%% "catalysts-platform" % "0.0.2",
libraryDependencies += "org.typelevel" %%% "catalysts-macros" % "0.0.2" % "test",
libraryDependencies += "org.scalatest" %%% "scalatest" % "3.0.0-M7" % "test")
libraryDependencies += "org.scalatest" %%% "scalatest" % "3.0.0-M8" % "test")


/**
Expand Down Expand Up @@ -208,7 +208,7 @@ lazy val kernel = crossProject.crossType(CrossType.Pure)
.jsSettings(commonJsSettings:_*)
.jvmSettings((commonJvmSettings ++ (mimaPreviousArtifacts := Set("org.typelevel" %% "cats-kernel" % "0.6.0"))):_*)

lazy val kernelJVM = kernel.jvm
lazy val kernelJVM = kernel.jvm
lazy val kernelJS = kernel.js

lazy val kernelLaws = crossProject.crossType(CrossType.Pure)
Expand Down
45 changes: 23 additions & 22 deletions core/src/main/scala/cats/data/NonEmptyVector.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import scala.collection.immutable.VectorBuilder
import cats.instances.vector._

/**
* A data type which represents a non empty Vector.
* A data type which represents a `Vector` guaranteed to contain at least one element.
* <br/>
* Note that the constructor is `private` to prevent accidental construction of an empty
* `NonEmptyVector`. However, due to https://issues.scala-lang.org/browse/SI-6601, on
* Scala 2.10, this may be bypassed due to a compiler bug.
*/
final case class NonEmptyVector[A] private (toVector: Vector[A]) {
final class NonEmptyVector[A] private (val toVector: Vector[A]) extends AnyVal {

/** Gets the element at the index, if it exists */
def get(i: Int): Option[A] =
Expand All @@ -19,14 +23,14 @@ final case class NonEmptyVector[A] private (toVector: Vector[A]) {

/** Updates the element at the index, if it exists */
def updated(i: Int, a: A): Option[NonEmptyVector[A]] =
if (toVector.isDefinedAt(i)) Some(NonEmptyVector(toVector.updated(i, a))) else None
if (toVector.isDefinedAt(i)) Some(new NonEmptyVector(toVector.updated(i, a))) else None
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 a bit nervous about isDefinedAt I'm afraid this could be coming from Function and not be specialized well for vector. What about:

@inline
private def inBounds(i: Int): Boolean = 0 <= i && i <= toVector.size

And use it here and on get?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea we could do that. For reference Vector inherits isDefinedAt from GenSeqLike - https://github.com/scala/scala/blob/v2.11.8/src/library/scala/collection/GenSeqLike.scala#L64-L72

Definition is pretty much the same, with the exception that a local implementation can be private and inlined.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, this is fine then. (Lacking an IDE, finding such definitions is really painful for me in the insane collections hierarchy).


/**
* Updates the element at the index, or throws an `IndexOutOfBoundsException`
* if none exists (if `i` does not satisfy `0 <= i < length`).
*/
def updatedUnsafe(i: Int, a: A):
NonEmptyVector[A] = NonEmptyVector(toVector.updated(i, a))
NonEmptyVector[A] = new NonEmptyVector(toVector.updated(i, a))

def head: A = toVector.head

Expand All @@ -38,27 +42,22 @@ final case class NonEmptyVector[A] private (toVector: Vector[A]) {
def filter(f: A => Boolean): Vector[A] = toVector.filter(f)

/**
* Append another NonEmptyVector to this
* Alias for [[concat]]
*/
def concat(other: NonEmptyVector[A]): NonEmptyVector[A] = NonEmptyVector(toVector ++ other.toVector)

/**
* Alias for concat
*/
def ++(other: NonEmptyVector[A]): NonEmptyVector[A] = concat(other)
def ++(other: Vector[A]): NonEmptyVector[A] = concat(other)

/**
* Append another Vector to this
* Append another `Vector` to this, producing a new `NonEmptyVector`.
*/
def concat(other: Vector[A]): NonEmptyVector[A] = NonEmptyVector(toVector ++ other)
def concat(other: Vector[A]): NonEmptyVector[A] = new NonEmptyVector(toVector ++ other)

/**
* Alias for concat
* Append another `NonEmptyVector` to this, producing a new `NonEmptyVector`.
*/
def ++(other: Vector[A]): NonEmptyVector[A] = concat(other)
def concatNEV(other: NonEmptyVector[A]): NonEmptyVector[A] = new NonEmptyVector(toVector ++ other.toVector)
Copy link
Contributor

Choose a reason for hiding this comment

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

any reason not to do = concat(other.toVector)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No reason I can think of. I wouldn't have a huge preference either way because they are functionally equivalent.


/**
* find the first element matching the predicate, if one exists
* Find the first element matching the predicate, if one exists
*/
def find(f: A => Boolean): Option[A] = toVector.find(f)

Expand Down Expand Up @@ -88,13 +87,13 @@ final case class NonEmptyVector[A] private (toVector: Vector[A]) {
* Applies f to all the elements
*/
def map[B](f: A => B): NonEmptyVector[B] =
NonEmptyVector(toVector.map(f))
new NonEmptyVector(toVector.map(f))

/**
* Applies f to all elements and combines the result
*/
def flatMap[B](f: A => NonEmptyVector[B]): NonEmptyVector[B] =
NonEmptyVector(toVector.flatMap(a => f(a).toVector))
new NonEmptyVector(toVector.flatMap(a => f(a).toVector))

/**
* Left-associative reduce using f.
Expand Down Expand Up @@ -129,6 +128,8 @@ final case class NonEmptyVector[A] private (toVector: Vector[A]) {
s"NonEmpty${Show[Vector[A]].show(toVector)}"

def length: Int = toVector.length

override def toString: String = s"NonEmpty${toVector.toString}"
}

private[data] sealed trait NonEmptyVectorInstances {
Expand All @@ -139,7 +140,7 @@ private[data] sealed trait NonEmptyVectorInstances {
with Comonad[NonEmptyVector] with Traverse[NonEmptyVector] with MonadRec[NonEmptyVector] {

def combineK[A](a: NonEmptyVector[A], b: NonEmptyVector[A]): NonEmptyVector[A] =
a concat b
a concatNEV b

override def split[A](fa: NonEmptyVector[A]): (A, Vector[A]) = (fa.head, fa.tail)

Expand Down Expand Up @@ -213,19 +214,19 @@ private[data] sealed trait NonEmptyVectorInstances {
object NonEmptyVector extends NonEmptyVectorInstances {

def apply[A](head: A, tail: Vector[A]): NonEmptyVector[A] =
NonEmptyVector(head +: tail)
new NonEmptyVector(head +: tail)

def apply[A](head: A, tail: A*): NonEmptyVector[A] = {
val buf = Vector.newBuilder[A]
buf += head
tail.foreach(buf += _)
NonEmptyVector(buf.result)
new NonEmptyVector(buf.result)
}

def fromVector[A](vector: Vector[A]): Option[NonEmptyVector[A]] =
if (vector.isEmpty) None else Some(new NonEmptyVector(vector))

def fromVectorUnsafe[A](vector: Vector[A]): NonEmptyVector[A] =
if (vector.nonEmpty) NonEmptyVector(vector)
if (vector.nonEmpty) new NonEmptyVector(vector)
else throw new IllegalArgumentException("Cannot create NonEmptyVector from empty vector")
}
46 changes: 43 additions & 3 deletions tests/src/test/scala/cats/tests/NonEmptyVectorTests.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package cats
package tests

import catalysts.Platform

import cats.kernel.laws.{GroupLaws, OrderLaws}

import cats.data.NonEmptyVector
import cats.laws.discipline.{ComonadTests, SemigroupKTests, FoldableTests, SerializableTests, TraverseTests, ReducibleTests, MonadRecTests}
import cats.laws.discipline.arbitrary._

import scala.util.Properties

class NonEmptyVectorTests extends CatsSuite {
// Lots of collections here.. telling ScalaCheck to calm down a bit
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
Expand Down Expand Up @@ -55,7 +59,7 @@ class NonEmptyVectorTests extends CatsSuite {
nonEmptyVector.size should === (nonEmptyVector.toList.size.toLong)
}
}


test("Show is not empty and is formatted as expected") {
forAll { (nonEmptyVector: NonEmptyVector[Int]) =>
Expand Down Expand Up @@ -169,9 +173,9 @@ class NonEmptyVectorTests extends CatsSuite {
}
}

test("++ NonEmptyVector is consistent with concat") {
test("++ Vector is consistent with concatNEV") {
forAll { (nonEmptyVector: NonEmptyVector[Int], other: NonEmptyVector[Int]) =>
nonEmptyVector ++ other should === (nonEmptyVector.concat(other))
nonEmptyVector ++ other.toVector should === (nonEmptyVector.concatNEV(other))
}
}

Expand Down Expand Up @@ -218,4 +222,40 @@ class NonEmptyVectorTests extends CatsSuite {
}
}
}

test("NonEmptyVector#hashCode consistent with Vector#hashCode") {
forAll { (nonEmptyVector: NonEmptyVector[Int]) =>
nonEmptyVector.hashCode should === (nonEmptyVector.toVector.hashCode)
}
}

test("NonEmptyVector#equals consistent with Vector#equals") {
forAll { (lhs: NonEmptyVector[Int], rhs: NonEmptyVector[Int]) =>
lhs.equals(rhs) should === (lhs.toVector.equals(rhs.toVector))
}
}

test("NonEmptyVector#toString produces correct output") {
forAll { (nonEmptyVector: NonEmptyVector[Int]) =>
nonEmptyVector.toString should === (s"NonEmpty${nonEmptyVector.toVector.toString}")
}
NonEmptyVector(1, Vector.empty).toString should === ("NonEmptyVector(1)")
NonEmptyVector(1, Vector.empty).toVector.toString should === ("Vector(1)")
}

test("Cannot create a new NonEmptyVector from constructor") {
if(Platform.isJvm) {
if (!Properties.versionNumberString.startsWith("2.10")) {
// A bug in scala 2.10 allows private constructors to be accessed.
// We should still ensure that on scala 2.11 and up we cannot construct the
// object directly. see: https://issues.scala-lang.org/browse/SI-6601
"val bad: NonEmptyVector[Int] = new NonEmptyVector(Vector(1))" shouldNot compile
}
}
}

test("Cannot create a new NonEmptyVector from apply with an empty vector") {
Copy link
Contributor

Choose a reason for hiding this comment

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

test description a bit misleading, can remove the word "empty"
"Cannot create a new NonEmptyVector from apply with a vector".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point on the wording. Will rename when I get to a laptop I can work on it from. Hopefully this evening.

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 the perfect use-case for living on the wild side and using the in-browser GitHub editor :D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, that's true - :-). I didn't get a chance to get back to this unfortunately. I'll get to it, and the other comments, by the end of the week all going well.

"val bad: NonEmptyVector[Int] = NonEmptyVector(Vector(1))" shouldNot compile
}

}