質問

I have a function witch is written in an imperative style and cant get my head around on how to convert it to a more robust functional approach.

The function takes a seq of strings and returns a seq of tuples where each tuple consists of the 2,7,12,.. and 5,10,15,.. item from the input.

Example:

Input = { "Lorem", "ipsum", "dolor", "set", "amet", "consectetuer", "adipiscing", "elit", "Aenean", "commodo", "ligula", "eget", "dolor", "Aenean", "massa" }

Ouput = { ("ipsum", "amet"), ("adipiscing", "commodo"), ("eget", "massa") }

let convert (input : seq<string>) : seq<(string * string)> =
    let enum = input.GetEnumerator()
    let index = ref 0
    let first = ref ""
    let second = ref ""

    seq {
        while enum.MoveNext() do
            let modIndex = !index % 5
            index := !index + 1

            if (modIndex % 2 = 0 && !first = "") then first := enum.Current
            if (modIndex % 5 = 0 && !second = "") then second := enum.Current

            if modIndex = 0  then
                let result = (!first, !second)
                first := ""
                second := ""
                yield result
    }

Any help or tip for a starting point is appreciated.

役に立ちましたか?

解決

I do not completely understand the behaviour you want - what is the algorithm for generating indices that you want to pair? Anyway, one nice functional solution is to take the elements you want to pair separately and then combine them using Seq.zip.

You can use Seq.mapi to add indices to the values and then use Seq.choose to get the values with the right index (and skip all other values). For hardcoded indices, you can write something like:

let indexed = input |> Seq.mapi (fun i s -> i, s)
Seq.zip 
  (indexed |> Seq.choose (fun (i, v) -> if i=1 || i=6 || i=11 then Some v else None))
  (indexed |> Seq.choose (fun (i, v) -> if i=4 || i=9 || i=14 then Some v else None))

I used your numbers -1 because the indices are from 0 - so the above gives you the results you wanted. The second series looks like multiples of 5, so perhaps you wanted i%5 = 4 to generate second elements:

let indexed = input |> Seq.mapi (fun i s -> i, s)
Seq.zip 
  (indexed |> Seq.choose (fun (i, v) -> if i=1 || i=6 || i=11 then Some v else None))
  (indexed |> Seq.choose (fun (i, v) -> if i%5 = 4 then Some v else None))

I still don't see the general mechanism for generating the first elements though!

EDIT One more idea - is the first sequence generated by i*5 + 2 and the second by i*5? In that case, your example is wrong, but you could write it like this:

let indexed = input |> Seq.mapi (fun i s -> i, s)
Seq.zip 
  (indexed |> Seq.choose (fun (i, v) -> if i%5 = 2 then Some v else None))
  (indexed |> Seq.choose (fun (i, v) -> if i%5 = 0 then Some v else None))

... or if you want to make the code shroter, you can refactor:

let filterNthElements div rem = 
  input |> Seq.mapi (fun i s -> i, s)
        |> Seq.choose (fun (i, v) -> if i%div = rem then Some v else None)

Seq.zip (filterNthElements 5 2) (filterNthElements 5 0)

他のヒント

Here's more idiomatic way to do that. Actually, it's one-liner; I've just aligned it for better readability.

let Input = [ "Lorem"; "ipsum"; "dolor"; "set"; "amet"; "consectetuer";
              "adipiscing"; "elit"; "Aenean"; "commodo"; "ligula"; "eget";
              "dolor"; "Aenean"; "massa" ]

// Short solution that does not support more than two values
let Output1 =
    Input
    |> List.fold
        (fun (i, l1, l2) x ->
            if i=4 then 0, None, (l1.Value, x)::l2
            elif i=1 then i+1, Some x, l2
            else i+1, l1, l2
        )
        (0, None, [])
    |> fun (_, _, elem) -> elem
    |> List.rev

Idea

The general idea is based on three steps:

  1. Splitting the list into a List of tuples, taking 2nd and 5th strings. WARNING If the original data length is not a multiplier of 5, the trailing element will be lost.
  2. Filtering out temporary data from a triple by taking the third element, which is our primary goal;
  3. Reversing the list.

Explanation

The first line is the hardest one.

Let's define our state. It will be a triple of sequential number, an string option that contains strings ##2, 7, etc and an "outer" (string*string) list that is added once we meet elements ##5, 10, etc.

The function will place the 2nd, 7th, etc elements to the "inner" string option, or, if i equals to 5, 10, etc., form a tuple and add it to the "outer" List (dropping the inner value for sake of clarity).

We use List.fold, and therefore the final list is to be reversed.

An initial state is a triple of (0, None, []). More info onList.fold` in MSDN.

The second line just takes the third element from a triple. I've made it a function to allow chain binding.

The third line reverses the List due to the nature of :: operator.

As per length of the initial list. If it has found the "2nd" element but did not reach the "5th", the second element of a triple has the value. You may detect the erroneous situation by verifying it:

...
|> fun (_, temp, elem) ->
    if temp.IsSome
    then failwith "Data length must be a multiplier of 5"
    else elem
...

Here's a bit longer code that supports more than two elements:

let Output2 = 
    Input
    |> List.foldBack
        (fun x (i, l1, l2) ->
            if i = 4
            then 0, [], (x::l1)::l2
            else i+1, x::l1, l2
        )
        <| (0, [], [])
    |> fun (_, _, elem) -> elem
    |> List.choose
        (function
        | [_; first; _; _; second] -> Some (first, second)
        | _-> None
        )

Note this variant does not drop elements during the first call, so you may retrieve more than two items.

IMPORTANT: The list is processed in the reverse order, and so the item index is calculated from the end of input. You may change it to List.fold in cost or further reversing the list as in Output1.

Mind the reverse binding operator <| due to signature of List.foldBack.

You may check for errors in a similar way: by testing if the "inner" list is not empty.

I come from haskell not f# so i will give a probably non valid f# code idea:

At first I would generate two lists from my input:

let zeromod5 = filter (index == 0 % 5) input
let twomod5 = filter (index == 2 % 5) input

which should result in the lists

{ "ipsum", "adipiscing","eget"}
{ "amet", "commodo","massa" }

and then zip them, i. e. make a list of pairs by something like

zip zeromod5 twomod5

Edit:

Haskell version:

zipWeird :: [String] -> [(String, String)]
zipWeird ss = zip twoMod5s zeroMod5s
            where zeroMod5s = map fst $ filter (\(_,y) -> y `mod` 5 == 0) eSS
                  twoMod5s = map fst $ filter (\(_,y) -> y `mod` 5 == 2) eSS
                  eSS = zip ss [1..]

zipWeird2 :: [String] -> [(String, String)]
zipWeird2 ss = map fst $ filter (\(_,y) -> y `mod`5 ==1) ezSS
             where zSS = zip (tail ss) (drop 4 ss)
                   ezSS = zip zSS [1..]

input :: [String]
input = words ("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, "++
              "sed diam nonumy eirmod tempor invidunt ut labore et dolore "++
              "magna aliquyam erat, sed diam voluptua. At vero eos et "++
              "accusam et justo duo dolores et ea rebum. Stet clita kasd "++
              "gubergren, no sea takimata sanctus est Lorem ipsum dolor sit "++
              "amet.")

main :: IO ()
main = do 
          print $ zipWeird input
          print $ zipWeird2 input
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top