Code does not compile when using `T as v` instead of `using v: T`
Compiler version
3.6.0, 3.7.0, 3.7.1-RC2
Minimized code
//> using scala 3.7.0
trait Vectoric[V] {
def components: Array[V => Double]
def map(a: V)(f: Double => Double): V
}
trait VectoricOps {
extension [V: Vectoric as v](lhs: V) {
def map(f: Double => Double): V = v.map(lhs)(f)
def toArray: Array[Double] = v.components.map(c => c(lhs))
}
}
Output
[error] .\Main.scala:11:56
[error] parameter c does not take parameters
[error] def toArray: Array[Double] = v.components.map(c => c(lhs))
[error] ^
[error] .\Main.scala:11:63
[error] No given instance of type Vectoric[Array[V => Double]] was found for parameter v of method map in trait VectoricOps
[error] def toArray: Array[Double] = v.components.map(c => c(lhs))
[error]
Expectation
The code should compile. The same code compiles when using extension [V](lhs: V)(using v: Vectoric[V])
Maybe my expectation is wrong and the two forms are not equivalent, in which can I would be glad if anyone can explain the difference.
Note
There is no error when I remove the map method from VectoricOps extension.
The same error is produced when using:
extension [V: Vectoric](lhs: V) {
def map(f: Double => Double): V = ???
def toArray: Array[Double] = summon[Vectoric[V]].components.map(c => c(lhs))
}
Comparing the unexpected vs expected, the "using as" is in the wrong place for desugared map.
package <empty> {
trait Vectoric[V >: Nothing <: Any]() extends Object {
V
def components: Array[V => Double]
def map(a: Vectoric.this.V)(f: Double => Double): Vectoric.this.V
}
trait VectoricOps() extends Object {
extension [V >: Nothing <: Any](lhs: V) def map(f: Double => Double)(using
v: Vectoric[V]): V = v.map(lhs)(f)
extension [V >: Nothing <: Any](lhs: V)(using v: Vectoric[V]) def toArray:
Array[Double] =
this.map[Array[V => Double]](v.components)((c: Double) => c(lhs))(
/* missing */summon[Vectoric[Array[V => Double]]])
}
trait VectoricOps2() extends Object {
extension [V >: Nothing <: Any](lhs: V)(using v: Vectoric[V]) def map(
f: Double => Double): V = v.map(lhs)(f)
extension [V >: Nothing <: Any](lhs: V)(using v: Vectoric[V]) def toArray:
Array[Double] =
refArrayOps[V => Double](v.components).map[Double]((c: V => Double) =>
c.apply(lhs))(scala.reflect.ClassTag.apply[Double](classOf[Double]))
}
}
Does this mean this is a compiler bug, a spec bug (is the desugaring of context bounds for extensions even specced?), or is my code wrong?
That is a good question. It is specified here but doesn't mention collective extensions.
Either it's obvious that "end" means the end of the "collective" signature, or it's obvious that extensions are regular methods and "end" means the regular end of the signature.
An ugly workaround is
extension [V: Vectoric as v](lhs: V)(using v.type) {
def map(f: Double => Double): V = v.map(lhs)(f)
def toArray: Array[Double] = v.components.map(c => c(lhs))
}
My guess would be that this is "as specified", since similar requests to make collective extension do more work, such as introducing a scope, were rejected.
The problem with the code is that Array[Double] has no map member, so the call in toArray is wired to the nearby extension (instead of converting the array as expected).
Possibly one could concoct a more compelling use case to change desugaring of context bounds. I just ran out of steam (or coffee), so I won't attempt that right now. My intuition was the same as yours (the first obvious interpretation above) but it may be easier to reason about the current behavior (the second obvious interpretation).
Worth adding, maybe there was previously a strong reason to collect all implicit parameters at the very end of a signature, and to avoid interleaving, for reasons of usability. I don't know if that is currently the case.
What surprises me: when
extension [V >: Nothing <: Any](lhs: V) def map(f: Double => Double)(using v: Vectoric[V]): V
is tried unsuccessfully, why the search stops here and the .map from ArrayOps is not tried? How is having
extension [V >: Nothing <: Any](lhs: V)(using v: Vectoric[V]) def map(f: Double => Double): V
better?
I think the enigmatic
An extension method was tried, but could not be fully constructed
means that it hasn't chosen yet.
The doc says first it tries m(x) and then it consults implicit scope (much like implicit views in Scala 2), and conversions are checked in the second step.
https://dotty.epfl.ch/docs/reference/contextual/extension-methods.html#translation-of-calls-to-extension-methods
Apparently, "constructed" means that m(x) typechecks, which includes the subsequent implicit parameter list. It doesn't matter whether it was written after the def m, only the order of param lists matters.
extension (s: String) def f(using T) = s.reverse // "hi".f not constructed
extension (s: String) def f(i: Int)(using T) = s.reverse
"hi".f(42) // no given instance
To answer the question, having the implicit param list after the leading explicit param lets it "fail to construct" (when the implicit is missing) and then try the second step, which is extensions from implicit scope and also implicit conversions. The wrapper for Array is still an "old-style" implicit conversion.
As @som-snytt has shown, the expansion is different for the as vs explicit using. Why that makes a difference in the final typing would require more investigation. But anyway, having the same method name as a member and an extension is a minefield.