-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Remove some @uncheckedVariances, introduce empty in Iterable.
#7929
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
Conversation
This comment has been minimized.
This comment has been minimized.
|
Another random thought: can we select a type from the companion object that's returned by the factory, and hang the more precise collection type off of that? |
Something like that? trait IterableOps[+A, +CC[_], +C] {
val iterableFactory: IterableFactory[CC] { type Coll[X] <: C }
def fromSpecific(source: IterableOnce[A]): C = iterableFactory.from(source)
}
trait IterableFactory[CC[_]] {
type Coll[A] = CC[A]
def from[A](source: IterableOnce[A]): CC[A]
}The problem is that the trait IterableOps[+A, +CC[_], +C] {
implicit def ev: CC[A] <:< C
def iterableFactory: IterableFactory[CC]
def fromSpecific(source: IterableOnce[A]): C = ev(iterableFactory.from(source))
} |
This comment has been minimized.
This comment has been minimized.
I tried to get this working this morning, but failed trait DefaultFromSpecific[+A, +CC[_], +C] {
protected def ev: CC[A] @uncheckedVariance <:< C
protected def fromSpecific(coll: It[A @uncheckedVariance]): C = ev(iterableFactory.from(coll))
protected def newSpecificBuilder: Bldr[A @uncheckedVariance, C] = iterableFactory.newBuilder[A].mapResult(ev)
def iterableFactory: ItFact[CC]
}
trait ItOps[+A, +CC[_], +C] {
protected def newSpecificBuilder: Bldr[A @uncheckedVariance, C]
protected def fromSpecific(coll: It[A @uncheckedVariance]): C
def iterableFactory: ItFact[CC]
}
trait It[+A] extends ItOps[A, It, It[A]] with DefaultFromSpecific[A, It, It[A]] {
protected def ev: It[A] @uncheckedVariance <:< It[A] = implicitly
def iterableFactory: ItFact[It] = It
}
object It extends ItFact[It] {
def from[A](source: It[A]): It[A] = new It[A]{}
def newBuilder[A]: Bldr[A,It[A]] = new Bldr[A, It[A]] { def result(): It[A] = new It[A]{} }
}So far so good, but it breaks down in subclasses trait SqOps[A, +CC[_], +C] extends ItOps[A, CC, C]
trait Sq[A] extends It[A] with SqOps[A, Sq, Sq[A]] with DefaultFromSpecific[A, Sq, Sq[A]] {
override protected def ev: Sq[A] @uncheckedVariance <:< Sq[A] = implicitly
override def iterableFactory: ItFact[Sq] = Sq
}
object Sq extends ItFact[Sq] {
def from[A](source: It[A]): Sq[A] = new Sq[A]{}
def newBuilder[A]: Bldr[A, Sq[A]] = new Bldr[A, Sq[A]] { def result(): Sq[A] = new Sq[A]{} }
}gives Who can find a way to make this (or something similar) work? |
|
Here's a pragmatic version that would work, using a cast. Full example: /** CAUTION: only use this mixin when CC[A] =:= C */
trait DefaultFromSpecific[+A, +CC[_], +C] {
protected def fromSpecific(coll: It[A @uncheckedVariance]): C = iterableFactory.from(coll).asInstanceOf[C]
protected def newSpecificBuilder: Bldr[A @uncheckedVariance, C] = iterableFactory.newBuilder[A].mapResult(_.asInstanceOf[C])
def iterableFactory: ItFact[CC]
}
trait ItOps[+A, +CC[_], +C] {
protected def newSpecificBuilder: Bldr[A @uncheckedVariance, C]
protected def fromSpecific(coll: It[A @uncheckedVariance]): C
def iterableFactory: ItFact[CC]
def filter: C = fromSpecific(it)
def strictFilter: C = newSpecificBuilder.result()
def map[B](f: A => B): CC[B] = iterableFactory.newBuilder.result()
}
trait It[+A] extends ItOps[A, It, It[A]] with DefaultFromSpecific[A, It, It[A]] {
def it: It[A] = this
def iterableFactory: ItFact[It] = It
} |
trait DefaultFromSpecific[A, +CC[_]] { self: ItOps[A, CC, CC[A]] =>
protected def fromSpecific(coll: It[A]): CC[A] = iterableFactory.from(coll)
protected def newSpecificBuilder: Bldr[A, CC[A]] = iterableFactory.newBuilder[A]
}look ma, no casts! one more diff to your gist: trait It[+A] extends ItOps[A, It, It[A]] with DefaultFromSpecific[A @uncheckedVariance, It] {
// ...
trait Sq[A] extends It[A] with SqOps[A, Sq, Sq[A]] with DefaultFromSpecific[A, Sq] { |
|
Ah nice, I didn't see that :-) It might be slightly nicer to make |
|
True, but it's a good reminder your'e doing something dicey. Plus, you don't always need the "variance cast" either ( |
|
I'll refactor the PR using DefaultFromSpecific -- the self type wouldn't have occurred to me without the nice example. |
af9d20c to
73e8f43
Compare
This comment has been minimized.
This comment has been minimized.
|
Details| Class | empty's signature | | ------------- | ------------- | | scala.collection.MapView | scala.collection.MapView[K,V] | | scala.collection.SortedMap | scala.collection.SortedMap[K,V] | | **scala.collection.AbstractSet** | **scala.collection.Set[A]** | | **scala.collection.DefaultMap** | **scala.collection.Map[K,V]** | | **scala.collection.AbstractMapView** | **scala.collection.MapView[K,V]** | | scala.collection.Set | scala.collection.Set[A] | | scala.collection.SortedSet | scala.collection.SortedSet[A] | | scala.collection.BitSet | scala.collection.BitSet | | **scala.collection.AbstractMap** | **scala.collection.Map[K,V]** | | scala.collection.Map | scala.collection.Map[K,V] | | scala.collection.immutable.ListSet | scala.collection.immutable.ListSet[A] | | scala.collection.immutable.Set | scala.collection.immutable.Set[A] | | scala.collection.immutable.SortedMap | scala.collection.immutable.SortedMap[K,V] | | scala.collection.immutable.SortedSet | scala.collection.immutable.SortedSet[A] | | scala.collection.immutable.TreeSet | scala.collection.immutable.TreeSet[A] | | scala.collection.immutable.BitSet | scala.collection.immutable.BitSet | | **scala.collection.immutable.AbstractSet** | **scala.collection.immutable.Set[A]** | | scala.collection.immutable.VectorMap | scala.collection.immutable.VectorMap[K,V] | | scala.collection.immutable.HashMap | scala.collection.immutable.HashMap[K,V] | | scala.collection.immutable.TreeMap | scala.collection.immutable.TreeMap[K,V] | | scala.collection.immutable.HashSet | scala.collection.immutable.HashSet[A] | | **scala.collection.immutable.AbstractMap** | **scala.collection.immutable.Map[K,V]** | | scala.collection.immutable.TreeSeqMap | scala.collection.immutable.TreeSeqMap[K,V] | | scala.collection.immutable.Map | scala.collection.immutable.Map[K,V] | | scala.collection.immutable.LongMap | scala.collection.immutable.LongMap[T] | | scala.collection.immutable.IntMap | scala.collection.immutable.IntMap[T] | | scala.collection.immutable.ListMap | scala.collection.immutable.ListMap[K,V] | | scala.collection.immutable.SeqMap | scala.collection.immutable.SeqMap[K,V] | | scala.collection.mutable.SortedSet | scala.collection.mutable.SortedSet[A] | | scala.collection.mutable.TreeSet | scala.collection.mutable.TreeSet[A] | | scala.collection.mutable.LinkedHashSet | scala.collection.mutable.LinkedHashSet[A] | | scala.collection.mutable.BitSet | scala.collection.mutable.BitSet | | scala.collection.mutable.LinkedHashMap | scala.collection.mutable.LinkedHashMap[K,V] | | **scala.collection.mutable.AbstractSet** | **scala.collection.mutable.Set[A]** | | scala.collection.mutable.OpenHashMap | scala.collection.mutable.OpenHashMap[Key,Value] | | **scala.collection.mutable.MultiMap** | **scala.collection.mutable.Map[K,scala.collection.mutable.Set[V]]** | | scala.collection.mutable.CollisionProofHashMap | scala.collection.mutable.CollisionProofHashMap[K,V] | | scala.collection.mutable.HashMap | scala.collection.mutable.HashMap[K,V] | | scala.collection.mutable.TreeMap | scala.collection.mutable.TreeMap[K,V] | | scala.collection.mutable.HashSet | scala.collection.mutable.HashSet[A] | | **scala.collection.mutable.AbstractMap** | **scala.collection.mutable.Map[K,V]** | | scala.collection.mutable.Map | scala.collection.mutable.Map[K,V] | | scala.collection.mutable.LongMap | scala.collection.mutable.LongMap[V] | | scala.collection.mutable.Set | scala.collection.mutable.Set[A] | | scala.collection.mutable.ListMap | scala.collection.mutable.ListMap[K,V] | | scala.collection.mutable.SortedMap | scala.collection.mutable.SortedMap[K,V] | | scala.collection.mutable.WeakHashMap | scala.collection.mutable.WeakHashMap[K,V] | | scala.collection.mutable.AnyRefMap | scala.collection.mutable.AnyRefMap[K,V] | | scala.collection.mutable.SeqMap | scala.collection.mutable.SeqMap[K,V] | |
|
Generated in the repl with: |
|
It's kind of strange that |
|
@adriaanm There is a plan to pull it to |
|
Ah cool, then I think we may as well add it now. I already have it locally |
yay! 🎉 |
lrytz
left a comment
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 looked through the diff again, this LGTM now.
|
Ok, I think it's time for some squashing then. |
Avoid unexpected class casts due to potential misuse in subclasses of the following cunning trick, which allowed for centralized implementation of common factory methods, assuming the right overrides are provided in subclasses: `protected type IterableCC[X] = CC[X] @uncheckedVariance` Risk of not overriding correctly illustrated by https://gist.github.com/lrytz/fe09791208969f07e211c5c8601db125. It's basically an encoding of MyType, which is only sound under pretty severe restrictions, which are not enforced by the compiler. Instead, we mix in a default implementation of the various factory methods whenever we are in a trait where the collection's type is known (and of the regular shape CC[A] or MapCC[K, V]). It would be nice if we could conditionally mix in this trait into the *Ops traits whenever `C =:= CC[A]`, but we don't have that kind of inheritance (private inheritance would be nice too, I guess, since the `*FactoryDefaults` traits don't really add much utility to the public signature of these collection classes). While we're at it, also add an `empty` method at the top of the hierarchy to facilitate the switch to dotty's for comprehensions (since we want the stdlibs between Scala 2 & 3 to be the same, and 2.14 will try to only make backwards compatible changes from 2.13). Motivated by the intersection of scala#7458, scala#7929, scala#7800 (better refchecks and compiling with dotc revealed part of the problem) 🤯 Co-authored-by: Lukas Rytz <[email protected]>
So far I have not been able to come up with a correct optimization for this check. Since it doesn't seem to be performance critical after all, I'm disabling the optimization. I'd be glad to bring a sound version back, but it will have to come with a pretty compelling formal explanation of how to reason about overriding pairs. On the reporting side, we could try to be a bit less redundant in the presence of errors, by only reporting on similar pairs of overriding errors once (e.g., track pairs of symbol + info as seen from the current base, kind of like we already do for mixin errors.) (Note that for bridges we still need to exclude certain combos for correctness.)
| * method needs to be overridden to return a factory for the new type (the compiler will | ||
| * issue an error otherwise). | ||
| */ | ||
| def mapFactory: MapFactory[CC] |
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.
Iterable.iterableFactory has a default implementation so you can just extends Iterable in simple cases. It should probably be the same for mapFactory.
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 abstract method is in MapOps, the default implementation is in Map (https://github.com/scala/scala/pull/7929/files#diff-7e4a5aff40ea5ded3e138d24f2e3cacaR29). Or did I misunderstand?
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.
Oh, right. I blame it on the low contrast of the screen in the sun when working outdoors :-)
| override protected def newSpecificBuilder: mutable.Builder[A, CC[A]] = sortedIterableFactory.newBuilder[A] | ||
| override def empty: CC[A] = sortedIterableFactory.empty | ||
|
|
||
| override def withFilter(p: A => Boolean): SortedSetOps.WithFilter[A, WithFilterCC, CC] = |
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.
Having withFilter in these traits seems odd, but then again, withFilter was always the ugliest and most problematic part of the new collections design.
@uncheckedVariances, introduce empty in Iterable.
Otherwise things like the following wouldn't compile
```
class Top; class Sub extends Top
class Ok {
(??? : Seq[Sub]) match { case _: List[Top] => case _ => }
}
```
as soon as there's an invariant superclass in an otherwise
covariant class, the isPopulated check will bail unless
type arguments are equal, whereas for variant positions it
ignores the type arguments.
TODO: ignore `@uncheckedVariance` type args in isPopulated?
Give `VectorPointer` the same treatment since it's mixed into
the covariant Vector.
This leaves us with one unexplained test update in BuildFromTest.
I cannot figure out why this type ascription to a super type is
needed.
|
The test failures in previous commits were intentional to motivate the final commit. |
Avoid unexpected class casts due to
protected type IterableCC[X] = CC[X] @uncheckedVariance-- when misused in subclasses. Risk illustrated by https://gist.github.com/lrytz/fe09791208969f07e211c5c8601db125. It's basically an encoding of MyType, which is only sound under pretty severe restrictions, which are not enforced by the compiler.Instead, we mix in a default implementation of the various factory methods whenever we are in a trait where the collection's type is known (and of the regular shape CC[A] or MapCC[K, V]). It would be nice if we could conditionally mix in this trait into the *Ops traits whenever
C =:= CC[A], but we don't have that kind of inheritance (private inheritance would be nice too, I guess, since the*FactoryDefaultstraits don't really add much utility to the public signature of these collection classes).While we're at it, also add an
emptymethod at the top of the hierarchy to facilitate the switch to dotty's for comprehensions (since we want the stdlibs between Scala 2 & 3 to be the same, and 2.14 will try to only make backwards compatible changes from 2.13).Motivated by the intersection of #7458, #7929, #7800 (better refchecks and compiling with dotc revealed part of the problem)