Комбинация memoization и tail-recursion

Можно ли как-то совместить воспоминания и хвостовую рекурсию? Я изучаю 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)

Запоминание 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)

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

Ответы

Ответ 1

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

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), чтобы показать, что он не StackOverflow (он правильно использует хвосты).

Возможно, я обобщу 'memoize' и продемонстрирую его на следующем рисунке.

ИЗМЕНИТЬ

Хорошо, вот следующий шаг, я думаю, развязывание memoization от factorial:

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

Ответ 2

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

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

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

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

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

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

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 не будет вызвана.

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

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

Ответ 3

Я не уверен, есть ли более простой способ сделать это, но одним из способов было бы создать memoizing y-combinator:

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 не сохраняет хвостовые рекурсивные свойства memoee. Здесь пересмотренный комбинатор, который использует исключения и изменяемое состояние для memoize любой рекурсивной функции без (даже если исходная функция не является самой хвостовой рекурсивной!):

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]

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

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]

Раньше процесс memoization выглядел так:

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

Ответ 4

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

......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 возвращает те же результаты, что и прямая memoization, как эта функция.

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

Ответ 5

Это должно работать, если взаимная рекурсия хвоста через 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