1

I have a following problem.

class NonEmpty(elem: Tweet, left: TweetSet, right: TweetSet) extends TweetSet {

  @tailrec final def filterAcc(p: Tweet => Boolean, acc: TweetSet): TweetSet =
    if(p(elem))
      (this remove elem).filterAcc(p, acc incl elem)
    else
      (this remove elem).filterAcc(p, acc)

}

Scala tels me, that it can not optimize @tairec, because my method contains a recursive call targeting a supertype. (Superclass of NonEmpty is class TweetSet, where the method filterAcc is defined). How to deal with such an error?

Sergey
  • 2,880
  • 3
  • 19
  • 29
Maciej
  • 145
  • 5

2 Answers2

4

This is intended behavior. The error occurs due to how @tailrec works: in essence, the compiler tries to transform a recursive call into a local loop. In spite of the fact that your method is recursive, it recurses by calling itself on another instance not even of the same type, but of the supertype - and since other children of TweetSet may provide a completely different implementation of filterAcc, the compiler cannot make sure this method can be safely expanded into a loop.

A rule of thumb for tail-recursive functions is to ensure that it always calls exactly itself as the last statement - in your case, instead of calling

(this remove elem).filterAcc(...)

you have to try and transform acc in the way that emulates this remove elem and call

filterAcc(p, <transformed acc>)

In functional programming involving recursion it is a good idea to define the entire method in the superclass (in your case TweetSet) and rely on pattern matching by ADT members instead of polymorphism - that way there will be no issues with optimizing recursion.

Upd: There's a nice blog post explaining tail recursion, as specified in an answer to this question

Community
  • 1
  • 1
Sergey
  • 2,880
  • 3
  • 19
  • 29
  • I am afraid this is not possible, because filterAcc returns set of all elements of this, witch satisfies p condition (p is a logical formula), so i need to call filterAcc for smaller set each time. – Maciej Jun 01 '16 at 10:48
  • 1
    I just wanted to point out that this is *not* a general restriction of tail recursion. The OP's method *is* tail-recursive, it is, however, not *direct tail-recursive*. Scala only optimizes direct tail-recursion. There's also nothing in the semantics of the Scala language that prevents optimizing indirect tail-recursion or even general tail-calls. Rather, this restriction is a pragmatic choice: the Scala devs want a high-performance, highly interoperable Scala implementation on the JVM, and implementing advanced non-local control-flow on the JVM while keeping those two properties is impossible – Jörg W Mittag Jun 02 '16 at 10:41
  • 1
    I call it "Hickey's Law" after a throwaway sentence in an interview with Rich Hickey, the creator of Clojure (which makes a very similar choice) about why Clojure, unlike Scheme, doesn't support Proper Tail-Calls: "Advanced Control-Flow, Interop, Performance. Pick Two." It's not that Scala cannot do Proper Tail-Calls (It can, Scala-Native will do it soon, for example). It's not that you cannot implement Proper Tail-Calls on the JVM (Scheme implementations on the JVM do it). But you have to implement your own stack to do it (or something similar), which costs you interop with the JVM stack. – Jörg W Mittag Jun 02 '16 at 10:44
0

In this case it may make sense to move filterAcc into the companion object:

trait TweetSet {
  final def filterAcc(p: Tweet => Boolean) = TweetSet.filterAcc(p, this, Empty)
}

object TweetSet {
  @tailrec 
  private def filterAcc(p: Tweet => Boolean, self: TweetSet, acc: TweetSet) = self match {
    case ne: NonEmpty =>
      // the original body with `this` replaced by `self` and made an argument for recursive calls
      val elem = ne.elem
      if(p(elem))
        filterAcc(p, self remove elem, acc incl elem)
      else
        filterAcc(p, self remove elem, acc)
    // other cases
  }
}
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487