Reputation: 12356
Applying @tailrec I'm getting error from scala compiler: "could not optimize @tailrec annotated method get: it contains a recursive call targetting a supertype case _ => tail.get(n-1)". Could someone explain why is that?
trait List[T] {
def isEmpty: Boolean
def head: T
def tail: List[T]
def get(n: Int): T
}
class Cons[T](val head: T, val tail: List[T]) extends List[T]{
def isEmpty = false
@tailrec
final def get(n: Int) =
n match {
case 0 => head
case _ => tail.get(n-1)
}
}
class Nil[T] extends List[T]{
def isEmpty = true
def head = throw new NoSuchElementException("Nil.head")
def tail = throw new NoSuchElementException("Nil.tail")
final def get(n: Int): T = throw new IndexOutOfBoundsException
}
object Main extends App{
println(new Cons(4, new Cons(7, new Cons(13, new Nil))).get(3))
}
Upvotes: 4
Views: 451
Reputation: 32719
Ben and Jean-Phillipe Pellet have already explained why the compiler complains. As for how to fix it, there is a straightforward solution: move the implementation of get
right into List
:
trait List[T] {
def isEmpty: Boolean
def head: T
def tail: List[T]
@tailrec
final def get(n: Int): T = {
n match {
case 0 => head
case _ => tail.get(n-1)
}
}
}
class Cons[T](val head: T, val tail: List[T]) extends List[T]{
def isEmpty = false
}
class Nil[T] extends List[T]{
def isEmpty = true
def head = throw new NoSuchElementException("Nil.head")
def tail = throw new NoSuchElementException("Nil.tail")
}
Upvotes: 2
Reputation: 59994
Try to imagine what's happening here and what you're asking the compiler to do. Tail call optimization, roughly, turns the method call into a loop, taking the arguments of the method and turning them into variables that are reassigned at each iteration of the loop.
Here, there are two such “loop variables”: n
and the list cell itself on which the get
method is called, which is actually this
in the method body. The next value for n
is fine: it's n - 1
and also an Int
. The next value for the list cell, which is tail
, is a problem, however: this
has type Cons[T]
, but tail
only has type List[T]
.
Thus, there is no way the compiler can turn it into a loop, as there is no guarantee that tail
is a Cons[T]
— and sure enough, at the end of the list, it is a Nil
.
One way to “fix” it would be:
case class Cons[T](val head: T, val tail: List[T]) extends List[T] {
def isEmpty = false
@tailrec
final def get(n: Int) =
n match {
case 0 => head
case _ => tail match {
case c @ Cons(_, _) => c.get(n - 1)
case nil @ Nil() => nil.get(n - 1)
}
}
}
(Works if both Cons
and Nil
are case classes — but you'll probably want to make Nil
a case object
and List[T]
covariant in T
.)
Upvotes: 6
Reputation: 71400
In Cons.get
you call tail.get
as your tail call. But tail
is of type List[T]
, not Cons[T]
. So that call won't necessarily be handled by Cons.get
, and Scala can't apply the tail recursion optimisation; the optimisation would turn the method call into a local jump back to the start of Cons.get
, but that's not necessarily where the call is going.
Upvotes: 2