Нужны идеи для преобразования императивного кода F# в функционал

У меня есть функция, написанная в императивном стиле, и я не могу понять, как преобразовать ее в более надежный функциональный подход.

Функция принимает последовательность строк и возвращает последовательность кортежей, где каждый кортеж состоит из элемента 2,7,12,.. и 5,10,15,.. из входных данных.

Пример:

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
    }

Любая помощь или совет для отправной точки приветствуется.

3 ответа

Решение

Я не совсем понимаю, какое поведение вы хотите - каков алгоритм генерации индексов, которые вы хотите спарить? В любом случае, одно хорошее функциональное решение состоит в том, чтобы взять элементы, которые вы хотите соединить отдельно, а затем объединить их, используя Seq.zip,

Ты можешь использовать Seq.mapi добавить индексы к значениям, а затем использовать Seq.choose чтобы получить значения с правильным индексом (и пропустить все остальные значения). Для жестко закодированных индексов вы можете написать что-то вроде:

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))

Я использовал ваши цифры -1, потому что индексы от 0 - так что выше дает вам результаты, которые вы хотели. Вторая серия выглядит как кратные 5, так что, возможно, вы хотели i%5 = 4 генерировать вторые элементы:

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*5 + 2 а второй i*5? В этом случае ваш пример неверен, но вы можете написать это так:

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))

... или, если вы хотите улучшить код, вы можете рефакторинг:

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)

Вот более идиоматический способ сделать это. На самом деле, это одна строка; Я просто выровнял его для лучшей читаемости.

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

идея

Общая идея основана на трех этапах:

  1. Разделение списка на List кортежей, берут 2-ую и 5-ую струны. ВНИМАНИЕ! Если исходная длина данных не является множителем 5, конечный элемент будет потерян.
  2. Отфильтровываем временные данные из тройки, беря третий элемент, что является нашей главной целью;
  3. Перевернуть список.

объяснение

Первая строка самая сложная.

Давайте определим наше состояние. Это будет тройка последовательного числа, string option который содержит строки ##2, 7 и т. д. и "внешний" (string*string) list который добавляется, когда мы встречаемся с элементами ##5, 10 и т. д.

Функция поместит 2-й, 7-й и т. Д. Элементы во "внутренний" string option, или если i равно 5, 10 и т. д., сформировать кортеж и добавить его к "внешнему" List (отбрасывая внутреннее значение ради ясности).

Мы используем List.foldи, следовательно, окончательный список должен быть полностью изменен.

Начальное состояние - это тройка (0, None, []). More info onList.fold` в MSDN.

Вторая строка просто берет третий элемент из тройки. Я сделал это функцией, позволяющей связывать цепочки.

Третья строка меняет List из-за характера :: оператор.

По длине начального списка. Если он нашел "2-й" элемент, но не достиг "5-го", второй элемент тройки имеет значение. Вы можете обнаружить ошибочную ситуацию, проверив ее:

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

Вот немного более длинный код, который поддерживает более двух элементов:

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
        )

Обратите внимание, что этот вариант не удаляет элементы во время первого вызова, поэтому вы можете получить более двух элементов.

ВАЖНО: список обрабатывается в обратном порядке, поэтому индекс элемента рассчитывается с конца ввода. Вы можете изменить его на List.fold в стоимости или дальнейшего изменения списка, как в Output1,

Следите за оператором обратной привязки <| из-за подписи List.foldBack,

Вы можете проверять ошибки аналогичным образом: проверяя, не является ли "внутренний" список пустым.

Я пришел из haskell, а не из F#, поэтому я приведу, вероятно, неверную идею кода F#:

Сначала я бы сгенерировал два списка из моего ввода:

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

что должно привести к спискам

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

а затем заархивировать их, т.е. составить список пар чем-то вроде

zip zeromod5 twomod5

Редактировать:

Версия на Haskell:

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
Другие вопросы по тегам