-
Notifications
You must be signed in to change notification settings - Fork 72
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 missing
Maybe 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
,Map
andTreeMap
collections. They were challenging becauseMap
takes two type parameters where previous collections were only taking one type parameter, and also becauseTreeSet
andTreeMap
take an additional implicitOrdering
parameter.This is an alternative to #23 that makes
Set
extendIterable
andSortedSet
have two overloads ofmap
(one has an additional implicitOrdering
parameter).