Antes de tudo, recomendo examinar o Data.Vector , uma alternativa mais agradável ao Data.Array em alguns casos.
Array
e Vector
são ideais para alguns casos de memorização, conforme demonstrado na minha resposta para "Encontrar caminhos máximos" . No entanto, alguns problemas simplesmente não são fáceis de expressar em um estilo funcional. Por exemplo, o Problema 28 no Projeto Euler pede somar os números nas diagonais de uma espiral. Claro, deve ser bem fácil encontrar uma fórmula para esses números, mas construir a espiral é mais desafiador.
Data.Array.ST fornece um tipo de matriz mutável. No entanto, a situação do tipo é uma bagunça: ela usa uma classe MArray para sobrecarregar cada um de seus métodos, exceto o runSTArray . Portanto, a menos que você planeje retornar uma matriz imutável a partir de uma ação de matriz mutável, será necessário adicionar uma ou mais assinaturas 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
No entanto, minha solução para o Euler 28 resultou muito bem e não exigia a assinatura desse tipo porque eu o usava runSTArray
.
Usando Data.Map como uma "matriz mutável"
Se você deseja implementar um algoritmo de matriz mutável, outra opção é usar o Data.Map . Quando você usa uma matriz, você deseja ter uma função como esta, que altera um único elemento de uma matriz:
writeArray :: Ix i => i -> e -> Array i e -> Array i e
Infelizmente, isso exigiria a cópia de toda a matriz, a menos que a implementação usasse uma estratégia de copiar na gravação para evitá-la quando possível.
A boa notícia é que, Data.Map
tem uma função como esta, insira :
insert :: Ord k => k -> a -> Map k a -> Map k a
Como Map
é implementado internamente como uma árvore binária balanceada, insert
ocupa apenas O (log n) tempo e espaço e preserva a cópia original. Portanto, Map
não apenas fornece uma "matriz mutável" um tanto eficiente que é compatível com o modelo de programação funcional, mas também permite "voltar no tempo", se assim o desejar.
Aqui está uma solução para o Euler 28 usando o 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
Os padrões de estrondo impedem que um estouro de pilha causado pelos itens do acumulador (cursor, número e mapa) não seja usado até o final. Para a maioria dos golfistas de código, os casos de entrada não devem ser grandes o suficiente para precisar dessa provisão.