Skip to content
This repository was archived by the owner on Dec 22, 2021. It is now read-only.

Don't extend collection types in ArrayOps #479

Merged
merged 4 commits into from
Feb 27, 2018

Conversation

szeiger
Copy link
Contributor

@szeiger szeiger commented Feb 21, 2018

  • more ArrayOps methods and optimizations

See commit comments for details.

All collection-related methods for arrays are available on WrappedArray
so there is no need to duplicate them in ArrayOps. This allows ArrayOps
to be implemented as a value class with no inherited methods, thus it
should never be necessary to allocate an instance of it.

Unlike in the old collection library `ArrayOps` is not specialized. The
methods that were specialized before don’t seen worth specializing. In
particular, `apply` and `update` which *should* be specialized on arrays
are not provided at all by the new `ArrayOps` because they are available
natively on `Array`. The old specialization scheme would have no benefit
but a high cost: it is incompatible with a pure value class. Almost any
ArrayOps call in the old library needs to allocate an instance, with the
only exceptions being `length`, `apply` and `update`.

New and optimized implementations of many collection methods are
provided in ArrayOps, often adapted from Slick’s `ConstArray` class.
- `WrappedArray` and `ImmutableArray` can implement it with
  `Array.copy`.

- In `ArrayBuilder.addAll` we do not need to take specialization of
  `ImmutableArray` into account. The old implementation used
  `Array.copy` which falls back to a slow copy when the element types
  don’t match. This is still faster than the primitive implementation in
  `super.addAll` and `Iterator.copyToArray` as the last resort at least
  should not be worse. The new `addAll` based on `copyToArray` works
  well with all collection types that implement `copyToArray`
  efficiently.

- `prependedAll` and `appendedAll` in both, `ImmutableArray` and
  `ArrayOps` also do not need to optimize for known `ImmutableArray`
  arguments anymore.
@lrytz
Copy link
Member

lrytz commented Feb 22, 2018

Nice! What are your thoughts for keeping the "interface" (available methods) in synch? #465

@julienrf
Copy link
Contributor

This PR makes type inference fail on arrays usage. Also, what do you think of implementing the optimized versions of reduction operations on WrappedArray instead (which inherits from IterableOps)?

@szeiger
Copy link
Contributor Author

szeiger commented Feb 22, 2018

There's one type inference failure in the tests. I have not been able to figure out why this change makes it fail. The example we have is something like Array("") ++ Array("", null). ArrayOps should have exactly the same methods as before (i.e. a single ++ method that takes a ClassTag; the old implementation had another without the ClassTag which was defined in a superclass and therefore shadowed) but the failure is new. Note that the general problem already existed before this change, we just didn't have any test for it: Iterable("") ++ Array("", null) fails in the same way even though it does not involve ArrayOps.

@szeiger
Copy link
Contributor Author

szeiger commented Feb 22, 2018

We could additionally make WrappedArray use more optimized implementations but the primary place for them should be ArrayOps because it is a pure value class. Because of the specialization WrappedArray always needs to allocate an instance (unless you implement a method in all of the specialized subclasses, which was probably the reason for the duplicate apply and update implementations in the old ArrayOps).

@szeiger
Copy link
Contributor Author

szeiger commented Feb 22, 2018

Something weird is going on with the Array.apply inference problem. The good news is that it works when we're using Predef and the scala namespace:

[info] Running scala.tools.nsc.MainGenericRunner -usejavacp
Welcome to Scala 2.13.0-20180220-202434-37b204e (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_102).
Type in expressions for evaluation. Or try :help.

scala> Array("") ++ Array("", null)
res0: Array[String] = Array("", "", null)

scala> Iterable("") ++ Array("", null)
res1: Iterable[String] = List("", "", null)

The problem only occurs when testing in the strawman namespace. My guess would be that some special case handling in the compiler is getting in the way. For example, you can see in -Ytyper-debug output that arrayToWrappedArray is always considered as an implicit conversion even when it is not in scope and not imported.

@szeiger
Copy link
Contributor Author

szeiger commented Feb 22, 2018

Ticket for the overload problem: scala/bug#10746

The reason why it works from Predef is that we still have the separate implicits for converting primitive and reference arrays to WrappedArray, whereas in strawman there is only a single conversion method.

@szeiger szeiger force-pushed the wip/collection-less-arrayops branch from 2295fd1 to f5041c6 Compare February 22, 2018 16:44
@szeiger szeiger force-pushed the wip/collection-less-arrayops branch from f5041c6 to 3560a69 Compare February 22, 2018 17:30

private def mkArray(size: Int): Array[T] = {
val newelems = new Array[T](size)
// Anything short of this will make Dotty create an Array[Object]:
val newelems = java.lang.reflect.Array.newInstance(ct.runtimeClass, size).asInstanceOf[Array[T]]
Copy link
Contributor Author

@szeiger szeiger Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a strange one. I was unable to come up with a reproduction in the REPL but it fails consistently here. When calling ArrayOps.zip the ClassTag is the one for Tuple2 but I end up with an Array[Object] when I use new Array or Array.ofDim (even passing the ClassTag explicitly).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/** A copy of this array with an element appended. */
def appended[B >: A : ClassTag](x: B): Array[B] = {
val dest = new Array[B](xs.length + 1)
Array.copy(xs, 0, dest, 0, xs.length)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mkeskells suggests using Arrays.copyOf(xs, xs.length+1, classTag[B]) (scala/scala#6105 (comment))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. We can also optimize all slice-based operations with copyOfRange.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's possible to optimize for the case where B is A, could we have an overload that avoids materializing the ClassTag?

@szeiger szeiger merged commit 18c2481 into master Feb 27, 2018
@szeiger szeiger deleted the wip/collection-less-arrayops branch February 27, 2018 14:22
@julienrf julienrf added this to the 0.10.0 milestone Feb 27, 2018
@julienrf julienrf mentioned this pull request Mar 29, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants