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

В этом нет волшебства. Булевы выражения, такие как a || b || c || d, оцениваются лениво. Interpeter ищет значение a, он не определен, поэтому он является ложным, поэтому он перемещается, а затем видит b, который является нулевым, что по-прежнему дает ложный результат, поэтому он перемещается, а затем видит c - ту же историю. Наконец, он видит d и говорит: «Да, это не null, поэтому у меня есть результат», и он присваивает его конечной переменной.

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

30
задан Ronald Wildenberg 11 August 2010 в 19:31
поделиться

4 ответа

Как всегда, продолжения дают элегантное решение 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 (он правильно использует tailcalls).

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

EDIT

Хорошо, вот следующий шаг, я думаю, отвязка memoize от 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

EDIT

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

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
23
ответ дан 27 November 2019 в 23:59
поделиться

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

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

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

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

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 - точно так же, как стек в нехвостовой рекурсивной версии! Это совершенно верно. Любой нехвостовой рекурсивный алгоритм может быть преобразован в хвостовой рекурсивный путем перемещения "списка кадров стека" из стека в кучу и преобразования "постобработки" результата рекурсивного вызова в проход по этой структуре данных.

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

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

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

15
ответ дан 27 November 2019 в 23:59
поделиться

Я не уверен, что есть более простой способ сделать это, но один из подходов - создать мемоизирующий 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)

EDIT

Как указал Митя, 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

EDIT

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

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 вместо 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()
8
ответ дан 27 November 2019 в 23:59
поделиться

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

......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
3
ответ дан 27 November 2019 в 23:59
поделиться
Другие вопросы по тегам:

Похожие вопросы: