Search code examples
scalatypestype-inferencerefinement-type

Anonymous subclasses with extra methods -- scala2 vs. scala3


The following code works as (I) expect in scala2 but generates a compile-time error in scala3. I tried to find an explanation in Programming in Scala, Fifth Edition and the web but failed. Any insights?

object Main {
    def main(args:Array[String]): Unit = {

        val foo = new Foo(7)
        println(foo.double)             // 14

        val bar = new Foo(8) {
            def triple:Int = this.i * 3
        }

        println(bar.double)             // 16
        println(bar.triple)             // 24 in scala2; compile error in scala3
    }
}

class Foo(val i:Int) {
    def double:Int = i*2
}

Compilation error:

-- [E008] Not Found Error: /tmp/example.scala:12:20 ----
12 |        println(bar.triple)
   |                ^^^^^^^^^^
   |                value triple is not a member of Foo

Solution

  • If you compile your code with scala -Xprint:typer , you'll see that val bar now explicitly has type Foo in Scala 3:

        def main(args: Array[String]): Unit = 
          {
            val foo: Foo = new Foo(7)
            println(foo.double)
            val bar: Foo = 
              {
                final class $anon() extends Foo(8) {
                  def triple: Int = this.i.*(3)
                }
                new $anon():Foo
              }
            println(bar.double)
            println(bar.triple)
          }
    

    Contrast this to Scala 2, where bar gets the refinement type Foo { def triple: Int }:

          val bar: Foo{def triple: Int} = {
            final class $anon extends Foo {
              def <init>(): <$anon: Foo> = {
                $anon.super.<init>(8);
                ()
              };
              def triple: Int = this.i.*(3)
            };
            new $anon()
          };
    

    So, it's

    val bar: Foo                   // In Scala 3
    val bar: Foo{def triple: Int}  // In Scala 2
    

    To achieve the behavior of Scala 2, you could introduce a new class Bar explicitly:

            class Bar extends Foo(8) {
              def triple: Int = this.i * 3
            }
            val bar = new Bar
    

    Full compilable code:

    object Main {
        def main(args:Array[String]): Unit = {
    
            val foo = new Foo(7)
            println(foo.double)             // 14
    
            class Bar extends Foo(8) {
              def triple: Int = this.i * 3
            }
            val bar = new Bar
    
            println(bar.double)             // 16
            println(bar.triple)             // 24 in scala2; compile error in scala3
        }
    }
    
    class Foo(val i:Int) {
        def double:Int = i*2
    }
    

    As for the reason why it's inferring Foo instead of Foo { def triple: Int } - I'm not exactly sure, I guess they just figured out eventually that this behavior plays better with other language features.

    How would you explain that val x = "hello" infers x: String and not x: "hello"?