Przede wszystkim polecam przyjrzeć się Data.Vector , w niektórych przypadkach ładniejszą alternatywą dla Data.Array .
Array
i Vector
są idealne do niektórych przypadków zapamiętywania, jak pokazano w mojej odpowiedzi na „Znajdowanie maksymalnych ścieżek” . Jednak niektóre problemy po prostu nie są łatwe do wyrażenia w funkcjonalnym stylu. Na przykład problem 28 w projekcie Euler wymaga zsumowania liczb na przekątnych spirali. Jasne, znalezienie wzoru na te liczby powinno być dość łatwe, ale zbudowanie spirali jest trudniejsze.
Data.Array.ST zapewnia zmienny typ tablicy. Jednak sytuacja typu jest bałaganem: używa klasy MArray do przeciążenia każdej z metod oprócz runSTArray . Tak więc, chyba że planujesz zwrócić niezmienną tablicę z działania zmiennej tablicy, będziesz musiał dodać jeden lub więcej podpisów typu:
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
Niemniej jednak moje rozwiązanie Euler 28 okazało się całkiem nieźle i nie wymagało tego podpisu, ponieważ go użyłem runSTArray
.
Używanie Data.Map jako „tablicy zmiennych”
Jeśli chcesz zaimplementować zmienny algorytm tablicowy, inną opcją jest użycie Data.Map . Kiedy używasz tablicy, żałujesz, że nie masz takiej funkcji, która zmienia pojedynczy element tablicy:
writeArray :: Ix i => i -> e -> Array i e -> Array i e
Niestety, wymagałoby to skopiowania całej tablicy, chyba że implementacja zastosowała strategię kopiowania przy zapisie, aby tego uniknąć, jeśli to możliwe.
Dobrą wiadomością jest to, że Data.Map
ma taką funkcję, wstaw :
insert :: Ord k => k -> a -> Map k a -> Map k a
Ponieważ Map
jest implementowany wewnętrznie jako zrównoważone drzewo binarne, insert
zajmuje tylko O (log n) czas i przestrzeń oraz zachowuje oryginalną kopię. Dlatego Map
nie tylko zapewnia nieco wydajną „zmienną tablicę”, która jest kompatybilna z funkcjonalnym modelem programowania, ale pozwala nawet „cofnąć się w czasie”, jeśli chcesz.
Oto rozwiązanie Euler 28 za pomocą 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
Wzory uderzenia zapobiegają przepełnieniu stosu spowodowanemu przez to, że przedmioty akumulatorowe (kursor, liczba i mapa) nie były używane do samego końca. W przypadku większości golfów kodowych przypadki wprowadzania nie powinny być wystarczająco duże, aby potrzebować tego przepisu.