10

Prepending to a list in F# is somewhat annoying because once you're done you have to reverse it. Is there a way to build a list straight from start?

Trident D'Gao
  • 18,973
  • 19
  • 95
  • 159

4 Answers4

9

If you need to append elements to the end, you can use the type known as DList. There is an implementation available in FSharpX.

However, there is some runtime overhead associated with this (e.g. see the comments here) and so I think that building list by prepending and then reversing is generally going to be more efficient. It is also quite standard thing to do in functional programming - it may look a bit confusing at first, but it is a very common "design pattern" when implementing recursive functions that walk over lists, so I would not try to avoid it.

Community
  • 1
  • 1
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
5

There is nothing wrong with prepending and reversing the list. You can use append (@) on a single-element list, but it is a code smell. A tolerable (tail-recursive) approach is:

let appendSingle xs x =
    [ yield! xs
      yield x ]

All the above solutions have O(n) execution time. For your use case, you could keep a private ResizeArray to avoid the use of reverse. It is fine since mutability is hidden. Compare this function

let filter f l = 
  let rec loop acc l =
    match l with 
    | [] -> List.rev acc                        
    | x::xs when f x -> loop (x::acc) xs  
    | x::xs -> loop acc xs
  loop [] l

with its more efficient counterpart

let filter f l = 
  let rec loop (acc : ResizeArray<_>) l =
    match l with 
    | [] -> Seq.toList acc                        
    | x::xs when f x -> 
        acc.Add(x)  
        loop acc xs  
    | x::xs -> loop acc xs
  loop (ResizeArray()) l
pad
  • 41,040
  • 7
  • 92
  • 166
  • 1
    Why do you say using append (@) with a single element list a code smell? It's a perfectly valid approach IMHO. You're `appendSingle` function does basicly the same than xs @ [x] with the same performance characteristics but in a more complicated way – Gustavo Guerra Sep 17 '13 at 15:36
  • I should have said both are unidiomatic and the second one is tolerable due to tail-recursiveness. – pad Sep 17 '13 at 16:20
4

Append x to xs like this:

xs @ [x]

Note that this is O(n).

J D
  • 48,105
  • 13
  • 171
  • 274
  • Maybe f# has DList like Haskell, so you can get O(1) appends? – Peaker Sep 20 '13 at 21:22
  • I'm sure there are libraries implementing lists with append and catenable lists but there is basically no demand for this in the real world – J D Dec 03 '17 at 19:14
3

Referring back to your original code, I think what you are looking for is to use List.foldBack rather than List.fold. Essentially, foldBack repeatedly applies the folder function starting from the end of the list rather than from the start of the list. It's not as efficient as fold but it's better to use foldBack and avoid reversing the list.

With foldBack, your accumulation function folder is applied to a list x0::x1::...::xlast as follows, where the initial argument to folder isinit:

folder x0 (folder x1 ( ... (folder xlast init) ... ) )

c.f. fold

folder (... (folder (folder init x0) x1) ...) xlast

There are some other answers to your original question that suggest alternative solutions, but sticking with your code, substituting foldBack for fold results in a first implementation

let chunkOrig items chunkSize =
    let folder =
        fun x (result, chunk) ->
            if List.length chunk < chunkSize then
                (result, x::chunk)
            else
                (chunk::result, [x])
    let (a,b) = List.foldBack folder items ([], []) 
    b::a

Already this is a lot simpler, as all the list reversing has gone. And it seems to work.

> chunkOrig [1..10] 2;;
val it : int list list = [[1; 2]; [3; 4]; [5; 6]; [7; 8]; [9; 10]]

However when your list doesn't divide into equal chunks it goes wrong, because foldBack starts from the last element.

> chunkOrig [1..11] 2;;
val it : int list list = [[1]; [2; 3]; [4; 5]; [6; 7]; [8; 9]; [10; 11]]

What you need to do is parameterise your local function folder by the length remaining in the current chunk rather than by the chunk itself.

let chunk items chunkSize =
    let folder =
        fun x (result, lenLeft) ->
        if lenLeft > 0 then
            match result with
               | [] -> ([[x]], lenLeft-1)
               | r0::rtail -> ((x::r0)::rtail, lenLeft-1)
        else
            ([x]::result, chunkSize-1)
    let (result, lenLeft) = List.foldBack folder items ([], (List.length items) % chunkSize) 
    result

> chunk [1..10] 2;;
val it : int list list = [[1; 2]; [3; 4]; [5; 6]; [7; 8]; [9; 10]]

> chunk [1..11] 2;;
val it : int list list = [[1; 2]; [3; 4]; [5; 6]; [7; 8]; [9; 10]; [11]]
Community
  • 1
  • 1
TooTone
  • 7,129
  • 5
  • 34
  • 60