This is because return in your first fragment is non-local (it is nested inside lambda). Scala uses exceptions to compile non-local return , so the code is converted by the compiler from this:
@annotation.tailrec def listSize(l : Seq[Any], s: Int = 0): Int = { if (l.isEmpty) { None.getOrElse( return s ) } listSize(l.tail, s + 1) }
Something similar to this (compile with scalac -Xprint:tailcalls ):
def listSize2(l : Seq[Any], s: Int = 0): Int = { val key = new Object try { if (l.isEmpty) { None.getOrElse { throw new scala.runtime.NonLocalReturnControl(key, 0) } } listSize2(l.tail, s + 1) } catch { case e: scala.runtime.NonLocalReturnControl[Int @unchecked] => if (e.key == key) e.value else throw e } }
The latter indicates that recursive calls are not tail calls when they are wrapped in try / catch blocks. Basically, this example:
def self(a: Int): Int = { try { self(a) } catch { case e: Exception => 0 } }
This is related, which is obviously not tail recursive:
def self(a: Int): Int = { if (self(a)) { // ... } else { // ... } }
There are certain special cases where you can optimize this (up to two stack frames, if not one), but there does not seem to be a universal rule to apply to this situation.
In addition, the return expression in this fragment is not a non-local return , so the function can be optimized:
@annotation.tailrec def listSize(l : Seq[Any], s: Int = 0): Int = { if (l.isEmpty) { // `return` happens _before_ map `is` called. Some(()).map( return s ) } listSize(l.tail, s + 1) }
This works because in Scala, return e is an expression, not an operator. Its type is Nothing , although it is a subtype of everything, including Unit => X , which is the type required by the map parameter. The evaluation is quite simple: e returned from the closing function before map even executed (the arguments are evaluated before the method is called, obviously). This can be confusing because you expect map(return e) be parsed / interpreted as map(_ => return e) , but it is not.