Tout d'abord, je recommande de regarder Data.Vector , une alternative plus agréable à Data.Array dans certains cas.
Array
et Vector
sont idéales pour certains cas de mémorisation, comme démontré dans ma réponse à "Trouver des chemins maximaux" . Cependant, certains problèmes ne sont tout simplement pas faciles à exprimer dans un style fonctionnel. Par exemple, le problème 28 du projet Euler appelle à additionner les nombres sur les diagonales d'une spirale. Bien sûr, il devrait être assez facile de trouver une formule pour ces nombres, mais la construction de la spirale est plus difficile.
Data.Array.ST fournit un type de tableau mutable. Cependant, la situation de type est un gâchis: il utilise une classe MArray pour surcharger chacune de ses méthodes à l'exception de runSTArray . Donc, à moins que vous ne prévoyiez de retourner un tableau immuable à partir d'une action de tableau mutable, vous devrez ajouter une ou plusieurs signatures de type:
import Control.Monad.ST
import Data.Array.ST
foo :: Int -> [Int]
foo n = runST $ do
a <- newArray (1,n) 123 :: ST s (STArray s Int Int) -- this type signature is required
sequence [readArray a i | i <- [1..n]]
main = print $ foo 5
Néanmoins, ma solution à Euler 28 s'est avérée assez bien et ne nécessitait pas cette signature de type parce que je l'ai utilisé runSTArray
.
Utilisation de Data.Map comme un "tableau mutable"
Si vous cherchez à implémenter un algorithme de tableau mutable, une autre option consiste à utiliser Data.Map . Lorsque vous utilisez un tableau, vous souhaitez en quelque sorte avoir une fonction comme celle-ci, qui change un seul élément d'un tableau:
writeArray :: Ix i => i -> e -> Array i e -> Array i e
Malheureusement, cela nécessiterait de copier l'intégralité du tableau, à moins que l'implémentation n'utilise une stratégie de copie sur écriture pour l'éviter lorsque cela est possible.
La bonne nouvelle est, Data.Map
a une fonction comme celle-ci, insérez :
insert :: Ord k => k -> a -> Map k a -> Map k a
Parce qu'il Map
est implémenté en interne comme un arbre binaire équilibré, insert
ne prend que du temps et de l'espace O (log n) et conserve la copie d'origine. Par conséquent, Map
non seulement fournit un "tableau mutable" quelque peu efficace qui est compatible avec le modèle de programmation fonctionnelle, mais il vous permet même de "remonter dans le temps" si vous le souhaitez.
Voici une solution pour Euler 28 utilisant Data.Map:
{-# LANGUAGE BangPatterns #-}
import Data.Map hiding (map)
import Data.List (intercalate, foldl')
data Spiral = Spiral Int (Map (Int,Int) Int)
build :: Int -> [(Int,Int)] -> Map (Int,Int) Int
build size = snd . foldl' move ((start,start,1), empty) where
start = (size-1) `div` 2
move ((!x,!y,!n), !m) (dx,dy) = ((x+dx,y+dy,n+1), insert (x,y) n m)
spiral :: Int -> Spiral
spiral size
| size < 1 = error "spiral: size < 1"
| otherwise = Spiral size (build size moves) where
right = (1,0)
down = (0,1)
left = (-1,0)
up = (0,-1)
over n = replicate n up ++ replicate (n+1) right
under n = replicate n down ++ replicate (n+1) left
moves = concat $ take size $ zipWith ($) (cycle [over, under]) [0..]
spiralSize :: Spiral -> Int
spiralSize (Spiral s m) = s
printSpiral :: Spiral -> IO ()
printSpiral (Spiral s m) = do
let items = [[m ! (i,j) | j <- [0..s-1]] | i <- [0..s-1]]
mapM_ (putStrLn . intercalate "\t" . map show) items
sumDiagonals :: Spiral -> Int
sumDiagonals (Spiral s m) =
let total = sum [m ! (i,i) + m ! (s-i-1, i) | i <- [0..s-1]]
in total-1 -- subtract 1 to undo counting the middle twice
main = print $ sumDiagonals $ spiral 1001
Les modèles de bang empêchent un débordement de pile provoqué par les éléments de l'accumulateur (curseur, nombre et carte) qui ne sont pas utilisés jusqu'à la fin. Pour la plupart des golfs de code, les cas d'entrée ne devraient pas être assez grands pour avoir besoin de cette disposition.