38 votes

Comment fonctionne Data.MemoCombinators ?

J'ai regardé la source pour Data.MemoCombinateurs mais je ne vois pas vraiment où est le cœur de l'affaire.

S'il vous plaît, expliquez-moi la logique derrière tous ces combinateurs et la mécanique de la façon dont ils fonctionnent réellement pour accélérer votre programme dans le monde réel de la programmation.

Je suis à la recherche de détails pour este implémentation, et éventuellement comparaison/contraste avec d'autres approches Haskell de la mémorisation. Je comprends ce qu'est la mémorisation et je suis no Je cherche une description de son fonctionnement général.

59voto

luqui Points 26009

Cette bibliothèque est une combinatoire directe de la technique bien connue de la mémorisation. Commençons par l'exemple canonique :

fib = (map fib' [0..] !!)
    where
    fib' 0 = 0
    fib' 1 = 1
    fib' n = fib (n-1) + fib (n-2)

J'interprète ce que vous avez dit comme signifiant que vous savez comment et pourquoi cela fonctionne. Je vais donc me concentrer sur la combinatoire.

Nous essayons essentiellement de capturer et de généraliser l'idée de (map f [0..] !!) . Le type de cette fonction est (Int -> r) -> (Int -> r) ce qui est logique : il prend une fonction de Int -> r et renvoie une version mémorisée de la même fonction. Toute fonction qui est sémantiquement l'identité et qui a ce type est appelée un "memoizer for Int "(même id qui ne mémorise pas). Nous généralisons à cette abstraction :

type Memo a = forall r. (a -> r) -> (a -> r)

Donc un Memo a un mémorisateur pour a prend une fonction de a à n'importe quoi, et retourne une fonction sémantiquement identique qui a été mémorisée (ou non).

L'idée des différents memoizers est de trouver un moyen d'énumérer le domaine avec une structure de données, de mapper la fonction sur eux, puis d'indexer la structure de données. bool est un bon exemple :

bool :: Memo Bool
bool f = table (f True, f False)
    where
    table (t,f) True = t
    table (t,f) False = f

Fonctions de Bool sont équivalentes aux paires, sauf qu'une paire n'évaluera chaque composant qu'une seule fois (comme c'est le cas pour chaque valeur qui apparaît en dehors d'une lambda). Nous nous contentons donc de faire un mapping vers une paire et inversement. Le point essentiel est que nous élevons l'évaluation de la fonction au-dessus de la lambda de l'argument (ici le dernier argument de table ) en énumérant le domaine.

Memoizing Maybe a est une histoire similaire, sauf que maintenant nous devons savoir comment mémoriser a pour le Just cas. Donc le mémorisateur pour Maybe prend un mémorisateur pour a comme argument :

maybe :: Memo a -> Memo (Maybe a)
maybe ma f = table (f Nothing, ma (f . Just))
    where
    table (n,j) Nothing = n
    table (n,j) (Just x) = j x

Le reste de la bibliothèque n'est que variations sur ce thème.

La façon dont il mémorise les types intégraux utilise une structure plus appropriée que celle de [0..] . C'est un peu compliqué, mais il s'agit en fait de créer un arbre infini (en représentant les nombres en binaire pour clarifier la structure) :

1
  10
    100
      1000
      1001
    101
      1010
      1011
  11
    110
      1100
      1101
    111
      1110
      1111

Ainsi, la recherche d'un nombre dans l'arbre a un temps d'exécution proportionnel au nombre de bits dans sa représentation.

Comme le souligne sclv, la bibliothèque MemoTrie de Conal utilise la même technique sous-jacente, mais utilise une présentation par classes de types au lieu d'une présentation par combinateurs. Nous avons publié nos bibliothèques indépendamment au même moment (en fait, en quelques heures !). La bibliothèque de Conal est plus facile à utiliser dans des cas simples (il n'y a qu'une seule fonction, memo et il déterminera la structure de mémo à utiliser en fonction du type), alors que le mien est plus flexible, car vous pouvez faire des choses comme ceci :

boundedMemo :: Integer -> Memo Integer
boundedMemo bound f = \z -> if z < bound then memof z else f z
   where
   memof = integral f

Qui ne mémorise que les valeurs inférieures à une limite donnée, nécessaire pour l'implémentation d'un des problèmes d'euler du projet.

Il existe d'autres approches, par exemple l'exposition d'une fonction de point fixe ouverte sur une monade :

memo :: MonadState ... m => ((Integer -> m r) -> (Integer -> m r)) -> m (Integer -> m r)

Ce qui permet encore plus de flexibilité, par exemple la purge des caches, LRU, etc. Mais c'est une douleur dans le cul à utiliser, et aussi il met des contraintes de rigueur sur la fonction à mémoriser (par exemple pas de récursion infinie à gauche). Je ne crois pas qu'il y ait de bibliothèques qui implémentent cette technique.

Cela a-t-il répondu à votre question ? Si ce n'est pas le cas, peut-être pouvez-vous expliciter les points qui vous laissent perplexe ?

18voto

Daniel Velkov Points 9244

Le cœur est le bits fonction :

-- | Memoize an ordered type with a bits instance.
bits :: (Ord a, Bits a) => Memo a
bits f = IntTrie.apply (fmap f IntTrie.identity)

C'est la seule fonction (à l'exception de la fonction triviale unit :: Memo () ) qui peut vous donner un Memo a valeur. Il utilise la même idée que dans ce page sur la mémorisation en Haskell. La section 2 montre la stratégie de mémorisation la plus simple à l'aide d'une liste et la section 3 fait de même à l'aide d'un arbre binaire de naturels semblable au IntTree utilisés dans les mémocombinateurs.

L'idée de base est d'utiliser une construction comme (map fib [0 ..] !!) ou dans le cas des memocombinateurs - IntTrie.apply (fmap f IntTrie.identity) . Ce qu'il faut remarquer ici, c'est la correspondance entre IntTie.apply y !! et aussi entre IntTrie.identity y [0..] .

L'étape suivante consiste à mémoriser les fonctions avec d'autres types d'arguments. Ceci est fait avec la fonction wrap qui utilise un isomorphisme entre les types a y b pour construire un Memo b d'un Memo a . Par exemple :

Memo.integral f
=>
wrap fromInteger toInteger bits f
=>
bits (f . fromInteger) . toInteger
=>
IntTrie.apply (fmap (f . fromInteger) IntTrie.identity) . toInteger
~> (semantically equivalent)
(map (f . fromInteger) [0..] !!) . toInteger

Le reste du code source traite des types tels que List, Maybe, Either et la mémorisation d'arguments multiples.

7voto

sclv Points 25335

Une partie du travail est effectuée par IntTrie : http://hackage.haskell.org/package/data-inttrie-0.0.4

La bibliothèque de Luke est une variante de la bibliothèque MemoTrie de Conal, qu'il a décrite ici : http://conal.net/blog/posts/elegant-memoization-with-functional-memo-tries/

Un peu plus d'expansion -- la notion générale derrière la mémorisation fonctionnelle est de prendre une fonction de a -> b et le mapper à travers une structure de données indexée par tout ce qui est possible valeurs de a et contenant des valeurs de b . Une telle structure de données devrait être paresseuse de deux façons : d'abord, elle devrait être paresseuse dans les valeurs qu'elle contient. Deuxièmement, elle devrait être produite paresseusement elle-même. La première est par défaut dans un langage non strict. Le dernier est accompli en utilisant des essais généralisés.

Les différentes approches des memocombinateurs, memotrie, etc. sont toutes des moyens de créer des compositions de morceaux d'essais sur des types individuels de structures de données pour permettre la construction simple d'essais pour des structures de plus en plus complexes.

0voto

jejansse Points 26

@luqui Une chose qui n'est pas claire pour moi : est-ce que cela a le même comportement opérationnel que ce qui suit :

fib :: [Int]
fib = map fib' [0..]
    where fib' 0 = 0
             fib' 1 = 1
             fib' n = fib!!(n-1) + fib!!(n-2)

Ce qui précède devrait mémoriser la fibre au niveau supérieur, et donc si vous définissez deux fonctions :

f n = fib!!n + fib!!(n+1)

Si nous calculons alors f 5 nous obtenons que Fibre 5 n'est pas recalculé lors du calcul de Fibre 6 . Il n'est pas clair pour moi si les combinateurs de mémorisation ont le même comportement (c'est-à-dire la mémorisation au niveau supérieur au lieu d'interdire seulement le recalcul "à l'intérieur" du calcul de la fibre), et si oui, pourquoi exactement ?

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