-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Proposal: make Set covariant #6728
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
We now always define them with bounds like this:
trait FooSetOps[
A,
+CC[X] <: FooSetOps[X, CC, _] with FooSet[X],
+C <: FooSetOps[A, CC, C] with CC[A]
]
This is the most precise bounds we can give to this type. Most of the
time this precision is not needed but this makes it easier to add new
operations without breaking compatibility.
The same scheme could also be used for other *Ops traits but I haven't
tried it.
e7426dd to
60c51be
Compare
|
about the @odersky was very down on removing |
As a first step, we make Set and immutable.Set covariant, but keep their operations as-is, annotated with @UV to typecheck. We also create InvariantSetOps and immutable.InvariantSetOps that we will use in subsequent commits to hold the operations that only make sense on invariant subclasses of Set. Note that currently, the only non-mutable Sets that must be invariant are the SortedSets, so we could fusion InvariantSetOps with SortedSetOps to reduce the API at the cost of some flexibility.
`contains` now takes an extra type parameter to pass variance checks. To avoid boxing we overload `contains` in BitSet. The implementation of `contains` in GenKeySet and TreeSet are currently unsafe and hard to make safe due to erasure, this will have to be resolved before this PR can be merged. `+`/`incl` are changed similarly. Additionally, overloaded versions with their old type signatures are added to InvariantSetOps. This is crucial for all the SortedSets since adding an element of the same type should preserve the sorting.
Note: it would be cleaner to use a bounded wildcard:
def intersect(that: Set[_ >: A]): C
Unfortunately that would result in worse type inference, see
scala/bug#10920
This is handled similarly to `incl`.
It should never be needed with a covariant Set. This means we could also completely remove the type parameter from the definition of `toSet`, however that would break source compatibility. This might be fixable by adding a `toSet[B >: A]: Set[B]` as a deprecated extension method but I haven't explored that.
|
I'm hesitant to allow this change: |
|
@retronym Yeah, that's a trade-off. Of course the same issue already exists with Seq, List, etc. This could be solved by emitting a linting warning when the inferred type parameter of |
|
Isn't there any solution? Maybe making contains an extension method? Using
some kind of typeclass?
…On Tue, Jun 5, 2018 at 7:41 PM Guillaume Martres ***@***.***> wrote:
@retronym <https://github.com/retronym> Yeah, that's a trade-off. Of
course the same issue already exists with Seq, List, etc. This could be
solved by emitting a linting warning when the inferred type parameter of
contains does not match the element type of the collection on which
contains is called.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#6728 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAGAUJiED_pEWrySHbZI5XvzPklMWcabks5t5xc1gaJpZM4Ube5S>
.
|
|
If you find one, let us know ;). |
|
I think making |
|
You can already do this: So why should the other direction be a problem? That's just a consequence of using universal |
|
Because if you have a Set[Object] you can't know whether it contains a
String until runtime, so testing at runtime is a reasonable and often
necessary thing to do.
On the other hand, with the extra method type parameter, it's happy to LUB
to Any. So we are legalizing writing expressions that we essentially know
at compile time are an expensive way of saying `false` -- almost certainly
a bug.
…On Wed, Jun 6, 2018, 12:28 AM Georgi Krastev ***@***.***> wrote:
You can already do this:
scala> Set[Object]("42").contains("42")
res0: Boolean = true
So why should the other direction be a problem?
That's just a consequence of using universal equals and hashCode. If Set
expected Hash and Eq type classes we wouldn't be able to make it
covariant, but that's not the design choice made by the collections library.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#6728 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAGAUCvRBpSbE4aQD_WuBJfEOwiH4R-Aks5t51pvgaJpZM4Ube5S>
.
|
|
A
So a Also, a set implies an equivalence relation on its type parameter |
|
@retronym This code should produce a warning because |
| extends immutable.AbstractSet[Value] | ||
| with immutable.SortedSet[Value] | ||
| with immutable.SetOps[Value, immutable.Set, ValueSet] | ||
| with immutable.SortedSetOps[Value, immutable.SortedSet, ValueSet] |
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, this is an interesting change that should be applied even if this PR is rejected.
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.
@smarter Do you mind opening another PR with this change only?
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.
| * @return a new $coll with the given elements added, omitting duplicates. | ||
| */ | ||
| def concat(that: collection.Iterable[A]): C = fromSpecificIterable(new View.Concat(toIterable, that)) | ||
| def concat[B >: A](that: collection.Iterable[B]): CC[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.
Isn’t it possible to implement it as follows?
fromIterable(new View.Concat(this, that))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, but I don't think that's necessary since this is already how it's implemented in IterableOps. The def here is only useful for the Set-specific documentation comment.
| trait SortedSetOps[A, +CC[X] <: SortedSet[X], +C <: SortedSetOps[A, CC, C]] | ||
| extends SetOps[A, Set, C] | ||
| trait SortedSetOps[A, +CC[X] <: SortedSetOps[X, CC, _] with SortedSet[X], +C <: SortedSetOps[A, CC, C] with CC[A]] | ||
| extends InvariantSetOps[A, CC, 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.
Can SortedSet be made covariant too?
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, because if I have a SortedSet[Int], I don't have a SortedSet[Any] since the latter would require an Ordering[Any] which is not something I can construct from an Ordering[Int].
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, thanks for clarifying!
|
Also, since |
|
I thought I could implement a safe and efficient version of |
|
Can someone tell me what the point is in a type parameter that isn't used
anywhere other than as the value parameter's type? How is it different than
giving the parameter type Any? After all scala is willing to infer Any or
whatever the LUB is.
@ def m[A >: Int](a: A) = 1
defined function m
@ m("a")
res2: Int = 1
@ desugar(m("a"))
res3: Desugared = ammonite.$sess.cmd1.m[Any]("a")
…On Wed, Jun 6, 2018 at 6:00 PM Guillaume Martres ***@***.***> wrote:
I thought I could implement a safe and efficient version of
TreeSet#contains with a signature of [A1 >: A](x: A): Boolean but I now
believe this might not be possible which would be a show-stopper. A
TreeSet[A] contains an Ordering[A] which must be used in some way by
contains to make it O(log(n)) and not O(n). But I have no way of passing
safely a value of type A1 >: A to such an Ordering, and no way of
checking if a value of type A1 has the more precise type A (
isInstanceOf[A] is meaningless because of erasure, and even storing a
ClassTag in TreeSet wouldn't help if A itself is parameterized, e.g.
TreeSet[List[Int]]), so it appears that we're stuck with the current
design :/.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#6728 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAGAUCg9bMeOijNsIa-FLfn7ZroBmWdtks5t6FD9gaJpZM4Ube5S>
.
|
|
Huge mistake. Just for the record. |
|
@paulp thanks for commenting. Can you expand your reasoning? (FWIW I'm also against.) |
|
@nafg I spent years explaining this. I don't work for free anymore, especially for people who can't bring themselves to look at previous work. It's all still out there. |
|
|
||
| override def empty = ValueSet.empty | ||
| def contains(v: Value) = nnIds contains (v.id - bottomId) | ||
| def contains [A1 >: Value](v: A1) = v match { |
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 made this point already in Conversation (no one responded), but, can someone tell me how this signature is meaningfully different from def contains(v: Any) ?
|
@paulp can you at least say whether your opposition is to Set being covariant per se, or to unsafe contains, or something else? |
|
BTW as far as typesafe contains, what was the issue with using an extension method? I got this to work: https://scalafiddle.io/sf/ygyRTMD/0 object P {
class CovariantSet[+A](xs: Seq[A]) {
private[P] def _contains(a: Any) = xs.contains(a)
}
implicit class CovariantSetContains[A](private val set: CovariantSet[A]) extends AnyVal {
@inline final def contains(a: A) = false
}
new CovariantSet(List(1, 2, 3)).contains("hello")
}(The above call to contains won't compile.) |
|
@nafg You can't "trick" mathematics. An extension method exploits an idiosyncrasy of type inference to sometimes not compile. It doesn't change anything fundamental - notice you can still call the method if you offer a type parameter. Expecting a game of "hide the variance" with the type inferencer to introduce any kind of correctness interacts nightmarishly with everything else which touches type inference. Elsewhere it was said that this doesn't even work in dotty because it looks harder, which nicely illustrates the point if true, but is irrelevant if untrue. The fundamental, defining property of a set is |
I don't think you can if the implicit class is used implicitly?
I see, because in dotty the type inferencer looks further down the road in the expression, and when it sees you using a String for A, it takes that into account when selecting the A for the implicit class. |
@nafg From a correctness standpoint this is irrelevant. |
Yeah that's what I belatedly realized too, so I'm closing the PR. Cheers! |
This is up for debate. I use |
|
Unique according to whom? All mysteries can be solved (and better answers revealed) with a principled investigation of that question. And I am not talking about Function1, which Set certainly should NOT extend, but that's neither here nor there. I am talking about set membership being the basis upon which sets are constructed. |
|
|
@lihaoyi The problem with this is that |
Oh ok. Personally I'd be happy with separating |
There's no fundamental reason for Set being invariant, quoting Martin in 2011 (https://www.scala-lang.org/old/node/9764):
This PR is an attempt at solving this irregularity, this is done in several steps:
Set[A]stop extendingA => Boolean, replacing it by an implicit conversion defined in the companion object. This is not 100% source compatible (there was one case in scala/scala where I had to add an explicit.contains) but it should be "good enough".Aappears in contravariant position such asdef + (elem: A): Cbydef + [B >: A](elem: B): CC[B].{collection,immutable}.InvariantSetOps, these traits are mixed in by the subclasses of Set which must be invariant (that is, all mutable Sets as well as all SortedSets). These overloads are crucial for SortedSets sinceSortedSet(1, 2, 3) + 42should return aSortedSet[Int]and not aSet[Int], butSortedSet(1, 2, 3) + "42"must return aSet[Any].This PR is missing a few things (more documentation, more tests, and proper implementations of
containsforGenKeySetandTreeSet), but I'd like to know if this is something that is worth pursuing or if it's simply too late/risky to do such a change now. In any case, I'm happy I did this PR since it forced me to learn how the new collections work :).