Как преобразовать эту функцию для использования хвостового вызова

Рекурсивная функция:

let rec listMerge (l1 : 'a list) (l2 : 'a list) =
    if l1.IsEmpty then      l2 
    elif l2.IsEmpty then    l1 
    else                    l1.Head :: l2.Head :: listMerge l1.Tail l2.Tail

Теперь, если я не ошибаюсь, это на самом деле не выполняет хвостовой вызов, он просто может выглядеть так, если не учитывать, что :: является правильным ассоциативным.

Затем у меня сложилось впечатление (из чего-то, что я читал, но не смог найти сейчас), что это можно легко преобразовать в хвостовую рекурсию, используя дополнительный fun или что-то в этом роде.

Итак, возможно ли это? Код?

Мой ответ: Итак, вот как я изменил функции благодаря ответам ниже:

let listMerge l1 l2 =
    let rec mergeLoop  (l1 : 'a list) (l2 : 'a list) acc =
        if l1.IsEmpty then      (List.rev acc) @ l2 
        elif l2.IsEmpty then    (List.rev acc) @ l1
        else                    mergeLoop l1.Tail l2.Tail (l2.Head :: l1.Head :: acc)
    mergeLoop l1 l2 []

person hyde    schedule 31.10.2012    source источник
comment
Я предлагаю вам прочитать о сопоставлении с образцом. Это сделает вашу компанию более читабельной и чистой.   -  person Ramon Snir    schedule 31.10.2012
comment
Если вы имеете в виду match, у меня также есть версия, в которой это используется, хотя я думаю, что эта версия более читабельна, чтение вслух почти на простом английском... Однако IL совсем другой, мне интересно, должен ли я опубликовать еще один вопрос, задающий что более эффективно.   -  person hyde    schedule 31.10.2012
comment
В целом сопоставление с образцом (в match, let, use, fun, function и т. д.) более практично для функционального кода. Это также позволит вам делать более сложные вещи, которые if просто не могут.   -  person Ramon Snir    schedule 31.10.2012
comment
Мне интересно, должен ли я опубликовать еще один вопрос, который более эффективен. Код, который вы здесь написали, очень плохой: небезопасный и медленный.   -  person J D    schedule 04.11.2012
comment
@JonHarrop Несмотря на то, что такой бесполезный комментарий с нулевой информацией в 99% случаев является троллингом, я укушу. Небезопасно как?   -  person hyde    schedule 04.11.2012
comment
@hyde Как небезопасно? Вы используете функции Head и Tail, которые могут генерировать исключения, когда вы могли бы использовать сопоставление с образцом, чтобы заставить компилятор доказать правильность вашего кода в этом отношении.   -  person J D    schedule 05.11.2012
comment
@JonHarrop Чем match бросает MatchFailureException лучше, чем Head или Tail бросает ArgumentException, если в логике есть ошибка? Извините, если я кажусь упрямым, я полностью за match, когда это дает дополнительную ценность, я просто не вижу дополнительной ценности в данном случае.   -  person hyde    schedule 05.11.2012
comment
@hyde Если когда-либо есть риск match выбросить MatchFailureException, компилятор выдаст предупреждение. В этом случае компилятор доказывает, что match никогда не вызовет исключения, поэтому во время компиляции вы знаете, что ваш код никогда не выйдет из строя таким образом.   -  person J D    schedule 05.11.2012
comment
@JonHarrop Ах, что ж, это очень хорошо (я исправляю все предупреждения, фундаменталист, у которого появляется сыпь от утиного набора текста).   -  person hyde    schedule 05.11.2012


Ответы (5)


Как предложил @Ramon, вы должны использовать сопоставление с образцом для лучшей читабельности:

let rec listMerge xs ys =
    match xs, ys with
    | [], _ -> ys
    | _, [] -> xs
    | x::xs', y::ys' -> x::y::listMerge xs' ys'

Как видите, два cons-конструктора (::) являются последними операциями над listMerge, поэтому функция не является хвостовой рекурсией.

Вы можете использовать аккумулятор для получения результатов хвостовой рекурсией:

let listMerge xs ys =
    let rec loop xs ys acc =
        match xs, ys with
        | [], zs | zs, [] -> (List.rev zs)@acc
        | x::xs', y::ys' -> loop xs' ys' (y::x::acc)
    List.rev (loop xs ys [])

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

В F# существует подход с хвостовой рекурсией, использующий выражения последовательности, которые соответствуют строке стиль перехода продолжения:

let listMerge xs ys =
    let rec loop xs ys =
        seq {
            match xs, ys with
            | [], zs | zs, [] -> yield! zs
            | x::xs', y::ys' -> 
                yield x
                yield y
                yield! loop xs' ys'
        }
    loop xs ys |> Seq.toList

Мне нравится этот подход, поскольку он удобен и близок к вашей исходной формулировке.

person pad    schedule 31.10.2012
comment
Принял это как наиболее полное, хотя я лично не согласен с тем, что match в этом случае более читабелен (для этого определения читабельности: случайный кодер, читающий код и понимающий его). - person hyde; 31.10.2012
comment
Я надеюсь, что когда вы привыкнете к F#, вы измените свое мнение. Мое определение удобочитаемости: случайные программисты F# читают код и сразу его понимают. - person pad; 31.10.2012
comment
Возможное улучшение для примера последовательности, работает ли это: | [],s | s,[] -> yield! s вместо отдельных yield! ys и yield! xs? И то же самое для хвостовой рекурсивной версии, конечно. - person hyde; 31.10.2012
comment
Вы правы. Это пример продвинутых вещей, которые вы можете сделать с помощью сопоставления с образцом. Я обновил свой ответ; первый пример хранится как есть, чтобы быть близким к вашему вопросу. - person pad; 31.10.2012
comment
Прохладный. Это начинает казаться мне чем-то, что делает match стоящим в моих глазах, делать вещи, которые if делать не может. - person hyde; 31.10.2012
comment
Также часто можно увидеть использование синтаксиса функции сопоставления с образцом для предотвращения затенения переменных в операторах сопоставления. let rec loop acc = function | [], zs | zs, [] -> и т. д. - person gradbot; 03.11.2012

Вы можете накапливать сконструированный результат при последующих вызовах listMerge и, наконец, возвращать накопленный результат. Мои навыки F# довольно заржавели, но вот простая функция Lisp.

(defun list-merge (xs ys &optional acc)
  (cond ((< 0 (length xs)) (list-merge (rest xs) ys (cons (first xs) acc)))
        ((< 0 (length ys)) (list-merge xs (rest ys) (cons (first ys) acc)))
        (t acc)))

(list-merge '(1 2 3) '(3 4 5)) ;=> (5 4 3 3 2 1)
(list-merge '() '(1 2))        ;=> (2 1)
(list-merge '() '())           ;=> nil
person Volkan Yazıcı    schedule 31.10.2012

Простая версия с использованием аккумулятора:

let rec listMerge (l1 : 'a list) (l2 : 'a list) acc =
    if l1.IsEmpty then      (List.rev l2)@acc 
    elif l2.IsEmpty then    (List.rev l1)@acc
    else                    listMerge l1.Tail l2.Tail (l1.Head :: l2.Head :: acc)

Я проверил это с двумя миллионами списков элементов, и не было переполнения стека, поэтому я достаточно уверен, что это хвостовая рекурсия.

person John Palmer    schedule 31.10.2012
comment
Нарушен порядок элементов. Если вы измените последнее предложение на (l2.Head :: l1.Head :: acc), по крайней мере, вы получите обратный порядок результатов OP. - person pad; 31.10.2012

Я думаю, вам придется повторно использовать исходный код F# из F# PowerPack.
Собственно, то, что вам нужно равно List.fold2, за исключением того, что функция не должна генерировать исключение SR.listsHadDifferentLengths в случае, если размеры списка различаются, а вместо этого должна обрабатывать оставшуюся часть более длинного списка, например:

let l1 = ["A1"; "A2"; "A3"; "A4"; "A5"; "A6"; "A7"]
let l2 = ["B1"; "B2"; "B3"; "B4"]

let expectedResult = ["A1"; "B1"; "A2"; "B2"; "A3"; "B3"; "A4"; "B4"; "A5"; "A6"; "A7"]

Вот как мы это делаем:

[<CompiledName("Fold2Tail")>]
let fold2Tail<'T1,'T2,'State> f g1 g2 (acc:'State) (list1:list<'T1>) (list2:list<'T2>) = 
    let f  = OptimizedClosures.FSharpFunc<_,_,_,_>.Adapt(f)
    let g1 = OptimizedClosures.FSharpFunc<_,_,_>.Adapt(g1)
    let g2 = OptimizedClosures.FSharpFunc<_,_,_>.Adapt(g2)
    let rec loop acc list1 list2 =
        match list1, list2 with 
        | [], [] -> acc
        | _,  []  -> g1.Invoke(acc, list1)
        | [], _   -> g2.Invoke(acc, list2)
        | (h1::t1),(h2::t2) -> loop (f.Invoke(acc,h1,h2)) t1 t2
    loop acc list1 list2

g1 и g2 являются предикатами типа 'State -> 'T1 list -> 'State и 'State -> 'T2 list -> 'State соответственно. Они говорят, как обрабатывать остатки обоих списков. Обратите внимание, что их два, так как в общем случае 'T1 и 'T2 — это разные типы. Да, это несколько накладно, но вы легко можете уменьшить его под свои нужды, пожертвовав универсальностью.

Применение:

let ret =
    fold2Tail
        (fun s x y  -> [ yield! s; yield x; yield y ] ) // f
        List.append // g1
        List.append // g2
        []          // 'State
        l1 l2
person bytebuster    schedule 31.10.2012

Вы должны использовать сопоставление с образцом:

let rec merge xs ys =
  match xs, ys with
  | [], xs | xs, [] -> xs
  | x::xs, y::ys -> x::y::merge xs ys

Чтобы получить хвостовые вызовы, вы можете использовать аккумулятор:

let merge xs ys =
  let rec loop xys xs ys =
    match xs, ys with
    | [], xs | xs, [] -> List.fold (fun xs x -> x::xs) xs xys
    | x::xs, y::ys -> loop (y::x::xys) xs ys
  loop [] xs ys

или стиль прохождения продолжения:

let merge xs ys =
  let rec loop xs ys k =
    match xs, ys with
    | [], xs | xs, [] -> k xs
    | x::xs, y::ys -> loop xs ys (fun xys -> k(x::y::xys))
  loop xs ys id
person J D    schedule 04.11.2012
comment
Стиль передачи продолжения тайно строит замыкание довольно большого размера, поэтому он ничем не лучше, чем вариант без хвоста. - person Andrej Bauer; 05.11.2012
comment
@AndrejBauer не лучше версии без хвоста. Версия без хвоста дает сбой с переполнением стека при больших входных данных, чего нет в версии CPS. Наверно так лучше? - person J D; 05.11.2012
comment
Ах, тогда закрытие должно приземлиться на кучу? - person Andrej Bauer; 05.11.2012
comment
@AndrejBauer: Точно. Замыкания фактически образуют связанный список в куче, и все вызовы находятся в хвостовой позиции, поэтому потребление стека ограничено для произвольно длинных входных данных. Согласно моим измерениям, он на самом деле немного быстрее, чем паттерн-аккумулятор... - person J D; 05.11.2012
comment
на самом деле это немного быстрее, чем шаблон аккумулятора — это противоречит моему выводу. - person Daniel; 10.11.2012