What is the standard way to optimise mutual recursion in F#/Scala?
Asked Answered
A

3

8

These languages do not support mutually recursive functions optimization 'natively', so I guess it must be trampoline or.. heh.. rewriting as a loop) Do I miss something?

UPDATE: It seems that I did lie about FSharp, but I just didn't see an example of mutual tail-calls while googling

Amazing answered 9/5, 2010 at 19:16 Comment(0)
K
10

First of all, F# supports mutually recursive functions natively, because it can benefit from the tailcall instruction that's available in the .NET IL (MSDN). However, this is a bit tricky and may not work on some alternative implementations of .NET (e.g. Compact Frameworks), so you may sometimes need to deal with this by hand.

In general, I that there are a couple of ways to deal with it:

  • Trampoline - throw an exception when the recursion depth is too high and implement a top-level loop that handles the exception (the exception would carry information to resume the call). Instead of exception you can also simply return a value specifying that the function should be called again.

  • Unwind using timer - when the recursion depth is too high, you create a timer and give it a callback that will be called by the timer after some very short time (the timer will continue the recursion, but the used stack will be dropped).

    The same thing could be done using a global stack that stores the work that needs to be done. Instead of scheduling a timer, you would add function to the stack. At the top-level, the program would pick functions from the stack and run them.

To give a specific example of the first technique, in F# you could write this:

type Result<´T> =
  | Done of ´T
  | Call of (unit -> ´T)

let rec factorial acc n = 
  if n = 0 then Done acc
  else Call(fun () -> factorial (acc * n) (n + 1))

This can be used for mutually recursive functions as well. The imperative loop would simply call the f function stored in Call(f) until it produces Done with the final result. I think this is probably the cleanest way to implement this.

I'm sure there are other sophisticated techniques for dealing with this problem, but those are the two I know about (and that I used).

Kimmel answered 9/5, 2010 at 19:21 Comment(0)
B
8

On Scala 2.8, scala.util.control.TailCalls:

import scala.util.control.TailCalls._ 

def isEven(xs: List[Int]): TailRec[Boolean] = if (xs.isEmpty) 
    done(true) 
  else 
    tailcall(isOdd(xs.tail)) 

def isOdd(xs: List[Int]): TailRec[Boolean] = if (xs.isEmpty) 
    done(false) 
  else 
    tailcall(isEven(xs.tail)) 

isEven((1 to 100000).toList).result
Bloodworth answered 10/5, 2010 at 1:19 Comment(1)
If anyone is curious, I believe the TailCalls library implements a trampoline. Someone please correct me if I'm wrong.Playful
P
7

Just to have the code handy for when you Bing for F# mutual recursion:

let rec isOdd x =
    if x = 1 then true else isEven (x-1)
and isEven x = 
    if x = 0 then true else isOdd (x-1)

printfn "%A" (isEven 10000000)

This will StackOverflow if you compile without tail calls (the default in "Debug" mode, which preserves stacks for easier debugging), but run just fine when compiled with tail calls (the default in "Release" mode). The compiler does tail calls by default (see the --tailcalls option), and .NET implementations on most platforms honor it.

Plumbago answered 9/5, 2010 at 22:3 Comment(1)
See also #681106Plumbago

© 2022 - 2024 — McMap. All rights reserved.