Skip to content
This repository was archived by the owner on Dec 22, 2021. It is now read-only.

Abstractions for constrained collection types #45

Merged
merged 8 commits into from
Mar 23, 2017
Merged
8 changes: 5 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ organization in ThisBuild := "ch.epfl.scala"

version in ThisBuild := "0.2.0-SNAPSHOT"

scalaVersion in ThisBuild := "2.12.1"
resolvers in ThisBuild += "scala-pr" at "https://scala-ci.typesafe.com/artifactory/scala-pr-validation-snapshots"
scalaVersion in ThisBuild := "2.12.2-ebe1180-SNAPSHOT" // from https://github.com/scala/scala/pull/5742
scalaBinaryVersion in ThisBuild := "2.12"

scalacOptions in ThisBuild ++=
Seq("-deprecation", "-unchecked", "-Yno-imports", "-language:higherKinds")
Expand All @@ -26,7 +28,7 @@ val collections =
<developer><id>ichoran</id><name>Rex Kerr</name></developer>
<developer><id>odersky</id><name>Martin Odersky</name></developer>
<developer><id>julienrf</id><name>Julien Richard-Foy</name></developer>
<developer><id>szeiger</id><name>Stefan Szeiger</name></developer>
<developer><id>szeiger</id><name>Stefan Zeiger</name></developer>
</developers>,
homepage := Some(url("https://github.com/scala/collection-strawman")),
licenses := Seq("BSD 3-clause" -> url("http://opensource.org/licenses/BSD-3-Clause")),
Expand Down Expand Up @@ -80,4 +82,4 @@ val memoryBenchmark =
}.evaluated
)

lazy val charts = inputKey[File]("Runs the benchmarks and produce charts")
lazy val charts = inputKey[File]("Runs the benchmarks and produce charts")
13 changes: 0 additions & 13 deletions src/main/scala/strawman/collection/BitSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,3 @@ trait BitSetMonoTransforms[+C <: BitSet]
def map(f: Int => Int): C

}

/** Factory methods for unconstrained collections of kind `*` */
trait BitSetFactories[+C] {

def newBuilder: Builder[Int, C]

final def empty: C = newBuilder.result

final def apply(elems: Int*): C = newBuilder.++=(elems.toStrawman).result

implicit val canBuild: () => Builder[Int, C] = () => newBuilder

}
68 changes: 68 additions & 0 deletions src/main/scala/strawman/collection/BuildFrom.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package strawman.collection

import scala.Any
import scala.annotation.implicitNotFound
import mutable.Builder

/** BuildFrom can be used by methods outside the collections framework to build collections of various types without
* having to define overloaded methods for unconstrained / constrained / map-like / etc. collections like we do in
* the collections framework.
*/
@implicitNotFound(msg = "Cannot construct a collection with elements of type ${Elem} based on a collection of type ${From}.")
trait BuildFrom[-From, -Elem] {
type To

/** Creates a new builder on request of a collection.
* @param from the collection requesting the builder to be created.
* @return a builder for collections of type `To` with element type `Elem`.
*/
def newBuilder(from: From): Builder[Elem, To]

def fromIterable(from: From)(it: Iterable[Elem]): To
}

object BuildFrom {
/** Build the source collection type from a PolyBuildable */
implicit def buildFromPolyBuildable[C[X] <: PolyBuildable[X, C], A, E]: BuildFrom[C[A], E] { type To = C[E] } = new BuildFrom[C[A], E] {
//TODO: Reuse a prototype instance
type To = C[E]
def newBuilder(from: C[A]): Builder[E, To] = from.newBuilder[E]
def fromIterable(from: C[A])(it: Iterable[E]): To = from.fromIterable(it)
}

/** Build the source collection type from a ConstrainedPolyBuildable */
implicit def buildFromConstrainedPolyBuildable[Ev[_], Impl[_], CC[_], A, E : Ev]: BuildFrom[ConstrainedPolyBuildable[A, CC, Ev], E] { type To = CC[E] } = new BuildFrom[ConstrainedPolyBuildable[A, CC, Ev], E] {
//TODO: Reuse a prototype instance
type To = CC[E]
def newBuilder(from: ConstrainedPolyBuildable[A, CC, Ev]): Builder[E, To] = from.newConstrainedBuilder[E]
def fromIterable(from: ConstrainedPolyBuildable[A, CC, Ev])(it: Iterable[E]): To = from.constrainedFromIterable(it)
}

/** Convert an IterableFactory to a BuildFrom */
implicit def buildIterableFactory[C[_], E](fact: IterableFactory[C]): BuildFrom[Any, E] { type To = C[E] } = new BuildFrom[Any, E] {
type To = C[E]
def newBuilder(from: Any): Builder[E, To] = fact.newBuilder
def fromIterable(from: Any)(it: Iterable[E]): To = fact.fromIterable(it)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This sounds like smell: we should not have to introduce this Any parameter if we don’t need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do need for the "proper" cases (implicit values for poly builders). When you do a manual breakout, the from value is ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alternatively, we could have a subclass like BuildFromAny[-Elem] extends BuildFrom[Any, Elem] with overloads that do not take a from value. This would be enough for the non-implicit use case in to. Is getting rid of the Any parameter here worth the extra complexity of a new class with two new methods?


/** Convert a ConstrainedIterableFactory to a BuildFrom */
implicit def buildConstrainedIterableFactory[CC[_], Ev[_], E : Ev](fact: ConstrainedIterableFactory[CC, Ev]): BuildFrom[Any, E] { type To = CC[E] } = new BuildFrom[Any, E] {
type To = CC[E]
def newBuilder(from: Any): Builder[E, To] = fact.constrainedNewBuilder
def fromIterable(from: Any)(it: Iterable[E]): To = fact.constrainedFromIterable(it)
}

/** Convert a MapFactory to a BuildFrom */
implicit def buildMapFactory[C[_, _], K, V](fact: MapFactory[C]): BuildFrom[Any, (K, V)] { type To = C[K, V] } = new BuildFrom[Any, (K, V)] {
type To = C[K, V]
def newBuilder(from: Any): Builder[(K, V), To] = fact.newBuilder
def fromIterable(from: Any)(it: Iterable[(K, V)]): To = fact.fromIterable(it)
}

/** Convert a ConstrainedMapFactory to a BuildFrom */
implicit def buildConstrainedMapFactory[CC[_, _], Ev[_], K : Ev, V](fact: ConstrainedMapFactory[CC, Ev]): BuildFrom[Any, (K, V)] { type To = CC[K, V] } = new BuildFrom[Any, (K, V)] {
type To = CC[K, V]
def newBuilder(from: Any): Builder[(K, V), To] = fact.constrainedNewBuilder
def fromIterable(from: Any)(it: Iterable[(K, V)]): To = fact.constrainedFromIterable(it)
}
}
90 changes: 83 additions & 7 deletions src/main/scala/strawman/collection/Iterable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import scala.reflect.ClassTag
import scala.{Any, Array, Boolean, Int, StringContext, Unit}
import java.lang.{String, UnsupportedOperationException}

import strawman.collection.mutable.{ArrayBuffer, StringBuilder}
import strawman.collection.mutable.{ArrayBuffer, Builder, StringBuilder}
import java.lang.String

/** Base trait for generic collections */
Expand Down Expand Up @@ -81,17 +81,22 @@ trait IterableOps[+A] extends Any {
/** A view representing the elements of this collection. */
def view: View[A] = View.fromIterator(iterator())

/** Given a collection factory `fi` for collections of type constructor `C`,
* convert this collection to one of type `C[A]`. Example uses:
/** Given a collection factory `fi`, convert this collection to the appropriate
* representation for the current element type `A`. Example uses:
*
* xs.to(List)
* xs.to(ArrayBuffer)
* xs.to(BitSet) // for xs: Iterable[Int]
*/
def to[C[X] <: Iterable[X]](fi: FromIterable[C]): C[A @uncheckedVariance] =
// variance seems sound because `to` could just as well have been added
// as a decorator. We should investigate this further to be sure.
def to[F <: TypeConstrainedFromIterable[A]](fi: F): fi.To[A @uncheckedVariance] =
fi.fromIterable(coll)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you need the F parameter here.

def to(fi: TypeConstrainedFromIterable[A]): fi.to[A]

should work, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll give it a try. (I'm used to writing type projections where this wouldn't work)


// Generic version of the method above that can build anything with BuildFrom. Note that `bf` is not implicit.
// We never want it to be inferred (because a collection could only rebuild itself that way) but we do rely on
// the implicit conversions from the various factory types to BuildFrom.
def to(bf: BuildFrom[Iterable[A], A]): bf.To =
bf.fromIterable(coll)(coll)

/** Convert collection to array. */
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure about overloading to. Why do we need both versions? Also, naming conventions vary a lot
TypeConstrainedFromIterable and BuildFrom seem to have nothing in common.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need both. The TypeConstrainedFromIterable version covers the most common case and doesn't require an implicit conversion from the factory type to a BuildFrom. It's only there for performance (but I haven't benchmarked it). The BuildFrom version alone is sufficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, actually BitSet doesn't currently work with the BuildFrom version. I'll try to fix it.

def toArray[B >: A: ClassTag]: Array[B] =
if (knownSize >= 0) copyToArray(new Array[B](knownSize), 0)
Expand Down Expand Up @@ -172,7 +177,7 @@ trait IterableMonoTransforms[+A, +Repr] extends Any {

/** Transforms over iterables that can return collections of different element types.
*/
trait IterablePolyTransforms[+A, +C[A]] extends Any {
trait IterablePolyTransforms[+A, +C[_]] extends Any {
protected def coll: Iterable[A]
def fromIterable[B](coll: Iterable[B]): C[B]

Expand All @@ -189,3 +194,74 @@ trait IterablePolyTransforms[+A, +C[A]] extends Any {
def zip[B](xs: IterableOnce[B]): C[(A @uncheckedVariance, B)] = fromIterable(View.Zip(coll, xs))
// sound bcs of VarianceNote
}

/** Transforms over iterables that can return collections of different element types for which an
* implicit evidence is required.
*/
Copy link
Contributor

@odersky odersky Mar 22, 2017

Choose a reason for hiding this comment

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

Should explain in comment with examples why we need both C and CC. It looked non-senscial at first to me, and I started to understand only when I looked at instantiations of the class. (of course if we found a way to merge C and CC that would be even better).

trait ConstrainedIterablePolyTransforms[+A, +C[_], +CC[X] <: C[X]] extends Any with IterablePolyTransforms[A, C] {
type Ev[_]

protected def coll: Iterable[A]
Copy link
Contributor

Choose a reason for hiding this comment

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

What’s the advantage of that compared to having a self type annotation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the same as in IterablePolyTransforms. It allows the use for pseudo-collection types like String and Array.

protected def constrainedFromIterable[B: Ev](it: Iterable[B]): CC[B]

/** Map */
def map[B : Ev](f: A => B): CC[B] = constrainedFromIterable(View.Map(coll, f))

/** Flatmap */
def flatMap[B : Ev](f: A => IterableOnce[B]): CC[B] = constrainedFromIterable(View.FlatMap(coll, f))

/** Concatenation */
def ++[B >: A : Ev](xs: IterableOnce[B]): CC[B] = constrainedFromIterable(View.Concat(coll, xs))

/** Zip. Interesting because it requires to align to source collections. */
def zip[B](xs: IterableOnce[B])(implicit ev: Ev[(A @uncheckedVariance, B)]): CC[(A @uncheckedVariance, B)] = constrainedFromIterable(View.Zip(coll, xs))
// sound bcs of VarianceNote

def collect[B: Ev](pf: scala.PartialFunction[A, B]): CC[B] = flatMap(a =>
if (pf.isDefinedAt(a)) View.Elems(pf(a))
else View.Empty
)

/** Widen this collection to the most specific unconstrained collection type. */
def unconstrained: C[A @uncheckedVariance]
}

/** Base trait for strict collections that can be built using a builder.
* @tparam A the element type of the collection
* @tparam Repr the type of the underlying collection
*/
trait MonoBuildable[+A, +Repr] extends Any with IterableMonoTransforms[A, Repr] {

/** Creates a new builder. */
protected[this] def newBuilderWithSameElemType: Builder[A, Repr]

/** Optimized, push-based version of `partition`. */
override def partition(p: A => Boolean): (Repr, Repr) = {
val l, r = newBuilderWithSameElemType
coll.iterator().foreach(x => (if (p(x)) l else r) += x)
(l.result, r.result)
}

// one might also override other transforms here to avoid generating
// iterators if it helps efficiency.
}

/** Base trait for strict collections that can be built for arbitrary element types using a builder.
* @tparam A the element type of the collection
* @tparam C the type constructor of the underlying collection
*/
trait PolyBuildable[+A, +C[_]] extends Any with FromIterable[C] {

/** Creates a new builder. */
def newBuilder[E]: Builder[E, C[E]]
}

/** Base trait for strict collections that can be built using a builder for element types with an implicit evidence.
* @tparam A the element type of the collection
* @tparam C the type constructor of the underlying collection
*/
trait ConstrainedPolyBuildable[+A, +CC[_], Ev[_]] extends Any with ConstrainedFromIterable[CC, Ev] {

/** Creates a new builder. */
def newConstrainedBuilder[E : Ev]: Builder[E, CC[E]]
}
34 changes: 28 additions & 6 deletions src/main/scala/strawman/collection/IterableFactories.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,46 @@ package collection

import strawman.collection.mutable.Builder

import scala.Int
import scala.{Any, Int, Nothing}
import scala.annotation.unchecked.uncheckedVariance

/** Base trait for instances that can construct a collection from an iterable */
trait FromIterable[+C[X] <: Iterable[X]] {
def fromIterable[B](it: Iterable[B]): C[B]
trait FromIterable[+C[_]] extends Any with TypeConstrainedFromIterable[Any] {
type To[X] = C[X] @uncheckedVariance
}

/** Base trait for companion objects of collections */
trait IterableFactories[+C[X] <: Iterable[X]] extends FromIterable[C] {
/** Base trait for instances that can construct a collection from an iterable for certain types
* (but without needing an evidence value). */
trait TypeConstrainedFromIterable[-B] extends Any {
Copy link
Contributor

@julienrf julienrf Mar 20, 2017

Choose a reason for hiding this comment

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

UpperBoundedFromIterable?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would say BoundedFromIterable. The upper is implied if left out.

type To[_]
def fromIterable[E <: B](it: Iterable[E]): To[E]
}

/** Base trait for instances that can construct a collection from an iterable by using an implicit evidence
* for the element type. */
trait ConstrainedFromIterable[+CC[_], Ev[_]] extends Any {
def constrainedFromIterable[E : Ev](it: Iterable[E]): CC[E]
}

/** Base trait for companion objects of unconstrained collection types */
trait IterableFactory[+C[_]] extends FromIterable[C] { self =>
def empty[A]: C[A] = fromIterable(View.Empty)

def apply[A](xs: A*): C[A] = fromIterable(View.Elems(xs: _*))

def fill[A](n: Int)(elem: => A): C[A] = fromIterable(View.Fill(n)(elem))

def newBuilder[A]: Builder[A, C[A]]
}

/** Base trait for companion objects of collections that require an implicit evidence */
trait ConstrainedIterableFactory[+CC[X], Ev[_]] extends ConstrainedFromIterable[CC, Ev] {

def empty[A : Ev]: CC[A] = constrainedFromIterable(View.Empty)

def apply[A : Ev](xs: A*): CC[A] = constrainedFromIterable(View.Elems(xs: _*))

implicit def canBuild[A]: () => Builder[A, C[A]] = () => newBuilder[A] // TODO Reuse the same instance
def fill[A : Ev](n: Int)(elem: => A): CC[A] = constrainedFromIterable(View.Fill(n)(elem))

def constrainedNewBuilder[A : Ev]: Builder[A, CC[A]]
Copy link
Contributor

Choose a reason for hiding this comment

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

I would name it newConstrainedBuilder instead of constrainedNewBuilder.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered that, too, but it sounds like a method that creates a ConstrainedBuilder, whereas constrainedNewBuilder is consistent with constrainedFromIterable.

}
24 changes: 19 additions & 5 deletions src/main/scala/strawman/collection/Map.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package strawman.collection

import strawman.collection.mutable.Builder

import scala.Option
import scala.{Option, Any, Nothing}
import scala.annotation.unchecked.uncheckedVariance
import scala.Predef.???

Expand Down Expand Up @@ -31,16 +31,30 @@ trait MapPolyTransforms[K, +V, +C[X, Y] <: Map[X, Y]] extends IterablePolyTransf
}

/** Factory methods for collections of kind `* −> * -> *` */
trait MapFactories[C[_, _]] {

trait MapFactory[+C[_, _]] { self =>
def newBuilder[K, V]: Builder[(K, V), C[K, V]]

def fromIterable[K, V](it: Iterable[(K, V)]): C[K, V] =
newBuilder[K, V].++=(it).result

def empty[K, V]: C[K, V] =
newBuilder[K, V].result

def apply[K, V](elems: (K, V)*): C[K, V] =
newBuilder[K, V].++=(elems.toStrawman).result
}

/** Factory methods for collections of kind `* −> * -> *` which require an implicit evidence value for the key type */
trait ConstrainedMapFactory[+C[_, _], Ev[_]] { self =>

implicit def canBuild[K, V]: () => Builder[(K, V), C[K, V]] = () => newBuilder[K, V]
def constrainedNewBuilder[K : Ev, V]: Builder[(K, V), C[K, V]]

}
def constrainedFromIterable[K : Ev, V](it: Iterable[(K, V)]): C[K, V] =
constrainedNewBuilder[K, V].++=(it).result

def empty[K : Ev, V]: C[K, V] =
constrainedNewBuilder[K, V].result

def apply[K : Ev, V](elems: (K, V)*): C[K, V] =
constrainedNewBuilder[K, V].++=(elems.toStrawman).result
}
26 changes: 0 additions & 26 deletions src/main/scala/strawman/collection/Sorted.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package strawman.collection

import strawman.collection.mutable.Builder

import scala.Ordering

/** Base trait for sorted collections */
Expand All @@ -12,28 +10,4 @@ trait SortedLike[A, +Repr] {
def ordering: Ordering[A]

def range(from: A, until: A): Repr

}

/** Polymorphic transformation methods on sorted collections */
trait SortedPolyTransforms[A, +C[X] <: Sorted[X]]
extends IterablePolyTransforms[A, Iterable] {

def map[B](f: A => B)(implicit ordering: Ordering[B]): C[B]

}

/**
* Factories for collections whose elements require an ordering
*/
trait OrderingGuidedFactories[C[_]] {

def newBuilder[A](implicit ordering: Ordering[A]): Builder[A, C[A]]

def empty[A : Ordering]: C[A] = newBuilder[A].result

def apply[A : Ordering](as: A*): C[A] = (newBuilder[A] ++= as.toStrawman).result

implicit def canBuild[A : Ordering]: () => Builder[A, C[A]] = () => newBuilder[A]

}
8 changes: 6 additions & 2 deletions src/main/scala/strawman/collection/SortedSet.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package strawman.collection

import scala.Ordering

/** Base type of sorted sets */
trait SortedSet[A]
extends Set[A]
Expand All @@ -8,6 +10,8 @@ trait SortedSet[A]

trait SortedSetLike[A, +C[X] <: SortedSet[X]]
extends SortedLike[A, C[A]]
with SortedPolyTransforms[A, C]
with ConstrainedIterablePolyTransforms[A, Set, SortedSet]
with SetLike[A, Set] // Inherited Set operations return a `Set`
with SetMonoTransforms[A, C[A]] // Override the return type of Set ops to return C[A]
with SetMonoTransforms[A, C[A]] { // Override the return type of Set ops to return C[A]
type Ev[X] = Ordering[X]
}
Loading