Qual è un buon modo per gestire le attività che richiedono array usando Haskell?


11

Spesso un'attività richiede array reali. Prendi ad esempio il compito di implementare Befunge o> <>. Ho provato a usare il Arraymodulo per questo, ma è davvero ingombrante, dato che mi sembra di scrivere un codice troppo dettagliato. Qualcuno può aiutarmi a risolvere compiti di code-golf meno dettagliati e più funzionali?


AFAIK, questo sito è solo per il codice golf stesso, non per domande correlate. Immagino che questo appartenga a StackOverflow.
Jesse Millikan,

@Jesse Millikan: vedi anche questa domanda e leggi le domande frequenti. Non afferma che non ti è permesso porre domande su come giocare a golf. Questo tipo di domande sono state anche una parte importante della domanda "sull'argomento" nella fase di definizione di questo sito. Per favore, ripensaci il tuo voto negativo e rimuovilo se capisci perché lo sto chiedendo qui.
FUZxxl,

Hmm, il mio male immagino.
Jesse Millikan,

@Jesse Millikan: Errare humanum est.
FUZxxl

Le FAQ non sono molto chiare a riguardo, però.
Jesse Millikan,

Risposte:


5

Prima di tutto, consiglio di guardare Data.Vector , in alcuni casi un'alternativa migliore a Data.Array .

Arraye Vectorsono ideali per alcuni casi di memoization, come dimostrato nella mia risposta a "Trovare i massimi percorsi" . Tuttavia, alcuni problemi semplicemente non sono facili da esprimere in uno stile funzionale. Ad esempio, il Problema 28 nel Progetto Euler richiede di sommare i numeri sulle diagonali di una spirale. Certo, dovrebbe essere abbastanza facile trovare una formula per questi numeri, ma costruire la spirale è più impegnativo.

Data.Array.ST fornisce un tipo di array mutabile. Tuttavia, la situazione del tipo è un casino: usa una classe MArray per sovraccaricare ogni suo metodo tranne runSTArray . Pertanto, a meno che non preveda di restituire un array immutabile da un'azione di array mutabile, dovrai aggiungere una o più firme di 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

Tuttavia, la mia soluzione a Euler 28 si è rivelata piuttosto carina e non ho richiesto quella firma di tipo perché l'ho usata runSTArray.

Utilizzo di Data.Map come "matrice mutabile"

Se stai cercando di implementare un algoritmo di array mutabile, un'altra opzione è utilizzare Data.Map . Quando si utilizza un array, si vorrebbe avere una funzione come questa, che cambia un singolo elemento di un array:

writeArray :: Ix i => i -> e -> Array i e -> Array i e

Sfortunatamente, ciò richiederebbe la copia dell'intero array, a meno che l'implementazione non utilizzasse una strategia di copia su scrittura per evitarlo quando possibile.

La buona notizia è che Data.Mapha una funzione come questa, inserire :

insert :: Ord k => k -> a -> Map k a -> Map k a

Poiché Mapè implementato internamente come un albero binario bilanciato, insertrichiede solo O (log n) tempo e spazio e conserva la copia originale. Quindi, Mapnon solo fornisce un "array mutabile" piuttosto efficiente che è compatibile con il modello di programmazione funzionale, ma ti permette anche di "tornare indietro nel tempo" se lo desideri.

Ecco una soluzione a Euler 28 usando 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

I modelli di botto impediscono un overflow dello stack causato dagli oggetti dell'accumulatore (cursore, numero e mappa) che non vengono utilizzati fino alla fine. Per la maggior parte dei golf di codice, i casi di input non dovrebbero essere abbastanza grandi da richiedere questa disposizione.


9

La risposta glib è: non usare le matrici. La risposta non così negativa è: prova a ripensare il tuo problema in modo che non abbia bisogno di array.

Spesso, un problema con qualche pensiero può essere fatto senza alcuna struttura come array. Ad esempio, ecco la mia risposta a Eulero 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]

Ciò che viene espresso nel codice qui è il modello della sequenza di numeri mentre crescono attorno alla spirale rettangolare. Non era necessario rappresentare effettivamente la matrice dei numeri stessi.

La chiave per pensare oltre le matrici è pensare a cosa significhi effettivamente il problema attuale, non a come rappresentarlo come byte nella RAM. Questo viene solo con la pratica (forse un motivo per cui codifico così tanto il golf!)

Un altro esempio è come ho risolto i percorsi massimi di code-golf. Lì, il metodo di far passare le soluzioni parziali come un'onda attraverso la matrice, riga per riga, è espresso direttamente dall'operazione di piegatura. Ricorda: sulla maggior parte delle CPU, non puoi gestire l'array nel suo insieme in una sola volta: il programma dovrà funzionare nel tempo. Potrebbe non essere necessario l'intero array contemporaneamente in qualsiasi momento.

Naturalmente, alcuni problemi sono indicati in modo tale da essere intrinsecamente basati su array. Lingue come> <>, Befunge o Brainfuck hanno array al centro. Tuttavia, anche lì, le matrici possono spesso essere eliminate. Ad esempio, vedi la mia soluzione per interpretare Brainfuck , il vero nucleo della sua semantica è una cerniera . Per iniziare a pensare in questo modo, concentrati sugli schemi di accesso e sulla struttura più vicina al significato del problema. Spesso questo non deve essere forzato in un array mutabile.

Quando tutto il resto fallisce e devi usare un array - i consigli di @ Joey sono un buon inizio.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.