Перш за все, я рекомендую переглянути Data.Vector , приємнішу альтернативу Data.Array в деяких випадках.
Array
і Vector
ідеально підходять для деяких випадків запам'ятовування, як показано у моїй відповіді на "Пошук максимальних шляхів" . Однак деякі проблеми просто непросто виразити у функціональному стилі. Наприклад, проблема 28 в проекті 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
Шаблони вибуху запобігають переповненню стека, викликаному елементами акумулятора (курсором, номером та картою), які не використовуються до самого кінця. Для більшості кодових гольфів вхідні випадки не повинні бути такими великими, щоб потребувати цього положення.