- Notifications
You must be signed in to change notification settings - Fork 70
Overloading based implementation of Set, TreeSet, Map and TreeMap #24
Conversation
| } | ||
| | ||
| /** Polymorphic transformation methods on sorted collections */ | ||
| trait SortedPolyTransforms[A, +C[X] <: Sorted[X]] { |
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 trait is analogous to IterablePolyTransforms but polymorphic transformation methods takes an additional implicit Ordering parameter.
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 would be possible to abstract over different evidence types (e.g. Ordering[_] vs _ <:< Int) but at least the Int case could be done with a simpler API. Do we have any other similar cases?
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 ClassTag[_], depending on how we deal with arrays. I don’t understand what you mean with “the Int case”.
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 mean that you wouldn't want to overload BitSet.map as def map[B](f: Int => B)(implicit ev: B <:< Int) but as def map(f: Int => 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.
But ClassTag could indeed be interesting. It would allow us to reuse a generic PolyTransformsWithEvidence for specialized collections.
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 created #28 to keep track of this issue.
| /** | ||
| * Factories for collections whose elements require an ordering | ||
| */ | ||
| trait OrderingGuidedFactories[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.
Analogous to IterableFactories, but with an implicit Ordering.
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.
Same here.
| // so that they take precedence over the one inherited from IterablePolyTransforms | ||
| def map[B](f: A => B)(implicit ordering: scala.Ordering[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.
One problem is that Sorted and Set are independent so there is no disambiguation mechanism between their overloaded methods. That’s why I had to re-define the methods of Sorted (to make them take precedence over those of Set).
I also tried an approach based on intermediate LowPriority linear traits, without success:
trait SortedSetLikeLowPriority1[A, +C[X] <: SortedSet[X]] extends SetLike[A, Set] with SetOps[A, C[A]] with SetMonoTransforms[A, C[A]] trait SortedSetLikeLowPriority2[A, +C[X] <: SortedSet[X]] extends SortedSetLikeLowPriority1[A, C] with SortedLike[A, C] trait SortedSetLike[A, +C[X] <: SortedSet[X]] extends SortedSetLikeLowPriority2[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.
Couldn't this be resolved by making SortedPolyTransforms extend 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.
Probably, but this is not what we want: we really want SortedSet[A] to extend SortedPolyTransforms[A, SortedSet] and IterablePolyTransforms[A, Set] (note that we have Set in the latter case).
We could define SortedPolyTransforms as follows, as a workaround:
trait SortedPolyTransforms[A, +C[X] <: Sorted[X], +D[X] <: Iterable[X]] extends IterablePolyTransforms[A, D]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 wouldn't call it a workaround, I think that's how it ought to be defined. You also have SeqPolyTransforms <: IterablePolytransforms. Enforcing the correct linearization makes overrides work in the desired order.
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 just pushed a commit that fixes the linearization order with a similar trick.
| def sortedSets(xs: strawman.collection.immutable.SortedSet[Int]): Unit = { | ||
| val xs1 = xs.map((x: Int) => x.toString) | ||
| val xs2: SortedSet[String] = xs1 | ||
| val xs3: strawman.collection.immutable.Set[String] = xs.map((x: Int) => x.toString) |
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 the type annotation x: Int 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.
This looks wrong. Scala 2.12 should be able to perform overload resolution correctly without a 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.
Where does the problem come from?
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.
@szeiger How could we investigate where does this issue come from?
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.
Simplify and run through -Ytyper-debug?
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.
Looks like a type inferencer bug. This is as far as I got: https://issues.scala-lang.org/browse/SI-10194
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.
| extends SortedLike[A, C] | ||
| with SetLike[A, Set] // Inherited Set operations return a `Set` | ||
| with SetOps[A, C[A]] // Override the return type of Set ops to return C[A] | ||
| with SetMonoTransforms[A, C[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.
There is subtlety here: we inherit from SetLike[A, Set] so that methods like map return a Set rather than a SortedSet. However we don’t want + or ++ to return a Set but a SortedSet, hence the SetOpt[A, C[A]] and SetMonoTransforms[A, C[A]] mixins.
| } | ||
| | ||
| /** Monomorphic transformation operations */ | ||
| trait SetMonoTransforms[A, +Repr] { |
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.
extends IterableMonoTransforms?
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.
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 added a commit with this change.
| } | ||
| | ||
| /** Polymorphic transformation methods on sorted collections */ | ||
| trait SortedPolyTransforms[A, +C[X] <: Sorted[X]] { |
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 would be possible to abstract over different evidence types (e.g. Ordering[_] vs _ <:< Int) but at least the Int case could be done with a simpler API. Do we have any other similar cases?
| /** | ||
| * Factories for collections whose elements require an ordering | ||
| */ | ||
| trait OrderingGuidedFactories[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.
Same here.
| // so that they take precedence over the one inherited from IterablePolyTransforms | ||
| def map[B](f: A => B)(implicit ordering: scala.Ordering[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.
Couldn't this be resolved by making SortedPolyTransforms extend IterablePolyTransforms?
| def sortedSets(xs: strawman.collection.immutable.SortedSet[Int]): Unit = { | ||
| val xs1 = xs.map((x: Int) => x.toString) | ||
| val xs2: SortedSet[String] = xs1 | ||
| val xs3: strawman.collection.immutable.Set[String] = xs.map((x: Int) => x.toString) |
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 wrong. Scala 2.12 should be able to perform overload resolution correctly without a type annotation.
| | ||
| /** Base trait for set operations */ | ||
| trait SetLike[A, +C[X] <: Set[X]] | ||
| extends IterableLike[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.
Coming back to my argument for StableIterable from the other PR, we also inherit methods like take and drop from Iterable which only make sense with a stable ordering. Should we move them down to StableIterable? Or is this not important enough in practice and we should simply allow it for non-stable orderings, too (like in 2.12 and the current strawman design)?
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 like this idea.
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 created issue #30 to keep track of this idea.
f33c2a7 to dffb444 Compare | Rebased to master and added |
| | ||
| def mapOps(xs: Map[Int, String]): Unit = { | ||
| val xs1 = xs.map((k, v) => (v, k)) | ||
| val xs2: strawman.collection.Map[String, Int] = xs1 |
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 example demonstrates that calling map with a function that returns a tuple returns a Map.
| val xs1 = xs.map((k, v) => (v, k)) | ||
| val xs2: strawman.collection.Map[String, Int] = xs1 | ||
| val xs3 = xs.map(kv => (kv._2, kv._1)) | ||
| val xs4: strawman.collection.Iterable[(String, Int)] = xs3 |
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 here the distinction between the map overload that returns a Map and the one that returns an Iterable is based on the arity of the supplied function.
This is because map method of Map is defined as follows:
def map[K2, V2](f: (K, V) => (K2, V2)): C[K2, V2]We could alternatively define it as follows:
def map[K2, V2](f: ((K, V)) => (K2, V2)): C[K2, V2]Note that here, f takes one parameter, which is a tuple, where in the first snippet, it takes two parameters.
I’m not sure which solution is better… I think both can work and make sense.
With the binary function I like the ability to just write kvs.map((k, v) => …) without having to write { case (k, v) => … }. But note that dotty already makes it possible to write kvs.map((k, v) => …) even though f would have type ((K, V)) => (K2, V2). Maybe we could put this feature into scalac too…
Not sure I’m clear… Do you have any strong preference and why?
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.
The binary function is potentially more efficient because it avoids tupling. Does the untupling in Dotty also work for overloaded functions? I thought it was done during type adaptation after overload resolution. If we can get scala/scala#5698 into Scala to make the case-syntax work here we will need a Function1 version but I still think we should have the Function2 version in addition.
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.
Dotty can compile this PR as-is. It would also have no probblem with the Function1 version. In fact, it would let you transparently use binary closures such as "(k, v) => ..." as unary function arguments, expanding them to { case (k, v) => ... ". So as far as dotty is concerned the Function1 version is better since it can make the Function2 variant redundant.
| val xs3 = xs.map(kv => kv._1) | ||
| val xs4: immutable.Iterable[String] = xs3 | ||
| val xs5 = xs.map((k: String, v: Int) => (v, k)) | ||
| val xs6: immutable.SortedMap[Int, String] = xs5 |
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 here we have to explicit the types of the parameters k and v. I think this type inference issue is already fixed by scala/scala#5708.
| class Foo | ||
| // 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 |
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.
The commented line fails as expected because there is no implicit ordering for Foo. (Note that, alternatively, the overloading resolution could have chosen to use the map method of Map, which does not require an additional Ordering, but that’s not the case and I think that’s better like that)
However, if we upcast the OrderedMap to a simple Map, then the overload that is picked is the one that requires no Ordering and returns a Map.
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've been thinking about Rex's CanBuildFrom idea and there are a few things I'd like to try here. I think it may be possible to find a solution that is close to CanBuild without the implementation complexity of CanBuildFrom, that can provide a fallback to the unconstrained collection type when the evidence is not available.
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 that specific case, though, I would expect a failure if the evidence is not available. When do you think you could push your experiment? For reference, I tried something here but it’s probably different from what you are thinking about: https://github.com/scala/collection-strawman/compare/poly-transforms#diff-e325090939d0c05b24284230016c77e7R189
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 pattern works when classes don't have type parameters of their own:
trait Foo; trait Bar; trait TypeClass[C] {} class Super { def foo[A](f: () => A): Foo = ??? } class Sub extends Super { def foo[A: TypeClass](f: () => A): Bar = ??? }If that pattern would work for collections it would be adequate to handle things like "this has to be a Tuple2 to stay a Map". Right now you get "missing parameter type" on (new Sub[String]).foo(_ + ".") if you give the classes type parameters. There's no real reason that has to be the case, though, AFAICT.
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.
Anyway, that would be my proposal for the "less complex than CanBuildFrom", @szeiger. As long as the pattern works we can put in whatever, but I think a simple witness that the arguments are the correct type will be sufficient for most use cases (i.e. not replacing breakOut, but everything else).
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.
The strawman LGTM so far. I propose let's go ahead and flesh it out. I did not quite get the simplified CBF idea. The example by @Ichoran looks good, but it seems to be exactly what is done now. After all, TreeSet's map
def map[B](f: (A) => B)(implicit ordering: Ordering[B]): TreeSet[B] = ??? is exactly the same as
def map[B: Ordering](f: (A) => B): TreeSet[B] = ??? which looks like the pattern that @Ichoran was proposing. Or am I missing something here?
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.
Let's go ahead with this version. I'll start working in my idea now, but it's only a generalization of what we're already doing, so it doesn't have to hold up any work on this PR.
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.
@odersky - The type inference doesn't currently work the way it probably ought to. It picks the most specific version and runs with it, then gives you an error if it doesn't work. So, in my toy example,
trait Foo; trait Bar; trait TypeClass[C] {} class Super[+A] { def foo[B](f: A => B): Foo = ??? } class Sub[+A] extends Super[A] { def foo[B: TypeClass](f: A => B): Bar = ??? }you can't actually get a Foo when your type is Sub unless you ask for it:
scala> val s = new Sub[String] s: Sub[String] = Sub@22495dd1 scala> s.foo(_ => "") <console>:14: error: could not find implicit value for evidence parameter of type TypeClass[String] s.foo(_ => "") ^ scala> s.foo(_ => ""): Foo scala.NotImplementedError: an implementation is missingMaybe this is what we want to avoid accidentally selecting the wrong types, but it doesn't feel like TreeMap is a Map if you can't throw whichever function you want at 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.
@Ichoran Another solution consists in pre-upcasting to Super: (s: Super[String]).foo(_ => "").
In my opinion it is fine to get a failure if I want to map a TreeSet into something that has no Ordering instance, or if I try to map an Array into something that has no ClassTag available. I prefer to have to explicitly upcast my TreeSet into a Set.
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.
When type signatures are complex, explicit upcasting is a large burden.
Another solution is to add an asSuper method that does nothing but the upcast. (E.g. for TreeMap it would be asMap.)
33779fc to 1c137f5 Compare | This PR is ready for review :) If you approve the design, I’ll start replacing the |
| val xs1 = xs.map((k, v) => (v, k)) | ||
| val xs2: strawman.collection.Map[String, Int] = xs1 | ||
| val xs3 = xs.map(kv => (kv._2, kv._1)) | ||
| val xs4: strawman.collection.Iterable[(String, Int)] = xs3 |
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.
Dotty can compile this PR as-is. It would also have no probblem with the Function1 version. In fact, it would let you transparently use binary closures such as "(k, v) => ..." as unary function arguments, expanding them to { case (k, v) => ... ". So as far as dotty is concerned the Function1 version is better since it can make the Function2 variant redundant.
| class Foo | ||
| // 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 |
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.
The strawman LGTM so far. I propose let's go ahead and flesh it out. I did not quite get the simplified CBF idea. The example by @Ichoran looks good, but it seems to be exactly what is done now. After all, TreeSet's map
def map[B](f: (A) => B)(implicit ordering: Ordering[B]): TreeSet[B] = ??? is exactly the same as
def map[B: Ordering](f: (A) => B): TreeSet[B] = ??? which looks like the pattern that @Ichoran was proposing. Or am I missing something here?
| I haven't formally reviewed it but it looks promising enough to me so that with Stefan's blessing I'm happy moving ahead. The type inference tweaks I'm suggesting wouldn't change the overall strategy much, just deliver a different user experience, so that's no reason to wait or change designs. |
I sketched the integration of
Set,TreeSet,MapandTreeMapcollections. They were challenging becauseMaptakes two type parameters where previous collections were only taking one type parameter, and also becauseTreeSetandTreeMaptake an additional implicitOrderingparameter.This is an alternative to #23 that makes
SetextendIterableandSortedSethave two overloads ofmap(one has an additional implicitOrderingparameter).