En primer lugar, recomiendo mirar Data.Vector , una alternativa mejor a Data.Array en algunos casos.
Array
y Vector
son ideales para algunos casos de memorización, como se demuestra en mi respuesta a "Encontrar las rutas máximas" . Sin embargo, algunos problemas simplemente no son fáciles de expresar en un estilo funcional. Por ejemplo, el problema 28 en el Proyecto Euler llama a la suma de los números en las diagonales de una espiral. Claro, debería ser bastante fácil encontrar una fórmula para estos números, pero construir la espiral es más difícil.
Data.Array.ST proporciona un tipo de matriz mutable. Sin embargo, la situación de tipo es un desastre: utiliza una clase MArray para sobrecargar cada uno de sus métodos, excepto runSTArray . Entonces, a menos que planee devolver una matriz inmutable de una acción de matriz mutable, tendrá que agregar una o más firmas de 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
Sin embargo, mi solución para Euler 28 resultó bastante bien, y no requería esa firma de tipo porque la usé runSTArray
.
Usando Data.Map como una "matriz mutable"
Si está buscando implementar un algoritmo de matriz mutable, otra opción es usar Data.Map . Cuando usa una matriz, desea tener una función como esta, que cambia un solo elemento de una matriz:
writeArray :: Ix i => i -> e -> Array i e -> Array i e
Desafortunadamente, esto requeriría copiar toda la matriz, a menos que la implementación utilizara una estrategia de copia en escritura para evitarla cuando sea posible.
La buena noticia es que Data.Map
tiene una función como esta, insertar :
insert :: Ord k => k -> a -> Map k a -> Map k a
Debido a que Map
se implementa internamente como un árbol binario equilibrado, insert
solo toma O (log n) tiempo y espacio, y conserva la copia original. Por lo tanto, Map
no solo proporciona una "matriz mutable" algo eficiente que es compatible con el modelo de programación funcional, sino que incluso le permite "retroceder en el tiempo" si así lo desea.
Aquí hay una solución para 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
Los patrones de explosión evitan un desbordamiento de la pila causado por los elementos del acumulador (cursor, número y mapa) que no se utilizan hasta el final. Para la mayoría de los códigos de golf, los casos de entrada no deberían ser lo suficientemente grandes como para necesitar esta disposición.