valor recursividad ejemplos cola f# functional-programming tail-recursion memoization

f# - ejemplos - recursividad valor



Combinar memoria y recursiĆ³n de cola (5)

¿Es posible combinar la memorización y la recursión de cola de alguna manera? Estoy aprendiendo F # en este momento y entiendo ambos conceptos, pero no puedo combinarlos.

Supongamos que tengo la siguiente función memoize (de la Programación Funcional Real-World ):

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)

y la siguiente función factorial :

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

El factorial memoria no es demasiado difícil y hacer que sea recursivo por la cola tampoco:

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)

¿Pero puedes combinar memoria y recursión final? Hice algunos intentos pero parece que no puedo hacer que funcione. ¿O esto simplemente no es posible?


Como siempre, las continuaciones producen una elegante solución de 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

Hay dos tipos de pruebas. En primer lugar, esto demuestra que llamar a F (4) almacena en caché F (4), F (3), F (2), F (1) como lo desea.

Luego, comente el *** printf y elimine el comentario de la prueba final (y compile en modo Release) para mostrar que no contiene (usa tailcalls correctamente).

Tal vez generalice ''memoize'' y lo demuestre en ''fib'' next ...

EDITAR

Bien, aquí está el próximo paso, creo, desacoplando la memoria de 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

EDITAR

Ok, aquí hay una versión completamente generalizada que parece funcionar.

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


Escribí una prueba para visualizar la memorización. Cada punto es una llamada recursiva.

......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

La solución de kvb arroja los mismos resultados que la memoria recta como esta función.

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 )

Probar el código fuente

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


La dificultad de memorizar las funciones recursivas de la cola es, por supuesto, que cuando la función recursiva de la cola

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

se llama a sí mismo, no está permitido hacer nada con el resultado de la llamada recursiva, incluida la colocación en caché. Difícil; ¿Entonces, qué podemos hacer?

La idea crítica aquí es que, dado que la función recursiva no tiene permitido hacer nada con el resultado de una llamada recursiva, ¡el resultado de todos los argumentos para las llamadas recursivas será el mismo! Por lo tanto, si el seguimiento de llamadas de recursión es este

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

entonces para todo x en x0, x1, ..., xN el resultado de fx será el mismo, es decir, res. Por lo tanto, la última invocación de una función recursiva, la llamada no recursiva, conoce los resultados de todos los valores anteriores; está en posición de almacenarlos en caché. Lo único que debe hacer es pasarle una lista de valores visitados. Esto es lo que podría parecer factorial:

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)

¡Pero espera! El parámetro Look - l de fact0 contiene todos los argumentos para las llamadas recursivas a fact0 - ¡igual que la pila en una versión no recursiva! Eso es exactamente correcto. Cualquier algoritmo recursivo no cola se puede convertir a recursivo de cola moviendo la "lista de montones de pila" de pila a pila y convirtiendo el "postprocesamiento" de resultado de llamada recursiva en una caminata sobre esa estructura de datos.

Nota pragmática: el ejemplo factorial anterior ilustra una técnica general. Es bastante inútil como es - para la función factorial es suficiente para almacenar en caché el fact n alto nivel, porque el cálculo de fact n para una n particular solo golpea una serie única de pares de argumentos (n, res) a fact0 - si (n, 1) aún no está en la memoria caché, entonces ninguno de los pares fact0 a los que se llamará son.

Nótese que en este ejemplo, cuando pasamos de un factorial recursivo sin cola a un factorial recursivo de cola, explotamos el hecho de que la multiplicación es asociativa y conmutativa - el factorial recursivo de cola ejecuta un conjunto diferente de multiplicaciones que el de una cola no consecutiva uno recursivo

De hecho, existe una técnica general para pasar del algoritmo recursivo sin cola al recursivo de cola, que produce un algoritmo equivalente a un tee. Esta técnica se llama "transformación de paso continuo". Yendo por esa ruta, puede tomar un factorial de memorización sin cola recursiva y obtener un factorial de memorización de cola recursiva mediante una transformación mecánica. Vea la respuesta de Brian para la exposición de este método.


No estoy seguro de si hay una forma más sencilla de hacerlo, pero un enfoque sería crear un combinador de y para la memorización:

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

Luego, puede usar este combinador en lugar de "let rec", con el primer argumento que representa la función para llamar recursivamente:

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)

EDITAR

Como señaló Mitya, memoY no conserva las propiedades recursivas de cola del memoee. Aquí hay un combinador revisado que usa excepciones y estado mutable para memorizar cualquier función recursiva sin desbordar la pila (incluso si la función original no es recursiva en sí misma):

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]

Desafortunadamente, la maquinaria que se inserta en cada llamada recursiva es algo pesada, por lo que el rendimiento en entradas no memorizadas que requieren recursión profunda puede ser un poco lento. Sin embargo, en comparación con algunas otras soluciones, esto tiene el beneficio de que requiere cambios bastante mínimos a la expresión natural de las funciones recursivas:

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

EDITAR

Me extenderé un poco sobre cómo esto se compara con otras soluciones. Esta técnica aprovecha el hecho de que las excepciones proporcionan un canal lateral: una función de tipo ''a -> ''b realidad no necesita devolver un valor de tipo ''b , sino que puede salir mediante una excepción. No necesitaríamos usar excepciones si el tipo de devolución contiene explícitamente un valor adicional que indique la falla. Por supuesto, podríamos usar la ''b option como el tipo de retorno de la función para este propósito. Esto llevaría al siguiente combinador de memorización:

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]

Previamente, nuestro proceso de memorización se veía así:

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

Ahora, necesitamos incorporar el hecho de que fib debe devolver una int option lugar de una int . Dado un flujo de trabajo adecuado para los tipos de option , esto podría escribirse de la siguiente manera:

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

Sin embargo, si estamos dispuestos a cambiar el tipo de devolución del primer parámetro (de la int option int a int option en este caso), también podemos usar todo el camino y simplemente usar continuaciones en el tipo de devolución, como en la solución de Brian. Aquí hay una variación de sus definiciones:

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

Y nuevamente, si tenemos una expresión de cálculo adecuada para construir funciones CPS, podemos definir nuestra función recursiva de la siguiente manera:

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

Esto es exactamente lo mismo que Brian, pero creo que la sintaxis aquí es más fácil de seguir. Para que esto funcione, todo lo que necesitamos son las dos definiciones siguientes:

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


Eso debería funcionar si la recursividad de cola mutua a través de y no está creando marcos de pila:

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