Linked list partition function and reversed results

前端 未结 5 713
耶瑟儿~
耶瑟儿~ 2020-12-02 00:26

I wrote this F# function to partition a list up to a certain point and no further -- much like a cross between takeWhile and partition.

<         


        
相关标签:
5条回答
  • 2020-12-02 00:51

    You can rewrite the function like this:

    let partitionWhile c l =
      let rec aux xs =
        match xs with
          | [] -> ([], [])
          | h :: t ->
              if c h then
                let (good, bad) = aux t in
                  (h :: good, bad)
              else
                ([], h :: t)
      aux l
    

    Yes, as Brian has noted it is no longer tail recursive, but it answers the question as stated. Incidentally, span in Haskell is implemented exactly the same way in Hugs:

    span p []       = ([],[])
    span p xs@(x:xs')
        | p x       = (x:ys, zs)
        | otherwise = ([],xs)
        where (ys,zs) = span p xs'
    

    A good reason for preferring this version in Haskell is laziness: In the first version all the good elements are visited before the list is reversed. In the second version the first good element can be returned immediately.

    0 讨论(0)
  • 2020-12-02 00:53

    I don't think I'm the only one to learn a lot from (struggling with) Daniel's CPS solution. In trying to figure it out, it helped me change several potentially (to the beginner) ambiguous list references, like so:

        let partitionWhileCps cond l1 =
    
            let rec aux f l2 = 
                match l2 with
                | h::t when cond h ->   aux  (fun (acc, l3) -> f (h::acc, l3))  t
                | l4 -> f ([], l4)
    
            aux id l1
    

    (Note that "[]" in the l4 match is the initial acc value.) I like this solution because it feels less kludgey not having to use List.rev, by drilling to the end of the first list and building the second list backwards. I think the other main way to avoid .rev would be to use tail recursion with a cons operation. Some languages optimize "tail recursion mod cons" in the same way as proper tail recursion (but Don Syme has said that this won't be coming to F#).

    So this is not tail-recursive safe in F#, but it makes my answer an answer and avoids List.rev (this is ugly to have to access the two tuple elements and would be a more fitting parallel to the cps approach otherwise, I think, like if we only returned the first list):

        let partitionWhileTrmc cond l1 = 
    
            let rec aux acc l2 =  
                match l2 with 
                | h::t when cond h ->  ( h::fst(aux acc t), snd(aux acc t)) 
                | l3 -> (acc, l3)
    
            aux [] l1
    
    0 讨论(0)
  • 2020-12-02 01:01

    Here's a continuation-based version. It's tail-recursive and returns the list in the original order.

    let partitionWhileCps c l =
      let rec aux f = function
        | h::t when c h -> aux (fun (acc, l) -> f ((h::acc), l)) t
        | l -> f ([], l)
      aux id l
    

    Here are some benchmarks to go along with the discussion following Brian's answer (and the accumulator version for reference):

    let partitionWhileAcc c l =
      let rec aux acc = function
        | h::t when c h -> aux (h::acc) t
        | l -> (List.rev acc, l)
      aux [] l
    
    let test = 
      let l = List.init 10000000 id
      (fun f ->
        let r = f ((>) 9999999) l
        printfn "%A" r)
    
    test partitionWhileCps // Real: 00:00:06.912, CPU: 00:00:07.347, GC gen0: 78, gen1: 65, gen2: 1
    test partitionWhileAcc // Real: 00:00:03.755, CPU: 00:00:03.790, GC gen0: 52, gen1: 50, gen2: 1
    

    Cps averaged ~7s, Acc ~4s. In short, continuations buy you nothing for this exercise.

    0 讨论(0)
  • 2020-12-02 01:02

    I usually prefer Sequences over List as they are lazy and you got List.toSeq and Seq.toList functions to convert between them. Below is the implementation of your partitionWhile function using sequences.

    let partitionWhile (c:'a -> bool) (l:'a list) = 
        let fromEnum (e:'a IEnumerator) = 
            seq { while e.MoveNext() do yield e.Current}
        use e = (l |> List.toSeq).GetEnumerator()
        (e |> fromEnum |> Seq.takeWhile c |> Seq.toList)
        ,(e |> fromEnum |> Seq.toList)
    
    0 讨论(0)
  • 2020-12-02 01:06

    I expect you can use continuations, but calling List.rev at the end is the best way to go.

    0 讨论(0)
提交回复
热议问题