Прежде всего, я рекомендую взглянуть на Data.Vector , в некоторых случаях более хорошую альтернативу Data.Array .
Array
и Vector
идеально подходят для некоторых случаев запоминания, как показано в моем ответе «Поиск максимальных путей» . Однако некоторые проблемы просто не легко выразить в функциональном стиле. Например, задача 28 в Project Euler требует суммирования чисел на диагонали спирали. Конечно, должно быть довольно легко найти формулу для этих чисел, но построение спирали является более сложной задачей.
Data.Array.ST предоставляет тип изменяемого массива. Однако ситуация типа является беспорядком: он использует класс MArray для перегрузки каждого из своих методов, кроме runSTArray . Таким образом, если вы не планируете возвращать неизменный массив из действия изменяемого массива, вам придется добавить одну или несколько сигнатур типа:
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
Тем не менее, мое решение для Euler 28 оказалось довольно приятным и не требовало такой сигнатуры, потому что я использовал runSTArray
.
Использование Data.Map в качестве «изменяемого массива»
Если вы хотите реализовать алгоритм изменяемого массива, другой вариант - использовать Data.Map . Когда вы используете массив, вы бы хотели иметь такую функцию, которая изменяет один элемент массива:
writeArray :: Ix i => i -> e -> Array i e -> Array i e
К сожалению, это потребует копирования всего массива, если реализация не использует стратегию копирования при записи, чтобы избежать ее, когда это возможно.
Хорошая новость заключается в том , Data.Map
имеет такую функцию, вставка :
insert :: Ord k => k -> a -> Map k a -> Map k a
Поскольку Map
реализован внутри как сбалансированное двоичное дерево, insert
занимает только O (log n) время и пространство и сохраняет оригинальную копию. Следовательно, Map
не только обеспечивает несколько эффективный «изменяемый массив», который совместим с моделью функционального программирования, но он даже позволяет вам «вернуться в прошлое», если вам так угодно.
Вот решение Euler 28 с использованием 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
Шаблоны взрыва предотвращают переполнение стека, вызванное тем, что элементы-накопители (курсор, число и карта) не используются до самого конца. Для большинства гольф-кодов входные регистры не должны быть достаточно большими, чтобы нуждаться в этом положении.