Prima di tutto, consiglio di guardare Data.Vector , in alcuni casi un'alternativa migliore a Data.Array .
Array
e Vector
sono ideali per alcuni casi di memoization, come dimostrato nella mia risposta a "Trovare i massimi percorsi" . Tuttavia, alcuni problemi semplicemente non sono facili da esprimere in uno stile funzionale. Ad esempio, il Problema 28 nel Progetto Euler richiede di sommare i numeri sulle diagonali di una spirale. Certo, dovrebbe essere abbastanza facile trovare una formula per questi numeri, ma costruire la spirale è più impegnativo.
Data.Array.ST fornisce un tipo di array mutabile. Tuttavia, la situazione del tipo è un casino: usa una classe MArray per sovraccaricare ogni suo metodo tranne runSTArray . Pertanto, a meno che non preveda di restituire un array immutabile da un'azione di array mutabile, dovrai aggiungere una o più firme di tipo:
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
Tuttavia, la mia soluzione a Euler 28 si è rivelata piuttosto carina e non ho richiesto quella firma di tipo perché l'ho usata runSTArray
.
Utilizzo di Data.Map come "matrice mutabile"
Se stai cercando di implementare un algoritmo di array mutabile, un'altra opzione è utilizzare Data.Map . Quando si utilizza un array, si vorrebbe avere una funzione come questa, che cambia un singolo elemento di un array:
writeArray :: Ix i => i -> e -> Array i e -> Array i e
Sfortunatamente, ciò richiederebbe la copia dell'intero array, a meno che l'implementazione non utilizzasse una strategia di copia su scrittura per evitarlo quando possibile.
La buona notizia è che Data.Map
ha una funzione come questa, inserire :
insert :: Ord k => k -> a -> Map k a -> Map k a
Poiché Map
è implementato internamente come un albero binario bilanciato, insert
richiede solo O (log n) tempo e spazio e conserva la copia originale. Quindi, Map
non solo fornisce un "array mutabile" piuttosto efficiente che è compatibile con il modello di programmazione funzionale, ma ti permette anche di "tornare indietro nel tempo" se lo desideri.
Ecco una soluzione a Euler 28 usando 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
I modelli di botto impediscono un overflow dello stack causato dagli oggetti dell'accumulatore (cursore, numero e mappa) che non vengono utilizzati fino alla fine. Per la maggior parte dei golf di codice, i casi di input non dovrebbero essere abbastanza grandi da richiedere questa disposizione.