scala3 icon indicating copy to clipboard operation
scala3 copied to clipboard

inconsistent behaviour of extension methods when called using dot versus function application wrt implicit conversion

Open bjornregnell opened this issue 4 years ago • 9 comments

Compiler version

3.0.2

Minimized code and output

(Sorry for not minimizing maximally, but I think it can be worth while to see a real use case...)

scala> def avg(xs: Vector[Long]): Double = xs.sum / xs.length.toDouble
def avg(xs: Vector[Long]): Double

scala> avg(Vector(1,2,3))   // as expected, number widening
val res33: Double = 2.0

scala> extension (xs: Vector[Long]) def avge: Double = xs.sum / xs.length.toDouble
def avge(xs: Vector[Long]): Double

scala> Vector(1,2,3).avge     // compiler complains
-- Error:
1 |Vector(1,2,3).avge
  |^^^^^^^^^^^^^^^^^^
  |value avge is not a member of Vector[Int].
  |An extension method was tried, but could not be fully constructed:
  |
  |    avge(Vector.apply[Int]([1,2,3 : Int]*))    failed with
  |
  |        Found:    Vector[Int]
  |        Required: Vector[Long]

scala> avge(Vector(1,2,3))    // compiler does not complain
val res34: Double = 2.0

Expectation

It is surprising that an extention method call compiles differently depending on how it is called. Should this be made consistent in the semantics? At least the above behaviour chould be explained in the lang ref doc page. https://docs.scala-lang.org/scala3/reference/contextual/extension-methods.html

bjornregnell avatar Oct 01 '21 16:10 bjornregnell

It looks impossible to fix and difficult to explain. When resolving an extension method we typecheck the prefix Vector(1, 2, 3) first, and that's a Vector[Int]. Note that a Vector[Int] is not a Vector[Long]. There's an implicit conversion from Int to Long but that does not translate to a conversion between the Vector types.

By contrast, when we typecheck avg(Vector(1,2,3)) we know the argument needs to be a Vector[Long], which means we know the elements to Vector need to be Long, which means we apply the implicit conversion from Int to Long for each element.

But what would be a good way to document this?

odersky avatar Oct 01 '21 21:10 odersky

Thanks for explanation! Yes, that makes sense to me and I too see no way to "fix" it, that wouldn't cause other troubles. But there should be a way to explain it in the docs. A combination of my example and your explanation above I think could help understanding of what happens in more cases than just this one.

An initial proposal of a way to document this for discussion of contents and placement (?):


Extension methods, type inference and conversions combined

When an extension method em is applied using method application as inexp.em(a), the compiler first typechecks the expression exp and then the arguments if any. In contrast, when an extension method is called using function application em(exp, a) the compiler typechecks all arguments in one go, including exp, and thus have more information. The latter case can make conversions between types applicable, as in the example below.

scala> extension (xs: Vector[Long]) def avg: Double = xs.sum / xs.length.toDouble
def avg(xs: Vector[Long]): Double

scala> Vector(1,2,3).avg     // this does not typecheck
-- Error:
1 |Vector(1,2,3).avg
 |^^^^^^^^^^^^^^^^^^
 |value avg is not a member of Vector[Int].
 |An extension method was tried, but could not be fully constructed:
 |
 |    avg(Vector.apply[Int]([1,2,3 : Int]*))    failed with
 |
 |        Found:    Vector[Int]
 |        Required: Vector[Long]

scala> avg(Vector(1,2,3))    // this typechecks
val res1: Double = 2.0

When resolving an extension method the compiler typechecks the prefix Vector(1, 2, 3) first, which is a Vector[Int]. Note that a Vector[Int] is not a Vector[Long]. There is an implicit conversion from Int to Long but that does not translate to a conversion between the Vector types.

In contrast, when avg(Vector(1,2,3)) is typechecked, the compiler knows that the argument needs to be a Vector[Long], which means it knows that the elements of Vector need to be Long, which means that the implicit conversion from Int to Long for each element is applied.


My suggestion is that this goes as a sub-section into the lang ref under extension methods somewhere.

bjornregnell avatar Oct 02 '21 13:10 bjornregnell

@bjornregnell I think this looks good. Do you want to make a PR for this?

odersky avatar Oct 04 '21 10:10 odersky

Thanks. Yes, I can make a PR. I won't be able to attend the issue spree tomorrow, but I can squeeze in this PR within a couple of days.

bjornregnell avatar Oct 04 '21 10:10 bjornregnell

Thanks, Bjorn!

On Mon, Oct 4, 2021 at 12:48 PM Bjorn Regnell @.***> wrote:

Thanks. Yes, I can make a PR. I won't be able to attend the issue spree tomorrow, but I can squeeze in this PR within a couple of days.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or unsubscribe. Triage notifications on the go with GitHub Mobile for iOS or Android.

--

Martin Odersky Professor, Programming Methods Group (LAMP) Faculty IC, EPFL Station 14, Lausanne, Switzerland

odersky avatar Oct 04 '21 11:10 odersky

@odersky Finally got around to this; now here: https://github.com/lampepfl/dotty/pull/13724

bjornregnell avatar Oct 10 '21 15:10 bjornregnell

Fixed in https://github.com/lampepfl/dotty/pull/13724

soronpo avatar Sep 09 '22 10:09 soronpo

I did not look, but the linked PR is closed not merged.

som-snytt avatar Sep 09 '22 16:09 som-snytt

Oh, my mistake.

soronpo avatar Sep 09 '22 17:09 soronpo