Skip to content

Code does not compile when using T as v instead of using v: T #23272

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

Open
OndrejSpanel opened this issue May 27, 2025 · 6 comments
Open

Code does not compile when using T as v instead of using v: T #23272

OndrejSpanel opened this issue May 27, 2025 · 6 comments

Comments

@OndrejSpanel
Copy link
Member

OndrejSpanel commented May 27, 2025

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))
  }
@OndrejSpanel OndrejSpanel added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels May 27, 2025
@som-snytt
Copy link
Contributor

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]))
  }
}

@OndrejSpanel
Copy link
Member Author

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?

@som-snytt
Copy link
Contributor

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).

@som-snytt
Copy link
Contributor

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.

@som-snytt som-snytt added itype:question area:desugar Desugaring happens after parsing but before typing, see desugar.scala area:extension-methods and removed itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label area:desugar Desugaring happens after parsing but before typing, see desugar.scala labels May 27, 2025
@OndrejSpanel
Copy link
Member Author

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?

@som-snytt
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants