Skip to content

Instantly share code, notes, and snippets.

@mrange
Last active November 16, 2018 11:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrange/3a275cc643c9753925f1c8a68b0bd7a0 to your computer and use it in GitHub Desktop.
Save mrange/3a275cc643c9753925f1c8a68b0bd7a0 to your computer and use it in GitHub Desktop.
Iterate efficiently in F# using tail-recursion

Coming from imperative languages many developers wonder how to write a for-loop the exits early as F# doesn't support break, continue nor return. The answer in F# is to use tail-recursion which is a more flexible and idiomatic way to iterate while still providing excellent performance.

Say we like to implement tryFind for List. If F# supported return we would write tryFind a bit like this:

    let tryFind predicate vs =
      for v in vs do
        if predicate v then
          return Some v
      None

This doesn't work in F#, instead we write the function using tail-recursion:

    let tryFind predicate vs =
      let rec loop = function
        | v::vs -> if predicate v then Some v else loop vs
        | _ -> None
      loop vs

Tail-recursion is performant in F# because when the F# compiler detect that a function is tail-recursive it rewrites the recursion into an efficient while-loop. Using ILSpy can we see that this is true for loop:

    internal static FSharpOption<a> loop@3-10<a>(FSharpFunc<a, bool> predicate, FSharpList<a> _arg1)
    {
      while (_arg1.TailOrNull != null)
      {
        FSharpList<a> fSharpList = _arg1;
        FSharpList<a> vs = fSharpList.TailOrNull;
        a v = fSharpList.HeadOrDefault;
        if (predicate.Invoke(v))
        {
          return FSharpOption<a>.Some(v);
        }
        FSharpFunc<a, bool> arg_2D_0 = predicate;
        _arg1 = vs;
        predicate = arg_2D_0;
      }
      return null;
    }

Apart from "interesting" assignments (which hopefully the JIT:er eliminates) this is essentially an efficient loop.

In addition tail-recursion is more idiomatic as it allows us to avoid mutable state. Consider a sum function that sums all elements in a List. An obvious first try would be this:

    let sum vs =
      let mutable s = LanguagePrimitives.GenericZero
      for v in vs do
        s <- s + v
      s

If we rewrite the loop into tail-recursion we can avoid the mutable state:

    let sum vs =
      let rec loop s = function
        | v::vs -> loop (s + v) vs
        | _ -> s
      loop LanguagePrimitives.GenericZero vs

For efficiency the F# compiler transforms this into a while-loop that uses mutable state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment