27 votes

Combiner memoization et la queue de la récursivité

Est-il possible de combiner memoization et la queue de la récursivité en quelque sorte? Je suis d'apprentissage F# au moment et à comprendre les deux concepts, mais ne semble pas possible de les combiner.

Supposons que j'ai la suite de memoize de la fonction (à partir du Monde Réel de la Programmation Fonctionnelle):

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)

et le lendemain factorial fonction de:

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

Memoizing factorial n'est pas trop difficile et la prise de l'arrière-récursive n'est pas:

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)

Mais pouvez-vous combiner memoization et la queue de la récursivité? J'ai fait quelques tentatives mais n'arrive pas à le faire fonctionner. Ou est-ce tout simplement pas possible?

20voto

Brian Points 82719

Comme toujours, les continuations de rendement d'un élégant tailcall solution:

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

Il existe deux types de tests. Tout d'abord, cette démos que l'appel de F(4) met en cache F(4), F(3), F(2), F(1) que vous le souhaitez.

Ensuite, commentaire de l' *** printf et décommentez la finale de test (et de les compiler en mode Release) pour montrer qu'elle n'a pas StackOverflow (il utilise tailcalls correctement).

Peut-être que je vais généraliser out "memoize' et les montrer sur " fib " suivant...

MODIFIER

Ok, voici la prochaine étape, je pense, le découplage memoization de factorielle:

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

MODIFIER

Ok, voici un totalement version généralisée qui semble fonctionner.

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

13voto

Mitya Points 1101

La situation de memoizing les fonctions récursives terminales est, bien sûr, que lorsque la queue-fonction récursive

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

appelle lui-même, il n'est pas autorisé à faire n'importe quoi avec un résultat de l'appel récursif, y compris la mise en cache. Délicat; alors, que pouvons-nous faire?

La vision critique ici, c'est que depuis la fonction récursive n'est pas autorisé à faire n'importe quoi avec un résultat de l'appel récursif, le résultat pour l'ensemble des arguments pour les appels récursifs sera le même! Par conséquent, si la récursivité trace d'appel est-ce

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

alors pour tout x de x0,x1,...,xN de la suite de l' f x sera le même, à savoir res. Ainsi, la dernière invocation d'une fonction récursive, le non-appel récursif, connaît les résultats pour toutes les valeurs précédentes -, il est en mesure de les mettre en cache. La seule chose que vous devez faire est de passer d'une liste de visité des valeurs. Voici ce que cela peut paraître, pour la factorielle:

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)

Mais attendez! Regardez - l paramètre de fact0 contient tous les arguments pour les appels récursifs à l' fact0 - tout comme la pile dans un non-récursives terminales version! C'est exactement ça. Tout non-queue algorithme récursif peut être converti en une queue-récursive par le déplacement de la "liste de la pile d'images" de la pile de segment et de la conversion de la "post-traitement" de l'appel récursif résultat dans une promenade au cours de cette structure de données.

Pragmatique remarque: La factorielle exemple ci-dessus illustre une technique générale. Il est tout à fait inutile pour fonction factorielle, il est tout à fait assez de mémoire cache de niveau supérieur fact n résultat, car le calcul de fact n pour un particulier n uniquement des résultats d'une série unique de (n,res) paires d'arguments à fact0 - si (n,1) n'est pas mis en cache pour l'instant, alors aucune des paires fact0 va être appelé sur sont.

Notez que dans cet exemple, quand nous sommes allés de non-queue-factorielle récursive pour une queue-factorielle récursive, nous avons exploité le fait que la multiplication est associative et commutative - queue-factorielle récursive exécuter un ensemble différent de multiplications qu'un non-queue-récursive.

En fait, une technique existe pour le passage du non-récursives terminales à la queue-algorithme récursif, ce qui donne un algorithme équivalent à un tee-shirt. Cette technique est appelée "continuatuion de passage de la transformation". Prendre ce chemin, vous pouvez prendre une non-récursives terminales memoizing factorielle et obtenir une queue-récursive memoizing factorielle par à peu près une transformation mécanique. Voir Brian de réponse pour l'exposition de cette méthode.

7voto

kvb Points 35490

Je ne sais pas si il y a un moyen plus simple de faire cela, mais une approche serait de créer un 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

Ensuite, vous pouvez utiliser cette combinator en lieu et place de "let rec", avec le premier argument représente la fonction à appeler récursivement:

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)

MODIFIER

Comme Mitya souligné, memoY ne préserve pas la queue récursive propriétés de la memoee. Voici une version révisée du combinator qui utilise les exceptions et mutable état de memoize toute fonction récursive sans débordement de la pile (même si la fonction d'origine n'est pas lui-même la queue récursive!):

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]

Malheureusement, la machine à laquelle est inséré dans chaque appel récursif est un peu lourd, de sorte que les performances de l'onu memoized entrées nécessitant une profondeur de récursivité peut être un peu lent. Cependant, par rapport à d'autres solutions, ce qui a l'avantage qu'il nécessite assez un minimum de changements de l'expression naturelle de fonctions récursives:

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

let _ = fib 5000

MODIFIER

Je vais développer un peu sur la façon dont cela se compare à d'autres solutions. Cette technique repose sur le fait que les exceptions de fournir un canal latéral: une fonction de type 'a -> 'b n'ont pas réellement besoin de retourner une valeur de type 'b, mais peut, au lieu de la sortie via une exception. Nous n'aurions pas besoin d'utiliser des exceptions si le type de retour de contenus explicitement une valeur supplémentaire indiquant l'échec. Bien sûr, on pourrait utiliser l' 'b option que le type de retour de la fonction à cet effet. Cela conduirait à la suite de memoizing combinator:

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]

Auparavant, notre memoization processus ressemblait à:

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

Maintenant, il faut tenir compte du fait qu' fib doit retourner un int option au lieu d'un int. Compte tenu d'un flux de travail adapté pour option types, ce qui pourrait être rédigé comme suit:

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

Cependant, si nous sommes prêts à changer le type de retour de la premier paramètre (à partir de int de int option dans ce cas), on peut aussi bien aller tout le chemin et de n'utiliser que des prolongements dans le type de retour au lieu de cela, comme dans celui de Brian solution. Voici une variation sur ses définitions:

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

Et encore une fois, si nous avons adapté le calcul de l'expression pour la construction de CPS fonctions, nous pouvons définir notre fonction récursive comme ceci:

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

C'est exactement la même chose que ce que Brian a fait, mais je trouve la syntaxe est plus facile à suivre. Pour faire ce travail, tous nous avons besoin sont les deux définitions suivantes:

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

let cps = CpsBuilder()

3voto

gradbot Points 9219

J'ai écrit un test pour visualiser la memoization. Chaque point est un appel récursif.

......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 la solution renvoie les mêmes résultats sont droites memoization comme cette fonction.

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
        )

Le code source de Test.

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

0voto

nicolas Points 2172

Cela devrait fonctionner si mutuelle de la queue de la récursivité par y ne sont pas de la création de la pile d'images:

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X