Является ли эта функция F # хвостовой рекурсивной, когда рекурсивная функция вызывается несколько раз внутри функции?

Есть несколько вопросов о хвостовой рекурсивной функции, например: this и this, но не смог найти ничего похожего на следующее.

Насколько я понимаю, функция, оптимизированная для хвостового вызова, должна возвращать накопленное значение в своем последнем вызове без какой-либо дополнительной оценки. Это довольно легко понять, используя, например, факториальную функцию, которая оптимизируется в циклы 2. Но не всегда очевидно, что в других случаях, например, в дальнейшем, что это за последний звонок? Их много, так как функция вызывается в теле рекурсивно более одного раза.

Брайан предлагает способ узнать, но я не уверен, как сделать его оптимизированным по запросу. Я могу передать компилятору флаг --tailcalls, чтобы он делал это автоматически, но всегда ли это удается?

f и g возвращают один и тот же тип.

type T = T of int * T list

let rec myfunc f (T (x,xs)) =
    if (List.isEmpty xs) then f x
    else 
        List.fold g acc (List.map (fun xxs -> myfunc f xxs) xs)

Любая помощь в оптимизации приведенного выше кода будет принята с благодарностью.


person vis    schedule 28.05.2012    source источник


Ответы (2)


Как уже сказал Джон, ваша функция не является хвостовой рекурсивной. Основная проблема заключается в том, что ему необходимо рекурсивно вызывать себя несколько раз (один раз для каждого элемента в списке xs, что выполняется в лямбда-функции, переданной в List.map).

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

В следующем примере показаны обычная версия (слева) и продолжение на основе (справа)

let res = foo a b                          fooCont a b (fun res ->
printfn "Result: %d" res                     printfn "Result: %d" res)

Чтобы написать функцию в стиле передачи продолжения, вам также потребуется использовать fold функцию, основанную на продолжении. Сначала вы можете избежать использования map, переместив операцию, выполненную в map, в лямбда-функцию fold:

List.fold g acc (List.map (fun xxs -> myfunc f xxs) xs)

Становится:

List.fold (fun state xxs -> g state (myfunc f xxs)) acc xs

Затем вы можете переписать код следующим образом (обратите внимание, что и f, и g, которые вы не указали в своем вопросе, теперь являются функциями, основанными на продолжении, поэтому они принимают дополнительный аргумент, который представляет продолжение):

// The additional parameter 'cont' is the continuation to be called 
// when the final result of myfunc is computed
let rec myfunc' f (T (x,xs)) cont = 
  if (List.isEmpty xs) then 
    // Call the 'f' function to process the last element and give it the
    // current continuation (it will be called when 'f' calculates the result)
    f x cont
  else  
    // Using the continuation-based version of fold - the lambda now takes current
    // element 'xxs', the accumulated state and a continuation to call
    // when it finishes calculating 
    List.foldCont (fun xxs state cont -> 
      // Call 'myfunc' recursively - note, this is tail-call position now!
      myfunc' f xxs (fun res -> 
        // In the continuation, we perform the aggregation using 'g'
        // and the result is reported to the continuation that resumes
        // folding the remaining elements of the list.
        g state res cont)) acc xs cont

Функция List.foldCont является версией fold, основанной на продолжении, и может быть записана следующим образом:

module List = 
  let rec foldCont f (state:'TState) (list:'T list) cont =
    match list with
    | [] -> cont state
    | x::xs -> foldCont f state xs (fun state ->
        f x state cont)

Поскольку вы не опубликовали полный рабочий пример, я не смог действительно протестировать код, но я думаю, что он должен работать.

person Tomas Petricek    schedule 29.05.2012
comment
Разве в ответе вам не нужны mapCont или минусы? - person J D; 30.05.2012
comment
Мне потребовалось некоторое время, чтобы прочитать немного о стиле передачи продолжения и, наконец, я смог внести изменения в свой код. Однако я также обнаружил, что могу изменить свой код (который действительно урезан в вопросе выше), чтобы не выполнять несколько вызовов рекурсивной функции в теле. Но ответы, представленные здесь, действительно помогли. К этому стилю нужно привыкнуть. - person vis; 30.05.2012

Насколько я понимаю, функция, оптимизированная для хвостового вызова, должна возвращать накопленное значение в своем последнем вызове ...

Почти. Хвостовая рекурсия - это когда все рекурсивные вызовы появляются в хвостовой позиции. Положение хвоста означает, что вызывающий объект возвращает результат напрямую от вызываемого объекта.

в дальнейшем, что это за последний звонок?

В хвостовой позиции есть два захода. Сначала вызов f. Во-вторых, звонок List.fold. Рекурсивный вызов не находится в хвостовой позиции, потому что его возвращаемое значение не возвращается напрямую вызывающим.

if (List.isEmpty xs) then f x

Также используйте сопоставление с образцом вместо isEmpty и друзей.

Любая помощь в оптимизации приведенного выше кода будет принята с благодарностью.

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

person J D    schedule 28.05.2012