views:

258

answers:

3

A simple append function like this (in F#):

let rec app s t =
   match s with
      | [] -> t
      | (x::ss) -> x :: (app ss t)

will crash when s becomes big, since the function is not tail recursive. I noticed that F#'s standard append function does not crash with big lists, so it must be implemented differently. So I wondered: How does a tail recursive definition of append look like? I came up with something like this:

let rec comb s t =
   match s with
      | [] -> t
      | (x::ss) -> comb ss (x::t)
let app2 s t = comb (List.rev s) t 

which works, but looks rather odd. Is there a more elegant definition?

+8  A: 

Traditional (not tail-recursive)

let rec append a b =
    match a, b with
    | [], ys -> ys
    | x::xs, ys -> x::append xs ys

With an accumulator (tail-recursive)

let append2 a b =
    let rec loop acc = function
        | [] -> acc
        | x::xs -> loop (x::acc) xs
    loop b (List.rev a)

With continuations (tail-recursive)

let append3 a b =
    let rec append = function
        | cont, [], ys -> cont ys
        | cont, x::xs, ys -> append ((fun acc -> cont (x::acc)), xs, ys)
    append(id, a, b)

Its pretty straight-forward to convert any non-tail recursive function to recursive with continuations, but I personally prefer accumulators for straight-forward readability.

Juliet
In the first example, what's the point of doing pattern matching on b if it's the same in all patterns? You can simply use b
Rubys
@Rubys: its a style choice, neither correct nor incorrect ;)
Juliet
You're sure it's working? I get> append2 [1;2] [3;4];;val it : int list = [2; 3; 4]and> append3 [1;2] [3;4];;val it : int list = [1; 3; 4]Though I don't see the error, append2 looks ok to me..
martingw
Very strange. Your code is perfectly fine, it runs with fsc. Does not run within fsi, though. Not the first issue I have with fsi on mono.
martingw
One last observation: The difference between append2 and append3 is not just readability: append2 [1..10000000] [] works, and append3 [1..10000000] [] leads to a stack overflow.
martingw
@martingw: append2 and append3 are correct, there's no reason whatsoever why you'd get a wrong result. Additionally, append3 is tail recursive, but you still might a stack overflow if you run the code in debug mode since tail calls are disabled by default (see http://stackoverflow.com/questions/1416415/f-stackoverflow-project-euler-4/1416436#1416436).
Juliet
I don't believe mono supports tail recursion, so that's why you would be getting a stackoverflow with append3.
justin
@Juliet: Yes, sorry for doubting your code :-)! Works fine with fsi on .NET, so what I encountered was definitely a mono issue. There's no stack overflow either with append3 for large lists on .NET, so that was maybe also some mono thing. @justin: append2 worked for large lists on mono, so some kind of tco must be implemented in mono, I guess.
martingw
+1  A: 

From a quick glance at the F# sources, it seems the tail is internally mutable. A simple solution would be to reverse the first list before consing its elements to the second list. That, along with reversing the list, are trivial to implement tail recursively.

Daniel
+6  A: 

In addition to what Juliet posted:

Using sequence expressions
Internally, sequence expressions generate tail-recursive code, so this works just fine.

let append xs ys = 
  [ yield! xs
    yield! ys ]

Using mutable .NET types
David mentioned that F# lists can be mutated - that's however limited only to F# core libraries (and the feature cannot be used by users, because it breaks the functional concepts). You can use mutable .NET data types to implement a mutation-based version:

let append (xs:'a[]) (ys:'a[]) = 
  let ra = new ResizeArray<_>(xs)
  for y in ys do ra.Add(y)
  ra |> List.ofSeq

This may be useful in some scenarios, but I'd generally avoid mutation in F# code.

Tomas Petricek
+1 for the sequence expressions solution
Daniel