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

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

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

Пример:

Input = { "Lorem", "ipsum", "dolor", "set", "amet", "consectetuer", "adipiscing", "elit", "Aenean", "commodo", "ligula", "eget" , "долор", "энейская", "масса" }

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
    }

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


person Alex    schedule 01.07.2012    source источник


Ответы (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))

Я до сих пор не вижу общего механизма генерации первых элементов!

EDIT Еще одна идея: первая последовательность создается 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)
person Tomas Petricek    schedule 01.07.2012
comment
Мне пришлось добавить a, чтобы изменить его на (fun i s -> i + 1, s), и это работает. Мне действительно больше нравится функциональный стиль. Спасибо за вашу помощь, ваш блог и тяжелую работу, которую вы вложили в ответы на такие вопросы! - person Alex; 02.07.2012

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

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. Отфильтровать временные данные из triple, взяв третий элемент, что является нашей основной целью;
  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< /а>.

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

Третья строка переворачивает List из-за природы оператора ::.

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

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

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

person bytebuster    schedule 02.07.2012
comment
Мне также нравится ваше решение, потому что вы не ограничены функцией Zip или Zip3 и можете просто добавить больше элементов в кортеж результата. Проблема здесь в том, что foldBack не существует для последовательностей. Спасибо за совет! - person Alex; 02.07.2012
comment
О, я только что обновил свой ответ для более короткого решения, которое не поддерживает несколько значений. Позвольте мне откатить его, чтобы сохранить оба. - person bytebuster; 02.07.2012

Я пришел из 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

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

Хаскель версия:

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
person epsilonhalbe    schedule 01.07.2012
comment
Как вы сказали, это недопустимо для F# (на самом деле, это также недопустимо для Haskell). Если вы работаете на Mac или Windows, вы можете легко изучить F# с помощью tryfsharp.org, на котором размещен компилятор F#. в браузере. Если вы используете Linux, то есть сайт, который запускает F# на стороне сервера — у него нет такого приятного пользовательского интерфейса, но вы все равно можете использовать его для запуска базового F#: tryfs.net - person Tomas Petricek; 02.07.2012