diff --git a/core/src/main/scala/cats/collections/BList.scala b/core/src/main/scala/cats/collections/BList.scala new file mode 100644 index 00000000..ab135569 --- /dev/null +++ b/core/src/main/scala/cats/collections/BList.scala @@ -0,0 +1,383 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.collections + +import scala.annotation.tailrec +import scala.annotation.unchecked.uncheckedVariance +import scala.util.hashing.MurmurHash3 + +sealed abstract class BList[+A] { + def uncons: Option[(A, BList[A])] + def prepend[B >: A](a: B): BList.NonEmpty[B] + def headOption: Option[A] + def tailOption: Option[BList[A]] + def get(idx: Long): Option[A] + def getUnsafe(idx: Long): A + def lastOption: Option[A] + def size: Long + def map[B](fn: A => B): BList[B] + def foldLeft[B](init: B)(fn: (B, A) => B): B + def drop(n: Long): BList[A] + def concat[B >: A](l2: BList[B]): BList[B] + def toList: List[A] + def isEmpty: Boolean + def iterator: Iterator[A] + + final def ::[B >: A](a: B): BList[B] = prepend(a) + final def ++[B >: A](l2: BList[B]): BList[B] = concat(l2) + + // for development and testing + private[collections] def toStringInBlocks: String + + override def toString: String = { + val strb = new java.lang.StringBuilder + strb.append("BList(") + @tailrec + def loop(first: Boolean, l: BList[A]): Unit = + l.uncons match { + case None => () + case Some((h, t)) => + if (!first) strb.append(", "): Unit + strb.append(h.toString) + loop(false, t) + } + + loop(true, this) + strb.append(")") + strb.toString + } + + def equals(other: Any): Boolean + def hashCode(): Int +} + +object BList { + final private[collections] val BlockSize = 4 // test with different values + + case object Empty extends BList[Nothing] { + def uncons = None + def prepend[B >: Nothing](a: B): BList.NonEmpty[B] = { + val ary = new Array[Any](BlockSize) + val offset = BlockSize - 1 + ary(offset) = a + Impl(offset, ary, Empty) + } + def headOption: None.type = None + def tailOption: None.type = None + def get(idx: Long): None.type = None + def getUnsafe(idx: Long): Nothing = throw new IndexOutOfBoundsException + def lastOption: None.type = None + def size: Long = 0 + def map[B](fn: Nothing => B): BList[B] = Empty + def foldLeft[B](acc: B)(fn: (B, Nothing) => B): B = acc + def drop(n: Long): BList[Nothing] = Empty + def concat[B](l2: BList[B]): BList[B] = l2 + override def toList: List[Nothing] = Nil + def isEmpty: Boolean = true + def iterator: Iterator[Nothing] = Iterator.empty + private[collections] def toStringInBlocks: String = "Empty" + + override def equals(other: Any): Boolean = other match { + case _: Empty.type => true + case _ => false + } + + override def hashCode(): Int = Empty.iterator.## + + } + sealed abstract class NonEmpty[+A] extends BList[A] { + def head: A + def tail: BList[A] + def map[B](fn: A => B): BList.NonEmpty[B] + def concat[B >: A](l2: BList[B]): BList.NonEmpty[B] + def uncons: Some[(A, BList[A])] + def headOption: Some[A] + def tailOption: Some[BList[A]] + } + + object NonEmpty { + def apply[A](h: A, t: BList[A]): NonEmpty[A] = + t.prepend(h) + def unapply[A](l: NonEmpty[A]): Some[(A, BList[A])] = + l.uncons + } + + private object Impl { + def apply[A](offset: Int, block: Array[A], tailBList: BList[A]): Impl[A] = + new Impl(offset, block, tailBList) + def apply[A](offset: Int, block: Array[Any], tailBList: BList[A]): Impl[A] = + new Impl(offset, block.asInstanceOf[Array[A]], tailBList) + def unapply[A](l: Impl[A]): Some[(Int, Array[A], BList[A])] = + Some((l.offset, l.block, l.tailBList)) + } + + private class Impl[+A](val offset: Int, val block: Array[A @uncheckedVariance], val tailBList: BList[A]) + extends NonEmpty[A] { + + override def equals(other: Any): Boolean = + other match { + case o: Impl[_] => + this.iterator.sameElements(o.iterator.asInstanceOf[Iterator[A]]) + case _ => false + } + + override def hashCode: Int = + MurmurHash3.orderedHash(this.iterator) + + def uncons: Some[(A, BList[A])] = { + Some((block(offset), this.tail)) + } + def prepend[B >: A](a: B): BList.NonEmpty[B] = { + if (offset > 0) { + val ary = block.clone().asInstanceOf[Array[B]] + val nextOffset = offset - 1 + ary(nextOffset) = a + Impl(nextOffset, ary, tailBList) + } else { + val ary = new Array[Any](BlockSize) + val offset = BlockSize - 1 + ary(offset) = a + Impl(offset, ary, this) + } + } + def head: A = { + block(offset) + } + def tail: BList[A] = { + if (offset < BlockSize - 1) { + val ary = new Array[Any](BlockSize) + System.arraycopy(block, offset + 1, ary, offset + 1, BlockSize - (offset + 1)) + Impl(offset + 1, ary, tailBList) + } else { + tailBList + } + } + def headOption: Some[A] = { + Some(block(offset)) + } + def tailOption: Some[BList[A]] = { + Some(tail) + } + def get(idx: Long): Option[A] = { + if (idx < 0) { None } + else { + @tailrec + def go(idx: Long, l: BList[A]): Option[A] = { + l match { + case Empty => None + case impl: Impl[A] @unchecked => + if (idx < BlockSize - impl.offset) { + Some(impl.block(impl.offset + idx.toInt)) + } else { + go(idx - (BlockSize - impl.offset), impl.tailBList) + } + } + } + go(idx, this) + } + } + def getUnsafe(idx: Long): A = { + if (idx < 0) + throw new IndexOutOfBoundsException + + @tailrec + def go(idx: Long, l: BList[A]): A = { + l match { + case Empty => throw new IndexOutOfBoundsException + case impl: Impl[A] @unchecked => + if (idx < BlockSize - impl.offset) { + impl.block(impl.offset + idx.toInt) + } else { + go(idx - (BlockSize - impl.offset), impl.tailBList) + } + } + } + go(idx, this) + } + def lastOption: Some[A] = { + @tailrec + def go(self: Impl[A]): Some[A] = { + self.tailBList match { + case Empty => Some(self.block(BlockSize - 1)) + case next: Impl[A] @unchecked => go(next) + } + } + go(this) + } + def size: Long = { + @tailrec + def loop(l: BList[A], acc: Long): Long = { + l match { + case Empty => acc + case impl: Impl[A] @unchecked => loop(impl.tailBList, acc + (BlockSize - impl.offset)) + } + } + loop(this, 0L) + } + def map[B](fn: A => B): BList.NonEmpty[B] = { + val ary = new Array[Any](BlockSize) + var i = offset + while (i < ary.length) { + ary(i) = fn(block(i)) + i += 1 + } + Impl(offset, ary, tailBList.map(fn)) + } + def foldLeft[B](acc: B)(fn: (B, A) => B): B = { + @tailrec + def loop(acc: B, l: BList[A]): B = + l match { + case Empty => acc + case impl: Impl[A] @unchecked => + var newacc = acc + var i = impl.offset + while (i < impl.block.length) { + newacc = fn(newacc, impl.block(i)) + i += 1 + } + loop(newacc, impl.tailBList) + } + loop(acc, this) + } + def drop(n: Long): BList[A] = { + @tailrec + def go(n: Long, l: BList[A]): BList[A] = { + if (n <= 0) { + l + } else { + l match { + case Empty => + Empty + case impl: Impl[A] @unchecked => + if (n >= BlockSize - impl.offset) { + go(n - (BlockSize - impl.offset), impl.tailBList) + } else { + val ary = new Array[Any](BlockSize) + val newOffset: Int = impl.offset + n.asInstanceOf[Int] // type conversion safe because 0: A](l2: BList[B]): BList.NonEmpty[B] = { + // not tail rec + def go(self: Impl[A]): BList.NonEmpty[B] = { + self.tailBList match { + case Empty => Impl(self.offset, self.block.asInstanceOf[Array[B]], l2) + case next: Impl[A] @unchecked => Impl(self.offset, self.block.asInstanceOf[Array[B]], go(next)) + } + } + + // for now, the only optimization is checking if either are empty + (this, l2) match { + case (_, Empty) => this + case (_, _) => go(this) + } + } + + override def toList: List[A] = { + val builder = List.newBuilder[A] + @tailrec + def loop(l: BList[A]): List[A] = + l match { + case Empty => builder.result() + case impl: Impl[A] @unchecked => + // append valid things in the block to acc + for (i <- impl.offset until BlockSize) { + builder += impl.block(i) + } + loop(impl.tailBList) + } + loop(this) + } + + def isEmpty: Boolean = false + def iterator: Iterator[A] = new BListIterator(this) + + private[collections] def toStringInBlocks: String = { + val strb = new java.lang.StringBuilder + strb.append("BList(") + @tailrec + def loop(first: Boolean, l: BList[A]): Unit = { + if (!first) strb.append(", "): Unit + l match { + case Empty => strb.append("Empty"): Unit + case impl: Impl[A] @unchecked => + strb.append("Block(") + strb.append(impl.block(impl.offset).toString) + for (i <- impl.offset + 1 until BlockSize) { + strb.append(", ") + strb.append(impl.block(i).toString) + } + strb.append(")") + loop(false, impl.tailBList) + } + } + loop(true, this) + strb.append(")") + strb.toString + } + final private class BListIterator(var curNode: Impl[A @uncheckedVariance]) extends Iterator[A] { + var curOffset: Int = curNode.offset + + def hasNext: Boolean = curOffset < BlockSize + def next(): A = + if (curOffset >= BlockSize) { + throw new NoSuchElementException + } else { + val next_elmt = curNode.block(curOffset) + curOffset += 1 + if (curOffset >= BlockSize) { + // try to advance to next block + curNode.tailBList match { + case impl: Impl[A] => + curOffset = impl.offset + curNode = impl + case Empty => // nothing, keep offset at blocksize + } + } + next_elmt + } + } + } + + def fromList[A](l: List[A]): BList[A] = + fromListReverse(l.reverse) + + def fromListReverse[A](l: List[A]): BList[A] = { + @tailrec + def go(l: List[A], acc: BList[A]): BList[A] = { + l match { + case Nil => acc + case h :: t => go(t, acc.prepend(h)) + } + } + go(l, empty) + } + + def empty[A]: BList[A] = Empty + +} diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala new file mode 100644 index 00000000..e8d82a5b --- /dev/null +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/ArbitraryBList.scala @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.collections.arbitrary + +import cats.collections.BList +import org.scalacheck.Arbitrary +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen + +trait ArbitraryBList { + def bListGen[A](implicit arb: Arbitrary[A]): Gen[BList[A]] = { + Gen.sized { + case 0 => Gen.const(BList.empty) + case n => + Gen.oneOf( + // empty + Gen.const(BList.empty), + // prepend + for { + hs <- Gen.resize(n / 2, bListGen[A]) + a <- arbitrary[A] + } yield hs.prepend(a), + // concat + for { + xs <- Gen.resize(n / 4, bListGen[A]) + ys <- Gen.resize(n / 4, bListGen[A]) + } yield xs.concat(ys), + // map + Gen.resize(n / 2, bListGen[A]).map { l => + l.map(identity) + } + ) + } + } + + implicit def arbitraryBList[A](implicit arb: Arbitrary[A]): Arbitrary[BList[A]] = + Arbitrary(bListGen(arb)) +} + +object ArbitraryBList extends ArbitraryBList diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/all.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/all.scala index f539e0c4..79b699b6 100644 --- a/scalacheck/src/main/scala/cats/collections/arbitrary/all.scala +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/all.scala @@ -27,4 +27,6 @@ trait AllArbitrary with ArbitraryMap with ArbitraryHashMap with ArbitraryPredicate + with ArbitraryTreeList + with ArbitraryBList with CogenInstances diff --git a/scalacheck/src/main/scala/cats/collections/arbitrary/package.scala b/scalacheck/src/main/scala/cats/collections/arbitrary/package.scala index df327980..9017b6b2 100644 --- a/scalacheck/src/main/scala/cats/collections/arbitrary/package.scala +++ b/scalacheck/src/main/scala/cats/collections/arbitrary/package.scala @@ -29,5 +29,7 @@ package object arbitrary { object map extends ArbitraryMap object hashmap extends ArbitraryHashMap object predicate extends ArbitraryPredicate + object blist extends ArbitraryBList object cogen extends CogenInstances + } diff --git a/tests/src/test/scala/cats/collections/BListSuite.scala b/tests/src/test/scala/cats/collections/BListSuite.scala new file mode 100644 index 00000000..3affdb4f --- /dev/null +++ b/tests/src/test/scala/cats/collections/BListSuite.scala @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.collections + +//import cats.syntax.all._ +import cats.collections.arbitrary.blist._ +//import cats.laws.discipline._ +import cats.Eq +import munit.DisciplineSuite +import org.scalacheck.Prop._ +import org.scalacheck.Test +//import org.scalacheck.{Arbitrary, Cogen, Gen, Test} +import scala.math.pow + +class BListSuite extends DisciplineSuite { + + override def scalaCheckTestParameters: Test.Parameters = + DefaultScalaCheckPropertyCheckConfig.default + + test("concatenating to form same list") { + val l1 = BList.empty[Int].prepend(5).prepend(4).prepend(3).prepend(2).prepend(1) + val m1 = BList.empty[Int].prepend(0).prepend(-1).prepend(-2) + + val l2 = BList.empty[Int].prepend(5).prepend(4).prepend(3).prepend(2) + val m2 = BList.empty[Int].prepend(1).prepend(0).prepend(-1).prepend(-2) + + val n1 = m1.concat(l1) + val n2 = m2.concat(l2) + assertEquals(n1, n2) + } + + test("uncons when tail is a new node") { + val l = BList.empty[Int].prepend(4).prepend(3).prepend(2).prepend(1) + val m = BList.empty[Int].prepend(0) + + val n = m.concat(l) + + n.uncons match { + // case None => fail("should not be empty") + case Some((h, t)) => + assertEquals(h, m.head) + assertEquals(t, l) + } + } + + test("prepend when block is full") { + var l = BList.empty[Int] + for (i <- (0 until BList.BlockSize).reverse) { + l = l.prepend(i) + } + val m = l.prepend(-1) + assertEquals(m.toList, (-1 until BList.BlockSize).toList) + } + + test("headOption on empty list") { + val l = BList.empty[Int] + assertEquals(l.headOption, None) + } + + test("headOption on single element") { + val l = BList.empty[Int].prepend(1) + assertEquals(l.headOption, Some(1)) + } + + test("tailOption on single element") { + val l = BList.empty[Int].prepend(1) + assertEquals(l.tailOption, Some(BList.empty[Int])) + } + + test("tail on single element") { + val l = BList.NonEmpty[Int](1, BList.empty[Int]) + assertEquals(l.tail, BList.empty[Int]) + } + + test("get/getUnsafe on a Blist with incomplete blocks") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until BList.BlockSize).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + assertEquals(m.getUnsafe((BList.BlockSize + (BList.BlockSize - 1)).toLong), 2) + assertEquals(m.get((BList.BlockSize + (BList.BlockSize - 1)).toLong), Some(2)) + } + test("get/getUnsafe index too small") { + val l = BList.empty[Int].prepend(5).prepend(4).prepend(3).prepend(2).prepend(1) + assertEquals(l.get(-1), None) + intercept[IndexOutOfBoundsException](l.getUnsafe(-1)) + } + test("get/getUnsafe index 0 on empty list") { + val l = BList.empty[Int] + assertEquals(l.get(0), None) + intercept[IndexOutOfBoundsException](l.getUnsafe(0)) + } + test("get/getUnsafe index too big") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until BList.BlockSize).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + // size is triangle numbers in BList.BlockSize. size should be n(n+1)/2 where n is BList.BlockSize + // so BList.BlockSize ** 2 + 1 will always be out of bounds + val idx: Long = pow(BList.BlockSize.toDouble, 2.0).toLong + 1L + assertEquals(m.get(idx), None) + intercept[IndexOutOfBoundsException](m.getUnsafe(idx)) + } + test("lastOption with incomplete blocks") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until BList.BlockSize).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + + // last should be blocksize-1 + assertEquals(m.lastOption, Some(BList.BlockSize - 1)) + } + test("lastOption on empty list") { + val l = BList.empty[Int] + assertEquals(l.lastOption, None) + } + test("size on a triangle number construction") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until BList.BlockSize).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + // size is triangle numbers in BList.BlockSize. formula: n(n+1)/2 + val expected: Long = (BList.BlockSize * (BList.BlockSize + 1)).toLong / 2L + assertEquals(m.size, expected) + } + test("size on empty") { + val l = BList.empty[Int] + assertEquals(l.size, 0L) + } + + test("prepend and uncons are inverses") { + val l = BList.empty[Int].prepend(4).prepend(3).prepend(2).prepend(1) + val m = l.prepend(0) + + m.uncons match { + // case None => fail("should not be empty") + case Some((h, t)) => + assertEquals(h, 0) + assertEquals(t, l) + } + } + + test("simple map") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 10).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + val mapped = m.map(_ + 10) + assertEquals(mapped.getUnsafe(20L), m.getUnsafe(20L) + 10) + assertEquals(mapped.getUnsafe(25L), m.getUnsafe(25L) + 10) + } + test("map to a different type") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 10).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + + val mapped = m.map(_ >= 3) + assertEquals(mapped.getUnsafe(20L), m.getUnsafe(20L) >= 3) + assertEquals(mapped.getUnsafe(25L), m.getUnsafe(25L) >= 3) + } + + test("map on empty") { + val l = BList.empty[Int] + assertEquals(l.map(_ + 100), l) + } + + test("foldleft with subtraction") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 10).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + assertEquals(m.foldLeft(m.foldLeft(0)((acc: Int, x: Int) => acc + x))((acc: Int, x: Int) => acc - x), 0) + } + + test("drop on fragmented list") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 10).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + assertEquals(m.drop(10).getUnsafe(15), m.getUnsafe(25)) + assertEquals(m.drop(10).getUnsafe(20), m.getUnsafe(30)) + assertEquals(m.drop(20).getUnsafe(5), m.getUnsafe(25)) + } + + test("drop everything") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 4).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + assertEquals(m.drop(10), BList.empty[Int]) + assertEquals(m.drop(20), BList.empty[Int]) + assertEquals(m.drop(1000), BList.empty[Int]) + } + test("drop nothing") { + var l = BList.empty[Int] + var m = BList.empty[Int] + for (i <- (0 until 4).reverse) { + l = l.prepend(i) + m = l.concat(m) + } + + assertEquals(m.drop(0), m) + assertEquals(m.drop(-10), m) + assertEquals(m.drop(-200), m) + } + + test("blocks don't matter for equality") { + // todo + val l1 = BList.empty[Int].prepend(3).prepend(2).prepend(1) + val m1 = BList.empty[Int].prepend(0).prepend(-1).prepend(-2) + + val l2 = BList.empty[Int].prepend(3).prepend(2) + val m2 = BList.empty[Int].prepend(1).prepend(0).prepend(-1).prepend(-2) + + val n1 = m1.concat(l1) + val n2 = m2.concat(l2) + assertEquals(n1, n2) + + } + + private def testHomomorphism[A, B: Eq](as: BList[A])(fn: BList[A] => B, gn: List[A] => B): Unit = { + val la = as.toList + assert(Eq[B].eqv(fn(as), gn(la))) + } + + property("size works")(forAll { (xs: BList[Int]) => + testHomomorphism(xs)({ _.size }, { _.size.toLong }) + }) + property("last is the same as get(size-1)")(forAll { (xs: BList[Int]) => + assertEquals(xs.lastOption, xs.get(xs.size - 1L)) + }) + + property("concat works")(forAll { (xs: BList[Int], ys: BList[Int]) => + testHomomorphism(xs)({ l => l.concat(ys).toList }, { _ ++ ys.toList }) + }) + + property("get and getUnsafe are consistent")(forAll { (xs: BList[Int]) => + assertEquals(xs.get(-1L), None) + assertEquals(xs.get(xs.size), None) + intercept[IndexOutOfBoundsException](xs.getUnsafe(-1L)) + intercept[IndexOutOfBoundsException](xs.getUnsafe(xs.size)) + + val list = xs.toList + (0L until xs.size).foreach { idx => + assertEquals(xs.get(idx), Some(list(idx.toInt))) + assertEquals(xs.getUnsafe(idx), list(idx.toInt)) + } + }) + + property("prepending head on to tail makes the same list")(forAll { (xs: BList[Int]) => + xs.headOption match { + case Some(h) => + xs.tailOption match { + case Some(t) => assertEquals(t.prepend(h), xs) + case None => assertEquals(BList.empty.prepend(h), xs) + } + case None => assertEquals(xs, BList.empty) + } + }) + + property("headoption, head consistent and tailoption, tail consistent")(forAll { (xs: BList[Int]) => + xs.headOption match { + case Some(h) => assertEquals(h, xs.asInstanceOf[BList.NonEmpty[Int]].head) + case None => // head wont work because xs is the empty list + } + xs.tailOption match { + case Some(t) => assertEquals(t, xs.asInstanceOf[BList.NonEmpty[Int]].tail) + case None => // tail wont work because xs is the empty list + } + }) + + property("headOption works")(forAll { (xs: BList[Int]) => + testHomomorphism(xs)({ _.headOption }, { _.headOption }) + }) + + property("lastOption works")(forAll { (xs: BList[Int]) => + testHomomorphism(xs)({ _.lastOption }, { _.lastOption }) + }) + + property("toList inverse of fromList")(forAll { (xs: BList[Int]) => + assertEquals(xs, BList.fromList(xs.toList)) + }) + + property("pattern matching works")(forAll { (xs: BList[Int]) => + xs match { + case BList.NonEmpty(h, t) => + assertEquals(xs.headOption, Option(h)) + assertEquals(xs.asInstanceOf[BList.NonEmpty[Int]].tail, t) + case BList.Empty => + assertEquals(xs, BList.Empty) + assertEquals(xs.uncons, None) + assertEquals(xs.headOption, None) + assertEquals(xs.tailOption, None) + assertEquals(xs.lastOption, None) + } + }) + + property("apply/unapply are inverses for NonEmpty")(forAll { (head: Int, tail: BList[Int]) => + BList.NonEmpty(head, tail) match { + case BList.NonEmpty(h, t) => + assertEquals(h, head) + assertEquals(t, tail) + } + }) + + property("fromListReverse == .reverse fromList")(forAll { (xs: List[Int]) => + assertEquals(BList.fromListReverse(xs), BList.fromList(xs.reverse)) + }) + + property("concat is associative")(forAll { (xs: BList[Int], ys: BList[Int], zs: BList[Int]) => + val l1 = xs.concat(ys).concat(zs) + val l2 = xs.concat(ys.concat(zs)) + assertEquals(l1, l2) + }) + property("== implies same toString for int lists")(forAll { (xs: BList[Int], ys: BList[Int]) => + if (xs.toString == ys.toString) { + assertEquals(xs, ys) + } + }) +}