Scala Programming Language
  1. Scala Programming Language
  2. SI-7200

Improperly inferred return type for recursive calls in abstract method implementation

    Details

    • Type: Bug Bug
    • Status: Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: Scala 2.10.0
    • Fix Version/s: Scala 2.10.1
    • Component/s: Type Inference
    • Labels:
      None

      Description

      Snippet below fails with ClassCastException at runtime.

      import language.higherKinds
      
      object FooExample extends App {
      
        // Slice of comonad is where this came up
        trait Foo[F[_]] {
          def coflatMap[A, B](f: F[A] => B): F[A] => F[B]
        }
      
        // A non-empty list
        case class Nel[A](head: A, tail: List[A])
      
        object NelFoo extends Foo[Nel] {
          
          // It appears that the return type for recursive calls is not inferred
          // properly, yet no warning is issued. Providing a return type or
          // type arguments for the recursive call fixes the problem.
          
          def coflatMap[A, B](f: Nel[A] => B) = // ok w/ return type
            l => Nel(f(l), l.tail match {
              case Nil => Nil
              case h :: t => {
                val r = coflatMap(f)(Nel(h, t)) // ok w/ type args
                r.head :: r.tail
              }
            })
        }
      
        // Without a recursive call all is well, but with recursion we get a 
        // ClassCastException from Integer to Nothing
        NelFoo.coflatMap[Int, Int](_.head + 1)(Nel(1, Nil)) // Ok
        NelFoo.coflatMap[Int, Int](_.head + 1)(Nel(1, List(2))) // CCE
      
      }
      

        Activity

        Hide
        Jason Zaugg added a comment -

        This bug has gone away in 2.10.1.

        https://github.com/retronym/scala/compare/scala:2.10.x...retronym:ticket/7200

        Broken in 2.9.2 and 2.10.0, but working in 2.10.1

        --- sandbox/2.10.0.log
        +++ sandbox/2.10.1.log
               def coflatMap[A >: Nothing <: Any, B >: Nothing <: Any](f: Test.Nel[A] => B): Test.Nel[A] => Test.Nel[B] = ((l: Test.Nel[A]) => Test.this.Nel.apply[B](f.apply(l), l.tail match {
                 case immutable.this.Nil => immutable.this.Nil
        -        case (hd: A, tl: List[A])scala.collection.immutable.::[A]((h @ _), (t @ _)) => {
        -          val r: Test.Nel[Nothing] = NelFoo.this.coflatMap[A, Nothing](f).apply(Test.this.Nel.apply[A](h, t));
        +        case (hd: A, tl: List[A])scala.collection.immutable.::[?A1]((h @ _), (t @ _)) => {
        +          val r: Test.Nel[B] = NelFoo.this.coflatMap[A, B](f).apply(Test.this.Nel.apply[A](h, t));
                   {
        -            <synthetic> val x$1: Nothing = r.head;
        -            r.tail.::[Nothing](x$1)
        +            <synthetic> val x$1: B = r.head;
        +            r.tail.::[B](x$1)
                   }
                 }
               }))
        

        b74c33e represents the exact moment of progression. We should
        investigate that a little further so we can fully connect the dots.

        But that diff is a bit troublesome: what it the TypeVar ?A1 doing in the post-typer tree? I will pull on that thread to see what I can find.

        I chatted to Lukas today about his patch to Namers that (ostensibly) fixed this, and it seems to me that our current scheme for allowing recursive methods to use inherited inferred return types is quite risky.

        scala> trait A { def foo: Any }; object B extends A { def foo = {if(false) foo; "" } }; B.foo
        defined trait A
        defined module B
        res0: String = ""
        

        During typechecking of `B#foo`, the method has the return type Any, which is later refined to String.
        We can see this by trying to ascribe the more specific type:

        scala> trait A { def foo: Any }; object B extends A { def foo = {if(false) foo: String; "" } }; B.foo
        <console>:9: error: type mismatch;
         found   : Any
         required: String
                      trait A { def foo: Any }; object B extends A { def foo = {if(false) foo: String; "" } };;
                                                                                          ^
        
        Show
        Jason Zaugg added a comment - This bug has gone away in 2.10.1. https://github.com/retronym/scala/compare/scala:2.10.x...retronym:ticket/7200 Broken in 2.9.2 and 2.10.0, but working in 2.10.1 --- sandbox/2.10.0.log +++ sandbox/2.10.1.log def coflatMap[A >: Nothing <: Any, B >: Nothing <: Any](f: Test.Nel[A] => B): Test.Nel[A] => Test.Nel[B] = ((l: Test.Nel[A]) => Test.this.Nel.apply[B](f.apply(l), l.tail match { case immutable.this.Nil => immutable.this.Nil - case (hd: A, tl: List[A])scala.collection.immutable.::[A]((h @ _), (t @ _)) => { - val r: Test.Nel[Nothing] = NelFoo.this.coflatMap[A, Nothing](f).apply(Test.this.Nel.apply[A](h, t)); + case (hd: A, tl: List[A])scala.collection.immutable.::[?A1]((h @ _), (t @ _)) => { + val r: Test.Nel[B] = NelFoo.this.coflatMap[A, B](f).apply(Test.this.Nel.apply[A](h, t)); { - <synthetic> val x$1: Nothing = r.head; - r.tail.::[Nothing](x$1) + <synthetic> val x$1: B = r.head; + r.tail.::[B](x$1) } } })) b74c33e represents the exact moment of progression. We should investigate that a little further so we can fully connect the dots. But that diff is a bit troublesome: what it the TypeVar ?A1 doing in the post-typer tree? I will pull on that thread to see what I can find. I chatted to Lukas today about his patch to Namers that (ostensibly) fixed this, and it seems to me that our current scheme for allowing recursive methods to use inherited inferred return types is quite risky. scala> trait A { def foo: Any }; object B extends A { def foo = {if(false) foo; "" } }; B.foo defined trait A defined module B res0: String = "" During typechecking of `B#foo`, the method has the return type Any , which is later refined to String . We can see this by trying to ascribe the more specific type: scala> trait A { def foo: Any }; object B extends A { def foo = {if(false) foo: String; "" } }; B.foo <console>:9: error: type mismatch; found : Any required: String trait A { def foo: Any }; object B extends A { def foo = {if(false) foo: String; "" } };; ^
        Hide
        Jason Zaugg added a comment -

        Marking as fixed in 2.10.1; test case and analysis here:

        https://github.com/scala/scala/pull/2322

        Show
        Jason Zaugg added a comment - Marking as fixed in 2.10.1; test case and analysis here: https://github.com/scala/scala/pull/2322

          People

          • Assignee:
            Jason Zaugg
            Reporter:
            Rob Norris
          • Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved:

              Development