scala / bug

Scala 2 bug reports only. Please, no questions — proper bug reports only.
https://scala-lang.org
230 stars 21 forks source link

The same function is described differently in REPL depending on how it was created? #12747

Closed atrigent closed 1 year ago

atrigent commented 1 year ago

Reproduction steps

Scala version: 2.13.10

Given:

trait MyTrait[T] {
  def method(v: T): Set[T]
}

object MyIntObj extends MyTrait[Int] {
  def method(v: Int): Set[Int] = Set(v)
}

object MyStrObj extends MyTrait[String] {
  def method(v: String): Set[String] = Set(v)
}

val objs = Seq(MyIntObj, MyStrObj)

Problem

In REPL:

scala> objs(0).method _
val res0: _1 => scala.collection.immutable.Set[_1] forSome { type _1 >: String with Int } = $Lambda$4707/204160032@4649d613

scala> objs(0).method(_)
val res1: String with Int => scala.collection.immutable.Set[_ >: String with Int] = $Lambda$4717/1170655249@17906677

I admittedly don't know for sure whether this is a bug or not. My intuition says that these should be the same function, and my understanding is that these are just two equivalent ways of describing a function type involving existentials, but maybe there's some subtle difference I can't see.

som-snytt commented 1 year ago

just two equivalent ways

This is a misunderstanding. Using https://github.com/scala/scala/pull/10309 with -Xlint:eta-impure:

scala> objs.head.method _
<console>:13: warning: impure expression as part of an eta-expansion. The expression is evaluated eagerly, before the function is created, not when the function is evaluated.
       objs.head.method _
            ^
<console>:13: warning: inferred existential type _1 => scala.collection.immutable.Set[_1] forSome { type _1 >: String with Int }, which cannot be expressed by wildcards,  should be enabled
by making the implicit value scala.language.existentials visible.
This can be achieved by adding the import clause 'import scala.language.existentials'
or by setting the compiler option -language:existentials.
See the Scaladoc for value scala.language.existentials for a discussion
why the feature should be explicitly enabled.
       objs.head.method _
                        ^
res0: _1 => scala.collection.immutable.Set[_1] forSome { type _1 >: String with Int } = $Lambda$2123/0x00000008015c3990@5f6a8efe

scala> objs.head.method(_)
res1: String with Int => scala.collection.immutable.Set[_ >: String with Int] = $Lambda$2140/0x00000008015c75e8@407f2029

scala>

Using -Vprint:typer,

        private[this] val res0: _1 => scala.collection.immutable.Set[_1] forSome { type _1 >: String with Int } = {
          <synthetic> val eta$0$1: MyTrait[_1] = $line6.$read.INSTANCE.$iw.$iw.objs.head;
          ((v: _1) => eta$0$1.method(v))
        };
        <stable> <accessor> def res0: _1 => scala.collection.immutable.Set[_1] forSome { type _1 >: String with Int } = $iw.this.res0

The subexpression is evaluated first when eta-expanded, in the first example.

In the second case, the placeholder syntax is "just syntax" for x => objs.head.method(x).

Obviously (so to speak), the semantics are entirely different.

I hope Lukas is inspired to get something like this lint merged.

Seth may intervene with, "Please ask questions on the forum and reserve the bug tracker for when you're sure it's a bug."

You did ask, and I was about to answer but was distracted.

Exactly how the REPL reports the type is the topic at https://github.com/lampepfl/dotty/issues/17032 where I ask for the more informative representation.

I think you're asking, Can't it just dummy it down a bit since we know it's really something like Int => Set[Int] or like dotty says

scala> objs.head.method _
val res0: Int & String => Set[? >: Int & String <: Int | String] = Lambda$1597/0x00000008015185e8@5cc669d
atrigent commented 1 year ago

Thanks for the quick response.

I do see that doing { val tmp = objs.head; tmp.method(_) } also produces the same existential type. With -Vprint:typer (thanks for telling me about that, I'll definitely be using it more), I see:

      private[this] val res14: _1 => scala.collection.immutable.Set[_1] forSome { type _1 >: String with Int } = {
        val tmp: MyTrait[_ >: String with Int] = $read.this.$line6$read.$iw.objs.head;
        ((x$1: _1) => tmp.method(x$1))
      };

...which is pretty similar to what you get with objs.head.method _. But, I'm still not quite getting why the type is different. Especially since, in the above snippet, tmp is a MyTrait[_ >: String with Int]. So the type parameter is _ >: String with Int, meaning the method return type should be Set[_ >: String with Int], shouldn't it?

I also noticed that { val tmp = objs.head; tmp.method _ } throws an error?

       error: type mismatch;
        found   : _1(in value tmp)
        required: (some other)_1(in value tmp)

yet another thing I don't understand...

Also, when I said "just two equivalent ways" I was referring to the types, not the way that the function was created. According to some things I've read, it sounds like these following types might actually be equivalent. Is that wrong?

_1 => scala.collection.immutable.Set[_1] forSome { type _1 >: String with Int }
String with Int => scala.collection.immutable.Set[_ >: String with Int]
SethTisue commented 1 year ago

They don't look equivalent to me — in the first, the function's input type is existential, in the second, it isn't (it's just String with Int).

Seth may intervene with, "Please ask questions on the forum and reserve the bug tracker for when you're sure it's a bug."

indeed, in general we ask that folks not use the bug tracker for questions, but use https://users.scala-lang.org . come to the bug tracker when you believe you have a solid case that there is a previously unreported bug and not just some unexpected behavior that surprised you

There are a number of previous tickets here in the tracker about existentials and/or type inference, perhaps there are relevant ones? And perhaps there's language in the Scala spec which would help distinguish "bug" from "unexpected behavior" here.

atrigent commented 1 year ago

They don't look equivalent to me — in the first, the function's input type is existential, in the second, it isn't (it's just String with Int).

That did occur to me - the one with the existential could be read as saying that every instance of _1 is the same type (that satisfies some constraint), while the second one would be that each of those types satisfies the constant, but they aren't necessarily the same type. It is a little bit ambiguous whether the forSome is grouped just with the return type or with the entire function type.

The former interpretation does seem to be the more correct one for this function, so why doesn't it use that type when I do objs.head.method(_)?

indeed, in general we ask that folks not use the bug tracker for questions, but use https://users.scala-lang.org

Ok, I will try this next time.

There are a number of previous tickets here in the tracker about existentials and/or type inference, perhaps there are relevant ones? And perhaps there's language in the Scala spec which would help distinguish "bug" from "unexpected behavior" here.

I wouldn't really know where to start to find something like this. Does anyone have any further insight?

som-snytt commented 1 year ago

This question

The former interpretation does seem to be the more correct one for this function, so why doesn't it use that type when I do objs.head.method(_)?

suggests you re-read my previous comment. If I write xs.map(expr.f(_)) then my expr may evaluate to a wildly different value every time it is invoked. Because of subtyping, you don't know if you're calling f on a Foo or a Bar.

On using github search on scala/bug/issues, "existential" is one of the easier search terms, but usually it is a challenge. Usually it is more like a dialogue with ChatGPT.

Closing as "not a bug" because if there is a documentation issue, it is out of scope.