Foundations for BList#825
Conversation
Co-authored-by: Sergey Torgashov <satorg@users.noreply.github.com>
Co-authored-by: Sergey Torgashov <satorg@users.noreply.github.com>
Co-authored-by: Sergey Torgashov <satorg@users.noreply.github.com>
| } | ||
|
|
||
| // (maybe impl will be covariant or not) | ||
| private case class Impl[A](offset: Int, block: Array[A], tail: BList[A]) extends NonEmpty[A] { |
There was a problem hiding this comment.
I think you can make this covariant if block: Array[_ <: A] that said, since we don't have a classtag for the type A and we may not want to require it, we may want to instead use Array[Any] and cast when we get out, which would require boxing primitives, but List also boxes primitives.
There was a problem hiding this comment.
It used to be Array[Any], but it ended up requiring so many uses of .asInstanceOf, and I didn't like how cluttered and confusing it made everything. Is there a good reason why we would want Impl to be covariant? If its worth it then I will try to find ways to contain the typecasts in a maintainable way.
| } | ||
| def getUnsafe(idx: Long): A = { | ||
| if (idx < 0) | ||
| throw new IndexOutOfBoundsException |
There was a problem hiding this comment.
we are throwing a different kind of exception for < 0 vs too large. Does List also do that? I would comment and follow List behavior here so it is fully compliant.
There was a problem hiding this comment.
Just checked and it seems to throw the same index out of bounds exception for index too small or too large!
| } | ||
|
|
||
| def empty[A]: BList[A] = Empty | ||
| def unapply[A](l: BList[A]): Option[(A, BList[A])] = l.uncons |
There was a problem hiding this comment.
I think this should be on the object NonEmpty inside BList and we should also have an apply method that builds a NonEmpty by prepending onto a BList. So BList.NonEmpty(h, t) can be used in construction and deconstruction.
There was a problem hiding this comment.
Yes, makes sense. I think something like this should work:
object BList {
object NonEmpty {
def apply[A](h: A, t: BList[A]): NonEmpty[B] = ...
def unapply[A](l: BList.NonEmpty[A]): Some[(A, BList[A])] = l.uncons
}
}There was a problem hiding this comment.
What kinds of things do in the object NonEmpty vs the object Impl?
There was a problem hiding this comment.
Also, should the return type of unapply still keep the Option?
There was a problem hiding this comment.
What kinds of things do in the object NonEmpty vs the object Impl?
NonEmpty is a part of the public API, whereas Impl hosts implementation details which we don't want to reveal.
Also, should the return type of unapply still keep the Option?
That is a part of the trick: if we returned Option[...], then Scala compiler might struggle on exhausiveness checks in some cases – because it doesn't know whether it resolves to Some or None at runtime.
However, when we return Some, then we reassure the compiler that the result does always exist:
val fooBList: BList[Int] = ...
fooBList match {
case BList.NonEmpty(head, tail) => // only matches if our `fooBList` is a `NonEmpty` instance.
case BList.Empty =>
// or `case _ : BList.Empty.type =>` or just `case _ =>` – the `match` should be exhaustive in any case.
}Minimal-ish example: https://scastie.scala-lang.org/smsfk232TM6C01PJonKThQ
| // TODO can put methods in here that are only safe for nonempty (ex. head, reduce) | ||
| def head: A | ||
| def tail: BList[A] | ||
| // def reduce |
There was a problem hiding this comment.
I'd suggest establishing NonEmpty API here sooner than later. We don't need to repeat everything from BList, but only the methods that change their signatures for NonEmpty, e.g.:
def map[B](fn: A => B): BList.NonEmpty[B]
It would give us a clearer picture of all the requirements and constraints that we'll need for the implementation.
| 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 combineK[B >: A](l2: BList[B]): BList[B] |
There was a problem hiding this comment.
I'm not sure about this name – combineK. It is very generic and moreover K suffix suggests higher kinded types. The name comes from SemigroupK, which can be used for many different types, not just collections.
Sequence-like structures usually stick to names like concat or ++. Alternatively, since BList tries to resemble the regular List, we can consider ::: as well.
There was a problem hiding this comment.
I can rename it. I think I chose this one because Arman called it that when he gave me an initial list of methods to implement.
Co-authored-by: Arman Bilge <arman@typelevel.org>
| } | ||
| def tail: BList[A] = { | ||
| if (offset < BlockSize - 1) { | ||
| Impl(offset + 1, block, tailBList) |
There was a problem hiding this comment.
Are we sure we want to share arrays between two BList instances? It can results in discarded objects blocked from GC:
val largeThings = BList(largeThing1, largeThing2, largeThing3)
val res = largeThings.tailIn this case, res won't have largeThing1 becasue it was discarded by .tail, but it will be still referenced by the same array that was shared between largeThings and res. Therefore, even if largeThings collection gets GC-ed, largeThing1 will be kept in memory until res is GC-ed as well.
There was a problem hiding this comment.
the fix will be in the next commit. I also made the same fix for uncons.
building out the framework for the new datatype