-
Notifications
You must be signed in to change notification settings - Fork 72
Abstractions for constrained collection types #45
Conversation
- Add an upper bound in `FromIterable` and `IterableFactory`. This allows us to express type-constrained iterables like `BitSet` without requiring an implicit evidence. - Drop the `C[X] <: Iterable[X]` constraint in `FromIterable`. Guaranteeing an `Iterable[X]` result is not necessary anywhere and it makes monomorphic collection types awkward, for example producing a `BitSet with Iterable[X]` for any `X <: Int`. What we really want is to be able to drop the precise element type in `C[X] = BitSet`. - Add `FromIterableBase` which contains the target type constructor as a type member. It is extended by `FromIterable` which pulls the type constructor out into a type parameter. This allows us to write polymorphic code for arbitrary `FromIterableBase` subtypes without having to infer the type constructor (which is not possible when it is not a real type constructor but a type lambda computation like `C[X] = BitSet` via `MonomorphicIterableFactory`. - Add `ConstrainedFromIterable` and `ConstrainedIterableFactory` to mirror `FromIterable` and `IterableFactory` but with an additional implicit evidence for the element type. An upper bound and a monomorphic version are not necessary: Constrained collection types are always polymorphic (otherwise they would already have the correct evidence value for the element type) and any type bound can be rolled into the evidence type. - Overload `to` for `FromIterable` and `ConstrainedFromIterable`. - Add `ConstrainedIterablePolyTransforms` which mirrors `IterablePolyTransforms` with an additional implicit evidence (so it is based on `ConstrainedFromIterable` instead of `FromIterable`). It provides an additional method `unconstrained` to perform a widening conversion to the most specific unconstrained collection type. - Replace the implicit `() => Builder` with a more generic implicit `MonomorphicIterableFactory`. It works via implicit lookup for all unconstrained, type-constrained and value-constrained unary type constructors. - Add `ConstrainedMapFactory` which mirrors `MapFactory` with an additional implicit evidence for the key type. I considered using a binary type constructor that allows the evidence to be based on both, key and value types but it would complicate the API with a projection type for the key-only constraints and I do not expect that we will need it for any collection. - Implement a builder-based `fromIterable` in `MapFactory` and `ConstrainedMapFactory` and add implicit `MonomorphicIterableFactory` implementations to both.
monomorphicIterableFactoryProto.asInstanceOf[MonomorphicIterableFactory[E, C[E]]] | ||
} | ||
|
||
trait MonomorphicIterableFactory[-B, +Repr] extends IterableFactory[B, ({ type L[X] = Repr })#L] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MonomorphicIterableFactory
makes sense from the POV of collection implementations. Calling it CanBuild
would better convey the meaning to users.
@@ -32,14 +42,11 @@ final class TreeSet[A]()(implicit val ordering: Ordering[A]) | |||
|
|||
// From SortedLike | |||
def range(from: A, until: A): TreeSet[A] = ??? | |||
|
|||
// From SortedPolyTransforms | |||
def map[B](f: (A) => B)(implicit ordering: Ordering[B]): TreeSet[B] = ??? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ConstrainedIterablePolyTransforms
extends IterablePolyTransforms
in order to get the linearization right for the purpose of overload resolution, so this override is no longer required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was not an override, just an implementation, FYI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to compile master without this line:
[info] Compiling 37 Scala sources to /Users/szeiger/code/collection-strawman/target/scala-2.12/classes...
[error] /Users/szeiger/code/collection-strawman/src/main/scala/strawman/collection/immutable/TreeSet.scala:10: class TreeSet needs to be abstract, since method map in trait SortedPolyTransforms of type [B](f: A => B)(implicit ordering: Ordering[B])strawman.collection.immutable.TreeSet[B] is not defined
[error] (Note that A => B does not match A => B: their type parameters differ)
[error] final class TreeSet[A]()(implicit val ordering: Ordering[A])
[error] ^
[error] one error found
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that’s what I meant by “this was an implementation”.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not though. The error message is misleading. I haven't tried to track this down further but there is no abstract map
method anywhere in the inheritance hierarchy of TreeSet
. It inherits two concrete implementations but without enforcing CIPS <: IPS
the types don't line up.
@@ -29,14 +30,21 @@ class TraverseTest { | |||
def traverseTest: Unit = { | |||
|
|||
val xs1 = immutable.List(Some(1), None, Some(2)) | |||
optionSequence(xs1)(immutable.List.canBuild[Int]) // TODO Remove explicit CanBuild parameter after https://issues.scala-lang.org/browse/SI-10081 is fixed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would still require an explicit parameter even with SI-10081 fixed
|
||
val xs2 = immutable.TreeSet(Some("foo"), Some("bar"), None) | ||
optionSequence(xs2)(immutable.TreeSet.canBuild[String]) | ||
val o2 = optionSequence(xs2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No explicit factory required, even for constrained collections. Both the previous implementation here and my old attempt in #39 needed it.
|
||
// This use case from https://github.com/scala/scala/pull/5233 still eludes us |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm considering to resurrect the idea of path-dependent types for CanBuild
scoping, essentially unifying the new way I implement to
(which doesn't need to infer a unary type constructor anymore) with the implicit builder factories. This would not enable a type-driven breakOut
but a value-driven approach with an explicit factory similar to the syntax below (like in to
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, I agree that we should try something like this.
I tried this trick for a trait Iterable[+A] extends IterableOnce[A] with IterableLike[A, Iterable] {
final type Build[-B, +Repr] = MonomorphicIterableFactory[B, Repr]
...
}
// Testing:
def pair[A, Repr <: Iterable[A], To](xs: Repr with Iterable[A])(implicit fi: Repr#Build[(A, A), To]): To = ???
val xs5 = immutable.List(1, 2, 3)
val p1 = pair(xs5)
val p1t: immutable.List[(Int, Int)] = p1 My understanding of the implicit scopes in 7.2 (http://www.scala-lang.org/files/archive/spec/2.11/07-implicits.html) is that |
You know, I'm getting worried that this level of abstraction is no better than and possibly even worse than CanBuildFrom. ConstrainedIterablePolyTransforms is particularly bad--I can't imagine that anyone who had trouble with CBF would find CIPT simple. (And the various self-types and factories are not terribly transparent either, I don't think, to a less expert user.) I think if this rewrite is going to be a success on the simplification dimension, we're going to have to rely less on clever and elaborate type encodings and more on the compiler to do the thing we want by default. If this means altering the scope or search strategy of implicits, I think that's fine. If we drop the simplification part--that is, understanding how collections work remains a dark art of the high wizards of Scala--then the existing design I think is quite good. And I'm not totally opposed to dropping that part; having better behavior and better implementation improves things quite nicely. But perhaps we do want the simplification, and then I think this is getting to be too weighty. In particular, I would take a completely orthogonal approach to constraints: a constrained collection does not even live in the same hierarchy, but requires boxing to become a full-fledged collection member. We're partway there with ConstrainedIterablePolyTransforms, and I think if you don't care to understand the four type parameters very carefully that using CIPT is actually quite straightforward. But having coexisting constrained and unconstrained methods makes things especially tricky to implement. I'll try to come up with a sketch this weekend where the hierarchy is held together more loosely, and where to get the full power of collections you may need to box into them. It's more of a breaking change, so maybe it's not going to work. But this experiment is convincing me that you can't really get CBF-like functionality without CBF-like complexity. |
/** Transforms over iterables that can return collections of different element types for which an | ||
* implicit evidence is required. | ||
*/ | ||
trait ConstrainedIterablePolyTransforms[+A, +C[A], +CC[X] <: C[X], Ev[A]] extends ConstrainedFromIterable[CC, Ev] with IterablePolyTransforms[A, C] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+C[A]
could be +C[_]
(also in IterablePolyTransforms
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I already removed a bunch of unnecessary names. I must have overlooked these ones.
The simplification vs
For users I think this is vastly simpler than Note that adding the unconstrained collection type constructor as another new type parameter is independent of abstracting over the evidence type.
We're already sacrificing functionality in this design: There's no automatic fallback to building an unconstrained collection and you can't implement a |
@lrytz - Yes, I like the improvement of usage you get from CIPT, as I alluded to earlier! And I do like that it's kept away from many of the collections. @szeiger - Actually I view the lack of automatic fallback an advantage, and I'm not saying that we shouldn't have constrained and unconstrained collections inherit different things. I'm actually wondering whether we can additionally simplify things by allowing the inheritance to vary even more. I'm reasonably happy with the existing design. I'm just looking for additional ways to simplify. For instance, with CIPT we have a fourth type parameter, C[_] or C[A], that exists solely to implement a single widening method. Nothing in CIPT cares (yet) that this method exists, but it complicates the type signature of the trait (both by having it and by requiring CC[X] <: C[X]). Can we just drop it? Or do we need it as a bridge to the less constrained methods? If we need it as a bridge, can we instead require a box? (Maybe this has too big a performance hit.) I think actually a good bit of the problem I sense intuitively is the |
I don't have strong feelings either way, but I'm OK with removing automatic fallback.
Implementing the
I wrote this code in the established Scala style of one-letter type parameters (already deviating a bit with |
Ah, right. So, if you didn't need to inherit There is precedent for longer type parameter names in |
Could you, please, state performance as one of main requirements for new collections. |
@plokhotnyuk use a separate ticket, please |
@szeiger - I played with a few variants of CIPT that have only three type parameters, and though I didn't get all the way through the necessary changes they were promising enough that I am now happy with this approach: I think there is a round or two of simplification that could be applied probably without any compiler changes. So that makes going with this exactly and then slightly refactoring a pretty safe bet. (Without getting everything compiling I couldn't tell whether type inference would work on the new case, but we can tweak type inference I imagine, as long as the inference is a sensible thing to do in general. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This strategy looks a little imposing for less-expert users but is nicely usable, and I think there are likely to be refactorings that somewhat reduce how imposing it is. This is working, so let's go with it, absent good ideas for how to improve it.
*/ | ||
trait ConstrainedIterablePolyTransforms[+A, +C[A], +CC[X] <: C[X], Ev[A]] extends ConstrainedFromIterable[CC, Ev] with IterablePolyTransforms[A, C] { | ||
|
||
protected def coll: Iterable[A] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
*/ | ||
def to[C[X] <: Iterable[X]](fi: FromIterable[C]): C[A @uncheckedVariance] = | ||
def to[F <: FromIterableBase[A]](fi: F): F#To[A @uncheckedVariance] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use fi.To[A]
as the return type instead of the F#To[A]
projection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case it shouldn't matter but I generally try to avoid path-dependent types when type projections are sufficient. With path-dependent types you always need to take care to get the paths right. It is easy to get into a situation where two types are "obviously" the same but the compiler doesn't agree. This is easier with type projections. Everything's fine as long as they dealias to the same type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that Dotty will not accept F#To
because it might be unsound. So we should definitely stick with fi.To[A]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this kind of type projections is going to be removed from Dotty because they are unsound. Since in our case the fi
parameter would always be a static object
I think that using a path-dependent type would not suffer from the problems you are mentioning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, this looked safe enough for me but I haven't tested it on Dotty. It's pretty much the same as scala/scala3#1051 (comment) though, so it won't work. I'll change it to use the path-dependent type instead.
|
||
implicit def canBuild[A]: () => Builder[A, C[A]] = () => newBuilder[A] // TODO Reuse the same instance | ||
def constrainedNewBuilder[A : Ev]: Builder[A, CC[A]] |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
.
// variance seems sound because `to` could just as well have been added | ||
// as a decorator. We should investigate this further to be sure. | ||
fi.fromIterable(coll) | ||
|
||
def to[C[_], Ev[_]](fi: ConstrainedFromIterable[C, Ev])(implicit ev: Ev[A @uncheckedVariance]): C[A @uncheckedVariance] = | ||
fi.constrainedFromIterable(coll) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe that’s unrelated to this PR, but I guess this method does not work with collections having binary type constructors (e.g. TreeMap
), right? BTW, does the unconstrained one work with HashMap
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, this still needs to be improved. The unconstrained version cannot work for binary type constructors. We only have an element type A
in Iterable
. While we can restrict it to work only for (A1,A2)
there is no way to compute these types A1
and A2
only through FromIterable
's upper bound. Conversions to maps either need another special case or a ConstrainedFromIterable
. Since we're special-casing Map
in other places we should probably overload to
with two additional cases for unconstrained and constrained maps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A CanBuildFrom
-like would nicely solve these issues. It’s probably OK if we have it only at this place instead of having it everywhere as in the current collections.
👍 to this work. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, I think this goes in the right direction. But we need to correct for complexity. For me a good rule of thumb is that complexity grows with the product of number of type parameters on a given class or method. So 4 type parameters for CIPT is a lot! It would be great if we could get rid of at least one. This seems to be possible since we seem to agree that automatic widening is not needed.
I also dislike the B
parameter on FromIterable
. We should be particularly careful to minimize type parameter lists of widely used base traits like it. It seems in most cases the parameter is instantiated to Any
. Which makes me wonder:
- can we find a less visible way to represent the parameter, e.g. a as a type member.
- or, should we not bother at all about this? How much code duplication would we get if we special cased those cases where the bound is not
Any
?
Let's also try to not go overboard with long names.
|
||
def newBuilder[A <: B]: Builder[A, C[A]] | ||
|
||
protected[this] lazy val monomorphicIterableFactoryProto: MonomorphicIterableFactory[B, C[B]] = new MonomorphicIterableFactory[B, C[B]] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would try to shorten this name, unless we want to give a great opportunity for ridicule. So my vote goes to CanBuild
and canBuildProto
(or maybe we can even leave out the proto?)
*/ | ||
def to[C[X] <: Iterable[X]](fi: FromIterable[C]): C[A @uncheckedVariance] = | ||
def to[F <: FromIterableBase[A]](fi: F): F#To[A @uncheckedVariance] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that Dotty will not accept F#To
because it might be unsound. So we should definitely stick with fi.To[A]
.
|
||
/** 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[-B, +C[_]] extends FromIterableBase[B] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need the bound here? It would be great if we could avoid the additonal B
parameter.
I don't see how. We need the unconstrained type constructor for manual widening and to get the linearization right. There is no automatic widening in this version.
I would have preferred that, too, but due to variance the default would be
|
@szeiger My tendency would be to special case |
Also in favor of special-casing: it is important that I'll try to finish up some of my 3-parameter CIPTs tonight or the next day. The basic idea is to drop the inheritance of IPT in CIPT and push responsibility for putting something sensible for |
I've just pushed a new commit. Ever since the implicit builders were merged I had this nagging feeling that we had the whole thing backwards. In the new design the source collection is responsible for building a target collection, we do not rely on an implicit CanBuildFrom for that, so why should we do that for implicit builders? It comes with the same old problem of getting different results depending on the static type (because we rely on implicit search in companion objects to decide which builder to use) and it also caused me all kinds of problems trying to unify implicit builders and companion objects. So I went back to the drawing board and started with the assumption that the source collection has to decide what to build. We already had Now we have everything in place to write methods outside the collection framework (like def optionSequence1[C[X] <: Iterable[X] with PolyBuildable[X, C], A](xs: C[Option[A]]): Option[C[A]] =
xs.foldLeft[Option[Builder[A, C[A]]]](Some(xs.newBuilder)) {
case (Some(builder), Some(a)) => Some(builder += a)
case _ => None
}.map(_.result)
def optionSequence1[Ev[_], CC[_], A](xs: ConstrainedPolyBuildable[Option[A], CC, Ev] with Iterable[Option[A]])(implicit ev: Ev[A]): Option[CC[A]] =
xs.foldLeft[Option[Builder[A, CC[A]]]](Some(xs.newConstrainedBuilder)) {
case (Some(builder), Some(a)) => Some(builder += a)
case _ => None
}.map(_.result) Since having to overload everything (if you want to support constrained collection types) is rather awkward, the next step was how to unify both through an implicit value. The result is essentially trait BuildFrom[-From, -Elem] {
type To
def newBuilder(from: From): Builder[Elem, To]
def fromIterable(from: From)(it: Iterable[Elem]): To
} Note that there is no method to get a While usage is essentially the same as for All implicit builders in companion objects are gone and so are the Now we can write a single method like this that abstracts over def optionSequence[CC[X] <: Iterable[X], A](xs: CC[Option[A]])(implicit bf: BuildFrom[CC[Option[A]], A]): Option[bf.To] = ... We can even do val xs4 = immutable.List[Option[(Int, String)]](Some((1 -> "a")), Some((2 -> "b")))
val o4 = optionSequence(xs4)(immutable.TreeMap) In fact, we can use it to define def to[F <: TypeConstrainedFromIterable[A]](fi: F): fi.To[A @uncheckedVariance] =
fi.fromIterable(coll)
// 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) The first version is purely for performance: It allows collection types without an evidence value to be built in a more direct way. Everything else uses the implicit conversion to
|
Aha! I like this one. I think it still needs a couple of rounds of simplification and renaming to make it comprehensible and to get rid of some of the veryLongNamesEspeciallyWhenTheyRepeat: LongNameEspeciallyWhenTheyRepeat = new LongNameEspeciallyWhenTheyRepeat things. But this seems like a better design overall. I have to think through and test cases where the source collection being more in-charge might be a problem. But from first principles, as you say, this seems better: if the source is in charge, dynamic dispatch takes care of loss of type information. Conceptually, then, the issue would be creating collections from scratch. I need to look into that in more detail. |
That could be useful to implement |
type To = C[E] | ||
def newBuilder(from: Any): Builder[E, To] = fact.newBuilder | ||
def fromIterable(from: Any)(it: Iterable[E]): To = fact.fromIterable(it) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UpperBoundedFromIterable
?
There was a problem hiding this comment.
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.
trait SortedSet[A] | ||
extends collection.SortedSet[A] | ||
with Set[A] | ||
with SortedSetLike[A, SortedSet] | ||
|
||
trait SortedSetLike[A, +C[X] <: SortedSet[X]] | ||
extends collection.SortedSetLike[A, C] | ||
with ConstrainedIterablePolyTransforms[A, Set, SortedSet] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with ConstrainedIterablePolyTransforms[A, Set, C]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, would it be possible to pull up this line at the level of collection.SortedSet
so that we don’t repeat it for collection.mutable.SortedSet
and collection.immutable.SortedSet
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's possible.
@@ -331,6 +336,8 @@ class StrawmanTest { | |||
// val xs7 = xs.map((k: String, v: Int) => (new Foo, v)) Error: No implicit Ordering defined for Foo | |||
val xs7 = (xs: immutable.Map[String, Int]).map((k, v) => (new Foo, v)) | |||
val xs8: immutable.Map[Foo, Int] = xs7 | |||
val xs9 = xs6.to(List).to(mutable.HashMap) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
// Breakout-like use case from https://github.com/scala/scala/pull/5233: | ||
val xs4 = immutable.List[Option[(Int, String)]](Some((1 -> "a")), Some((2 -> "b"))) | ||
val o4 = optionSequence(xs4)(immutable.TreeMap) // same syntax as in `.to` | ||
val o4t: Option[immutable.TreeMap[Int, String]] = o4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Also like the latest change! I'm a bit worried about keeping / promoting the "implicit buildFrom" pattern. We are moving away from it for the internal collections implementation and use overloading instead, why should we recommend it for external methods? I see that collection types don't have to define their own BuildFrom instances, which certainly makes things simpler. But there are other issues with implicit BuildFrom, like the complex signatures, Scaladoc, IDE discovery. For example, I defined your |
@Ichoran @julienrf Building an arbitrary target collection will require a factory object. Just as in the case of @lrytz I tried it with |
Then we will not be able to write generic code as you did with With the current collections we can write the following: class Unfolder[That] {
def apply[S, A](init: S)(next: S => Option[(A, S)])(implicit cbf: CanBuildFrom[Nothing, A, That]): That = {
val builder = cbf.apply()
…
}
} And then |
Yes, that's correct. It is consistent with |
My global feeling about this PR: I definitely think we need a |
@julienrf - I concur. That was one of the "rounds of simplification" I envisioned, along with one more attempt to check whether any more type parameters can be made type members instead. (Along with a renaming to things that might technically be a little less accurate but are more intuitive, e.g. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good! I am happy for this to go in after some polishings.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
// 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. */ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
||
/** Transforms over iterables that can return collections of different element types for which an | ||
* implicit evidence is required. | ||
*/ |
There was a problem hiding this comment.
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 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 { |
There was a problem hiding this comment.
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.
Should we try to merge Bounded (aka TypeConstrained) and Constrained? We can always express Bounded as a Constraint where we demand an instance of |
I pushed another commit to address @odersky's review comments.
The problem with |
@lrytz I noticed that I don’t have the same result as you: What bothers me is this |
@julienrf this changed at some point between my comment (#45 (comment)) and now. On the current master branch I get the same as you. |
@szeiger In practice, do you think we will have other instantiations of What would be the impact of removing this abstract layer |
I can't think of any other instantiations in the core collection library. Apart from some reusable interfaces we wouldn't lose much by not abstracting over |
`BuildFrom` is like `FromSpecificIterable` but with an extra `From` argument, like in the final version of scala#45. `FromSpecificIterable` existed conceptually in that version as `BuildFrom[Any, …]` but didn’t have a separate type. This new version has separate abstractions for buildable (strict) collection types in the form of `StrictBuildFrom` and `FromSpecificIterableWithBuilder`. Since we can get a surrogate builder (through one of the new `Builder.from` methods) for any lazy collection and we can restrict code to work only with strict collections via the `Buildable` trait, this is not strictly necessary. The only reason for separating the `Builder` abstractions is to avoid exposing them in `FromSpecificIterable`. Even though everything can be built in a strict way, these abstractions sit on top of the lazy ones, not below them.
`BuildFrom` is like `FromSpecificIterable` but with an extra `From` argument, like in the final version of #45. `FromSpecificIterable` existed conceptually in that version as `BuildFrom[Any, …]` but didn’t have a separate type. This new version has separate abstractions for buildable (strict) collection types in the form of `StrictBuildFrom` and `FromSpecificIterableWithBuilder`. Since we can get a surrogate builder (through one of the new `Builder.from` methods) for any lazy collection and we can restrict code to work only with strict collections via the `Buildable` trait, this is not strictly necessary. The only reason for separating the `Builder` abstractions is to avoid exposing them in `FromSpecificIterable`. Even though everything can be built in a strict way, these abstractions sit on top of the lazy ones, not below them.
This PR takes the result of #39 and the discussion there to implement the necessary abstractions properly. This integrates the
() => Builder
implicits that were added to master in the meantime and unifies these builder factories with type-constrained/unconstrained collection factories and with monomorphic collection factories that do not need hacks likeBitSet with Iterable[E]
anymore.Add an upper bound in
FromIterable
andIterableFactory
. Thisallows us to express type-constrained iterables like
BitSet
withoutrequiring an implicit evidence.
Drop the
C[X] <: Iterable[X]
constraint inFromIterable
.Guaranteeing an
Iterable[X]
result is not necessary anywhere andit makes monomorphic collection types awkward, for example producing
a
BitSet with Iterable[X]
for anyX <: Int
. What we really wantis to be able to drop the precise element type in
C[X] = BitSet
.Add
FromIterableBase
which contains the target type constructor asa type member. It is extended by
FromIterable
which pulls thetype constructor out into a type parameter. This allows us to write
polymorphic code for arbitrary
FromIterableBase
subtypes withouthaving to infer the type constructor (which is not possible when it
is not a real type constructor but a type lambda computation like
C[X] = BitSet
viaMonomorphicIterableFactory
.Add
ConstrainedFromIterable
andConstrainedIterableFactory
tomirror
FromIterable
andIterableFactory
but with an additionalimplicit evidence for the element type. An upper bound and a
monomorphic version are not necessary: Constrained collection types
are always polymorphic (otherwise they would already have the
correct evidence value for the element type) and any type bound can
be rolled into the evidence type.
Overload
to
forFromIterable
andConstrainedFromIterable
.Add
ConstrainedIterablePolyTransforms
which mirrorsIterablePolyTransforms
with an additional implicit evidence (so itis based on
ConstrainedFromIterable
instead ofFromIterable
). Itprovides an additional method
unconstrained
to perform a wideningconversion to the most specific unconstrained collection type.
Replace the implicit
() => Builder
with a more generic implicitMonomorphicIterableFactory
. It works via implicit lookup for allunconstrained, type-constrained and value-constrained unary type
constructors.
Add
ConstrainedMapFactory
which mirrorsMapFactory
with anadditional implicit evidence for the key type. I considered using
a binary type constructor that allows the evidence to be based on
both, key and value types but it would complicate the API with a
projection type for the key-only constraints and I do not expect that
we will need it for any collection.
Implement a builder-based
fromIterable
inMapFactory
andConstrainedMapFactory
and add implicitMonomorphicIterableFactory
implementations to both.