Je travaille sur l’implémentation de l’algorithme UCT dans Haskell, ce qui nécessite beaucoup de jonglage de données. Sans entrer dans trop de détails, c’est un algorithme de simulation où, à chaque “étape”, un nœud feuille dans l’arbre de recherche est sélectionné en fonction de certaines propriétés statistiques, un nouveau nœud enfant est construit à cette feuille et les statistiques correspondant à la nouvelle feuille et tous ses ancêtres sont mis à jour.
Compte tenu de tout ce jonglage, je ne suis pas assez précis pour comprendre comment créer une structure de données immuable à l’ Okasaki dans l’ensemble de l’arbre de recherche. Au lieu de cela, je joue un peu avec la monade ST
, créant des structures composées de STRef
mutables. Un exemple artificiel (sans rapport avec UCT):
import Control.Monad import Control.Monad.ST import Data.STRef data STRefPair sab = STRefPair { left :: STRef sa, right :: STRef sb } mkStRefPair :: a -> b -> ST s (STRefPair sab) mkStRefPair ab = do a' <- newSTRef a b' STRefPair sab -> ST s () derp p = do modifySTRef (left p) (\x -> x + 1) modifySTRef (right p) (\x -> x - 1) herp :: (Num a, Num b) => (a, b) herp = runST $ do p <- mkStRefPair 0 0 replicateM_ 10 $ derp p a <- readSTRef $ left p b <- readSTRef $ right p return (a, b) main = print herp -- should print (10, -10)
Evidemment, cet exemple particulier serait beaucoup plus facile à écrire sans utiliser ST
, mais j’espère que cela va de soi. Si je devais appliquer ce type de style à mon cas d’utilisation UCT, est-ce que c’est faux?
Quelqu’un a posé une question similaire il y a quelques années, mais je pense que ma question est un peu différente … Je n’ai aucun problème à utiliser les monades pour encapsuler l’état mutable lorsque cela est approprié, mais c’est la clause qui me convient. Je suis inquiet que je revienne prématurément à une mentalité orientée object, où j’ai un tas d’objects avec des getters et des setters. Haskell pas exactement idiomatique …
D’un autre côté, si c’est un style de codage raisonnable pour certains problèmes, je suppose que ma question est la suivante: existe-t-il des moyens bien connus de garder ce type de code lisible et maintenable? Je suis en quelque sorte grossi par toutes les lectures et écritures explicites, et particulièrement grossières par le fait de devoir STRef
mes structures STRef
sur STRef
dans la monade ST
en structures isomorphes mais immuables en dehors.
Je n’utilise pas beaucoup ST, mais c’est parfois la meilleure solution. Cela peut être dans de nombreux scénarios:
Lorsque j’utilise ST (et d’autres monades), j’essaie de suivre ces directives générales:
STRef s (Map k [v])
utiliser des choses comme STRef s (Map k [v])
. La carte elle-même est en cours de mutation, mais une grande partie des opérations lourdes est effectuée purement. IORef
avec STRef
et IO
avec ST
dans Data.HashTable était beaucoup plus facile qu’écrire une implémentation de table de hachage codée à la main, et probablement plus rapide. Une dernière remarque – si vous rencontrez des problèmes avec les lectures et écritures explicites, il existe des moyens de contourner le problème.
Algorithmes utilisant des mutations et des algorithmes différents des algorithmes. Parfois, il y a une traduction qui préserve les limites de la première à la seconde, parfois difficile, et parfois une seule qui ne préserve pas les limites de la complexité.
Un survol du document me révèle que je ne pense pas que cela fasse un usage essentiel de la mutation – et je pense donc qu’un algorithme fonctionnel paresseux, potentiellement très astucieux, pourrait être développé. Mais ce serait un algorithme différent mais lié à celui décrit.
Ci-dessous, je décris une telle approche – pas nécessairement la meilleure ou la plus intelligente, mais plutôt simple:
Voici la configuration que je comprends – A) un arbre de twigment est construit B) les gains sont ensuite repoussés des feuilles à la racine, ce qui indique le meilleur choix à chaque étape. Mais cela coûte cher. Au lieu de cela, seules des portions de l’arbre sont explorées sur les feuilles de manière non déterministe. De plus, chaque exploration ultérieure de l’arbre est déterminée par ce qui a été appris lors des explorations précédentes.
Nous construisons donc du code pour décrire l’arbre “par étapes”. Ensuite, nous avons une autre structure de données pour définir un arbre partiellement exploré avec des estimations de récompense partielles. Nous avons alors une fonction de randseed -> ptree -> ptree
qui donne une graine aléatoire et un arbre partiellement exploré, entreprend une autre exploration de l’arbre, mettant à jour la structure de ptree au fur et à mesure. Ensuite, nous pouvons simplement itérer cette fonction sur un ptree de graine vide pour obtenir une liste d’espaces de plus en plus échantillonnés dans le ptree. Nous pouvons alors parcourir cette liste jusqu’à ce qu’une condition de coupure spécifiée soit remplie.
Donc, maintenant, nous sums passés d’un algorithme où tout est mélangé à trois étapes distinctes: 1) construire l’arborescence complète, paresseusement, 2) mettre à jour une exploration partielle avec un échantillonnage d’une structure et 3) décider quand nous avons recueilli suffisamment d’échantillons.
Il peut être très difficile de savoir quand utiliser ST est approprié. Je vous suggère de le faire avec ST et sans ST (pas nécessairement dans cet ordre). Gardez la version non-ST simple; L’utilisation de ST doit être considérée comme une optimisation, et vous ne voulez pas le faire tant que vous n’en avez pas besoin.
Je dois admettre que je ne peux pas lire le code Haskell. Mais si vous utilisez ST pour muter l’arbre, vous pouvez probablement le remplacer par un arbre immuable sans perdre beaucoup car:
Vous devez muter chaque nœud au-dessus de la nouvelle feuille. Un arbre immuable doit remplacer tous les nœuds au-dessus du nœud modifié. Dans les deux cas, les nœuds touchés sont les mêmes, vous ne gagnez donc rien en complexité.
Par exemple, la création d’objects Java est plus coûteuse que la mutation, alors peut-être que vous pouvez gagner un peu ici en utilisant la mutation. Mais cela je ne sais pas avec certitude. Mais un petit gain ne vous achète pas beaucoup à cause du prochain point.
L’évaluation de la nouvelle feuille sera probablement beaucoup plus coûteuse que la mise à jour de l’arborescence. C’est du moins le cas pour UCT dans l’ordinateur Go.
L’utilisation de la monade ST est généralement (mais pas toujours) une optimisation. Pour toute optimisation, j’applique la même procédure:
L’autre cas d’utilisation que je connais est une alternative à la monade d’état. La principale différence est que, avec la monade d’état, le type de toutes les données stockées est spécifié de manière descendante, tandis que la monade ST est spécifiée de bas en haut. Il y a des cas où cela est utile.