Какой хороший способ справиться с задачами, требующими массивов, используя Haskell?

11

Часто задача требует реальных массивов. Возьмем, к примеру, задачу для реализации Befunge или> <>. Я попытался использовать Arrayмодуль для этого, но он действительно громоздок, так как мне кажется, что я слишком многословен. Кто-нибудь может мне помочь, как решить такие задачи кода-гольфа менее многословно и более функционально?

FUZxxl
источник
AFAIK, этот сайт предназначен только для кода гольф, а не для связанных вопросов. Я предполагаю, что это относится к Stackoverflow.
Джесси Милликен
@Jesse Millikan: Пожалуйста, посмотрите этот вопрос и прочитайте часто задаваемые вопросы. В нем не говорится, что вы не можете задавать вопросы о том, как играть в гольф. Подобные вопросы также были большой частью вопроса «по теме» на этапе определения этого сайта. Пожалуйста, дважды подумайте над своим понижением и удалите его, если вы понимаете, почему я спрашиваю это здесь.
FUZxxl
Хм, мой плохой, я думаю.
Джесси Милликен
@Jesse Millikan: Errare humanum est.
FUZxxl
FAQ не очень ясно об этом, хотя.
Джесси Милликен

Ответы:

5

Прежде всего, я рекомендую взглянуть на 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

Шаблоны взрыва предотвращают переполнение стека, вызванное тем, что элементы-накопители (курсор, число и карта) не используются до самого конца. Для большинства гольф-кодов входные регистры не должны быть достаточно большими, чтобы нуждаться в этом положении.

Джои Адамс
источник
9

Глибный ответ: не используйте массивы. Ответ не так прост: попробуйте переосмыслить свою проблему, чтобы она не требовала массивов.

Часто проблему с некоторыми мыслями можно решить без какого-либо массива, подобного структуре. Например, вот мой ответ на Эйлера 28:

-- | What is the sum of both diagonals in a 1001 by 1001 spiral?
euler28 = spiralDiagonalSum 1001

spiralDiagonalSum n
    | n < 0 || even n = error "spiralDiagonalSum needs a positive, odd number"
    | otherwise = sum $ scanl (+) 1 $ concatMap (replicate 4) [2,4..n]

То, что выражено в этом коде, это образец последовательности чисел, растущих вокруг прямоугольной спирали. Не было необходимости фактически представлять саму матрицу чисел.

Ключ к размышлению вне массивов - это думать о том, что на самом деле означает проблема, а не о том, как вы можете представить ее в виде байтов в оперативной памяти. Это приходит с практикой (возможно, поэтому я так много играю в гольф)

Другим примером является то, как я решил нахождение максимальных путей кода-гольфа. Там метод прохождения частичных решений в виде волны через матрицу, строка за строкой, выражается непосредственно операцией сгиба. Помните: на большинстве процессоров вы не можете работать с массивом в целом сразу: программе придется со временем работать через него. Может не потребоваться весь массив сразу в любое время.

Конечно, некоторые проблемы сформулированы таким образом, что они изначально основаны на массивах. Такие языки, как> <>, Befunge или Brainfuck, имеют в своей основе массивы. Тем не менее, даже там, массивы часто можно обойтись. Например, посмотрите мое решение для интерпретации Brainfuck , реальное ядро ​​его семантики - молния . Чтобы начать думать таким образом, сфокусируйтесь на моделях доступа и структуре, более близкой к значению проблемы. Часто это не нужно вводить в изменяемый массив.

Когда все остальное терпит неудачу, и вам нужно использовать массив - советы @ Joey - хорошее начало.

MtnViewMark
источник