Комбинат запоминания и хвостовой рекурсии

Можно ли как-то совместить запоминание и хвостовую рекурсию? Сейчас я изучаю F# и понимаю обе концепции, но не могу их объединить.

Предположим, у меня есть следующее memoize функция (из функционального программирования реального мира):

let memoize f = let cache = new Dictionary<_, _>()
                (fun x -> match cache.TryGetValue(x) with
                          | true, y -> y
                          | _       -> let v = f(x)
                                       cache.Add(x, v)
                                       v)

и следующее factorial функция:

let rec factorial(x) = if (x = 0) then 1 else x * factorial(x - 1)

Memoizing factorial не так уж сложно и сделать его хвост-рекурсивным тоже не так:

let rec memoizedFactorial =
  memoize (fun x -> if (x = 0) then 1 else x * memoizedFactorial(x - 1))

let tailRecursiveFactorial(x) =
  let rec factorialUtil(x, res) = if (x = 0)
                                  then res
                                  else let newRes = x * res
                                       factorialUtil(x - 1, newRes)
  factorialUtil(x, 1)

Но можете ли вы совместить запоминание и хвостовую рекурсию? Я сделал несколько попыток, но не могу заставить его работать. Или это просто невозможно?

5 ответов

Решение

Как всегда, продолжения дают элегантное решение:

open System.Collections.Generic 

let cache = Dictionary<_,_>()  // TODO move inside 
let memoizedTRFactorial =
    let rec fac n k =  // must make tailcalls to k
        match cache.TryGetValue(n) with
        | true, r -> k r
        | _ -> 
            if n=0 then
                k 1
            else
                fac (n-1) (fun r1 ->
                    printfn "multiplying by %d" n  //***
                    let r = r1 * n
                    cache.Add(n,r)
                    k r)
    fun n -> fac n id

printfn "---"
let r = memoizedTRFactorial 4
printfn "%d" r
for KeyValue(k,v) in cache do
    printfn "%d: %d" k v

printfn "---"
let r2 = memoizedTRFactorial 5
printfn "%d" r2

printfn "---"

// comment out *** line, then run this
//let r3 = memoizedTRFactorial 100000
//printfn "%d" r3

Есть два вида тестов. Во-первых, это демонстрации того, что вызов F(4) кэширует F(4), F(3), F(2), F(1), как вам бы хотелось.

Затем закомментируйте *** printf и раскомментируйте последний тест (и скомпилируйте в режиме Release), чтобы показать, что он не Stackru (он правильно использует tailcalls).

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

РЕДАКТИРОВАТЬ

Хорошо, вот следующий шаг, я думаю, отделяющий запоминание от факториала:

open System.Collections.Generic 

let cache = Dictionary<_,_>()  // TODO move inside 
let memoize fGuts n =
    let rec newFunc n k =  // must make tailcalls to k
        match cache.TryGetValue(n) with
        | true, r -> k r
        | _ -> 
            fGuts n (fun r ->
                        cache.Add(n,r)
                        k r) newFunc
    newFunc n id 
let TRFactorialGuts n k memoGuts =
    if n=0 then
        k 1
    else
        memoGuts (n-1) (fun r1 ->
            printfn "multiplying by %d" n  //***
            let r = r1 * n
            k r) 

let memoizedTRFactorial = memoize TRFactorialGuts 

printfn "---"
let r = memoizedTRFactorial 4
printfn "%d" r
for KeyValue(k,v) in cache do
    printfn "%d: %d" k v

printfn "---"
let r2 = memoizedTRFactorial 5
printfn "%d" r2

printfn "---"

// comment out *** line, then run this
//let r3 = memoizedTRFactorial 100000
//printfn "%d" r3

РЕДАКТИРОВАТЬ

Хорошо, вот полностью обобщенная версия, которая, кажется, работает.

open System.Collections.Generic 

let memoize fGuts =
    let cache = Dictionary<_,_>()
    let rec newFunc n k =  // must make tailcalls to k
        match cache.TryGetValue(n) with
        | true, r -> k r
        | _ -> 
            fGuts n (fun r ->
                        cache.Add(n,r)
                        k r) newFunc
    cache, (fun n -> newFunc n id)
let TRFactorialGuts n k memoGuts =
    if n=0 then
        k 1
    else
        memoGuts (n-1) (fun r1 ->
            printfn "multiplying by %d" n  //***
            let r = r1 * n
            k r) 

let facCache,memoizedTRFactorial = memoize TRFactorialGuts 

printfn "---"
let r = memoizedTRFactorial 4
printfn "%d" r
for KeyValue(k,v) in facCache do
    printfn "%d: %d" k v

printfn "---"
let r2 = memoizedTRFactorial 5
printfn "%d" r2

printfn "---"

// comment out *** line, then run this
//let r3 = memoizedTRFactorial 100000
//printfn "%d" r3

let TRFibGuts n k memoGuts =
    if n=0 || n=1 then
        k 1
    else
        memoGuts (n-1) (fun r1 ->
            memoGuts (n-2) (fun r2 ->
                printfn "adding %d+%d" r1 r2 //%%%
                let r = r1+r2
                k r)) 
let fibCache, memoizedTRFib = memoize TRFibGuts 
printfn "---"
let r5 = memoizedTRFib 4
printfn "%d" r5
for KeyValue(k,v) in fibCache do
    printfn "%d: %d" k v

printfn "---"
let r6 = memoizedTRFib 5
printfn "%d" r6

printfn "---"

// comment out %%% line, then run this
//let r7 = memoizedTRFib 100000
//printfn "%d" r7

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

let f x = 
   ......
   f x1

самому вызову запрещается делать что-либо с результатом рекурсивного вызова, в том числе помещать его в кеш. Tricky; Так что мы можем сделать?

Критическое понимание здесь заключается в том, что поскольку рекурсивной функции не разрешается делать что-либо с результатом рекурсивного вызова, результат для всех аргументов рекурсивных вызовов будет одинаковым! Поэтому, если трассировка рекурсивного вызова это

f x0 -> f x1 -> f x2 -> f x3 -> ... -> f xN -> res

тогда для всех x в x0,x1,...,xN результат f x будет такой же, а именно рез. Таким образом, последний вызов рекурсивной функции, нерекурсивный вызов, знает результаты для всех предыдущих значений - он может их кэшировать. Единственное, что вам нужно сделать, это передать ему список посещенных значений. Вот что может выглядеть для факториала:

let cache = Dictionary<_,_>()

let rec fact0 l ((n,res) as arg) = 
    let commitToCache r = 
        l |> List.iter  (fun a -> cache.Add(a,r))
    match cache.TryGetValue(arg) with
    |   true, cachedResult -> commitToCache cachedResult; cachedResult
    |   false, _ ->
            if n = 1 then
                commitToCache res
                cache.Add(arg, res)
                res
            else
                fact0 (arg::l) (n-1, n*res)

let fact n = fact0 [] (n,1)

Но ждать! Посмотрите - l параметр fact0 содержит все аргументы для рекурсивных вызовов fact0 - точно так же, как стек в нерекурсивной версии! Это совершенно верно. Любой не хвостовой рекурсивный алгоритм можно преобразовать в хвостовой рекурсивный, переместив "список кадров стека" из стека в кучу и преобразовав "постобработку" результата рекурсивного вызова в обход этой структуры данных.

Прагматическое примечание: приведенный выше пример факториала иллюстрирует общую технику. Это совершенно бесполезно как таковое - для факториальной функции вполне достаточно кэшировать верхний уровень fact n результат, потому что расчет fact n для определенного n только попадает в уникальную серию (n, res) пар аргументов для fact0 - если (n,1) еще не кешируется, то ни одна из пар fact0 не будет вызвана.

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

Фактически, существует общий метод перехода от нерекурсивного к хвостовому рекурсивному алгоритму, который дает алгоритм, эквивалентный тройнику. Эта техника называется "преобразованием, проходящим через продолжение". Пройдя по этому пути, вы можете взять нерекурсивный факториал для запоминания и получить хвостовой рекурсивный факториал для запоминания с помощью механического преобразования. См. Ответ Брайана для ознакомления с этим методом.

Я не уверен, есть ли более простой способ сделать это, но один из подходов - создать запоминающий y-комбинатор:

let memoY f =
  let cache = Dictionary<_,_>()
  let rec fn x =
    match cache.TryGetValue(x) with
    | true,y -> y
    | _ -> let v = f fn x
           cache.Add(x,v)
           v
  fn

Затем вы можете использовать этот комбинатор вместо "let rec", с первым аргументом, представляющим функцию, вызываемую рекурсивно:

let tailRecFact =
  let factHelper fact (x, res) = 
    printfn "%i,%i" x res
    if x = 0 then res 
    else fact (x-1, x*res)
  let memoized = memoY factHelper
  fun x -> memoized (x,1)

РЕДАКТИРОВАТЬ

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

let memoY f =
  let cache = Dictionary<_,_>()
  fun x ->
    let l = ResizeArray([x])
    while l.Count <> 0 do
      let v = l.[l.Count - 1]
      if cache.ContainsKey(v) then l.RemoveAt(l.Count - 1)
      else
        try
          cache.[v] <- f (fun x -> 
            if cache.ContainsKey(x) then cache.[x] 
            else 
              l.Add(x)
              failwith "Need to recurse") v
        with _ -> ()
    cache.[x]

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

let fib = memoY (fun fib n -> 
  printfn "%i" n; 
  if n <= 1 then n 
  else (fib (n-1)) + (fib (n-2)))

let _ = fib 5000

РЕДАКТИРОВАТЬ

Я немного расскажу о том, как это можно сравнить с другими решениями. Этот метод использует преимущество того факта, что исключения предоставляют побочный канал: функция типа 'a -> 'b на самом деле не нужно возвращать значение типа 'b, но вместо этого может выйти через исключение. Нам не нужно было бы использовать исключения, если тип возвращаемого значения явно содержал дополнительное значение, указывающее на ошибку. Конечно, мы могли бы использовать 'b option в качестве типа возврата функции для этой цели. Это привело бы к следующему комбинатору запоминания:

let memoO f =
  let cache = Dictionary<_,_>()
  fun x ->
    let l = ResizeArray([x])
    while l.Count <> 0 do
      let v = l.[l.Count - 1]
      if cache.ContainsKey v then l.RemoveAt(l.Count - 1)
      else
        match f(fun x -> if cache.ContainsKey x then Some(cache.[x]) else l.Add(x); None) v with
        | Some(r) -> cache.[v] <- r; 
        | None -> ()
    cache.[x]

Ранее наш процесс запоминания выглядел так:

fun fib n -> 
  printfn "%i" n; 
  if n <= 1 then n 
  else (fib (n-1)) + (fib (n-2))
|> memoY

Теперь нам нужно учесть тот факт, что fib должен вернуть int option вместо int, Учитывая подходящий рабочий процесс для option типы, это можно записать следующим образом:

fun fib n -> option {
  printfn "%i" n
  if n <= 1 then return n
  else
    let! x = fib (n-1)
    let! y = fib (n-2)
    return x + y
} |> memoO

Однако, если мы хотим изменить тип возврата первого параметра (с int в int option в этом случае), мы можем также пройти весь путь и использовать взамен продолжения в типе возврата, как в решении Брайана. Вот вариант его определения:

let memoC f =
  let cache = Dictionary<_,_>()
  let rec fn n k =
    match cache.TryGetValue(n) with
    | true, r -> k r
    | _ -> 
        f fn n (fun r ->
          cache.Add(n,r)
          k r)
  fun n -> fn n id

И снова, если у нас есть подходящее вычислительное выражение для построения функций CPS, мы можем определить нашу рекурсивную функцию следующим образом:

fun fib n -> cps {
  printfn "%i" n
  if n <= 1 then return n
  else
    let! x = fib (n-1)
    let! y = fib (n-2)
    return x + y
} |> memoC

Это в точности то же самое, что сделал Брайан, но я считаю, что здесь проще следовать синтаксису. Для этого нам понадобятся только два следующих определения:

type CpsBuilder() =
  member this.Return x k = k x
  member this.Bind(m,f) k = m (fun a -> f a k)

let cps = CpsBuilder()

Я написал тест для визуализации памятки. Каждая точка является рекурсивным вызовом.

......720 // factorial 6
......720 // factorial 6
.....120  // factorial 5

......720 // memoizedFactorial 6
720       // memoizedFactorial 6
120       // memoizedFactorial 5

......720 // tailRecFact 6
720       // tailRecFact 6
.....120  // tailRecFact 5

......720 // tailRecursiveMemoizedFactorial 6
720       // tailRecursiveMemoizedFactorial 6
.....120  // tailRecursiveMemoizedFactorial 5

Решение kvb возвращает те же результаты, что и прямое запоминание, как эта функция.

let tailRecursiveMemoizedFactorial = 
    memoize 
        (fun x ->
            let rec factorialUtil x res = 
                if x = 0 then 
                    res
                else 
                    printf "." 
                    let newRes = x * res
                    factorialUtil (x - 1) newRes

            factorialUtil x 1
        )

Протестируйте исходный код.

open System.Collections.Generic

let memoize f = 
    let cache = new Dictionary<_, _>()
    (fun x -> 
        match cache.TryGetValue(x) with
        | true, y -> y
        | _ -> 
            let v = f(x)
            cache.Add(x, v)
            v)

let rec factorial(x) = 
    if (x = 0) then 
        1 
    else
        printf "." 
        x * factorial(x - 1)

let rec memoizedFactorial =
    memoize (
        fun x -> 
            if (x = 0) then 
                1 
            else 
                printf "."
                x * memoizedFactorial(x - 1))

let memoY f =
  let cache = Dictionary<_,_>()
  let rec fn x =
    match cache.TryGetValue(x) with
    | true,y -> y
    | _ -> let v = f fn x
           cache.Add(x,v)
           v
  fn

let tailRecFact =
  let factHelper fact (x, res) = 
    if x = 0 then 
        res 
    else
        printf "." 
        fact (x-1, x*res)
  let memoized = memoY factHelper
  fun x -> memoized (x,1)

let tailRecursiveMemoizedFactorial = 
    memoize 
        (fun x ->
            let rec factorialUtil x res = 
                if x = 0 then 
                    res
                else 
                    printf "." 
                    let newRes = x * res
                    factorialUtil (x - 1) newRes

            factorialUtil x 1
        )

factorial 6 |> printfn "%A"
factorial 6 |> printfn "%A"
factorial 5 |> printfn "%A\n"

memoizedFactorial 6 |> printfn "%A"
memoizedFactorial 6 |> printfn "%A"
memoizedFactorial 5 |> printfn "%A\n"

tailRecFact 6 |> printfn "%A"
tailRecFact 6 |> printfn "%A"
tailRecFact 5 |> printfn "%A\n"

tailRecursiveMemoizedFactorial 6 |> printfn "%A"
tailRecursiveMemoizedFactorial 6 |> printfn "%A"
tailRecursiveMemoizedFactorial 5 |> printfn "%A\n"

System.Console.ReadLine() |> ignore

Это должно работать, если взаимная хвостовая рекурсия через y не создает стековые фреймы:

let rec y f x = f (y f) x

let memoize (d:System.Collections.Generic.Dictionary<_,_>) f n = 
   if d.ContainsKey n then d.[n] 
   else d.Add(n, f n);d.[n]

let rec factorialucps factorial' n cont = 
    if n = 0I then cont(1I) else factorial' (n-1I) (fun k -> cont (n*k))

let factorialdpcps  = 
    let d =  System.Collections.Generic.Dictionary<_, _>()
    fun n ->  y (factorialucps >> fun f n -> memoize d f n ) n id


factorialdpcps 15I //1307674368000
Другие вопросы по тегам