¿Cuál es una buena manera de lidiar con tareas que requieren matrices con Haskell?

11

A menudo, una tarea requiere matrices reales. Tome por ejemplo la tarea de implementar Befunge o> <>. Traté de usar el Arraymódulo para esto, pero es realmente engorroso, ya que parece que estoy codificando demasiado detallado. ¿Alguien puede ayudarme a resolver tales tareas de código de golf menos detalladas y más funcionales?

FUZxxl
fuente
AFAIK, este sitio es solo para el código de golf en sí, no para preguntas relacionadas. Supongo que esto pertenece a Stackoverflow.
Jesse Millikan
@Jesse Millikan: Consulte también esta pregunta y lea las preguntas frecuentes. No dice que no se le permite hacer preguntas sobre cómo jugar golf. Este tipo de preguntas también fueron una gran parte de la pregunta "sobre el tema" en la fase de definición de este sitio. Por favor, piense dos veces su voto negativo y elimínelo si comprende por qué le pregunto esto aquí.
FUZxxl
Hmm, mi mal, supongo.
Jesse Millikan
@Jesse Millikan: Errare humanum est.
FUZxxl
Sin embargo, las preguntas frecuentes no son muy claras al respecto.
Jesse Millikan

Respuestas:

5

En primer lugar, recomiendo mirar Data.Vector , una alternativa mejor a Data.Array en algunos casos.

Arrayy Vectorson ideales para algunos casos de memorización, como se demuestra en mi respuesta a "Encontrar las rutas máximas" . Sin embargo, algunos problemas simplemente no son fáciles de expresar en un estilo funcional. Por ejemplo, el problema 28 en el Proyecto Euler llama a la suma de los números en las diagonales de una espiral. Claro, debería ser bastante fácil encontrar una fórmula para estos números, pero construir la espiral es más difícil.

Data.Array.ST proporciona un tipo de matriz mutable. Sin embargo, la situación de tipo es un desastre: utiliza una clase MArray para sobrecargar cada uno de sus métodos, excepto runSTArray . Entonces, a menos que planee devolver una matriz inmutable de una acción de matriz mutable, tendrá que agregar una o más firmas de 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

Sin embargo, mi solución para Euler 28 resultó bastante bien, y no requería esa firma de tipo porque la usé runSTArray.

Usando Data.Map como una "matriz mutable"

Si está buscando implementar un algoritmo de matriz mutable, otra opción es usar Data.Map . Cuando usa una matriz, desea tener una función como esta, que cambia un solo elemento de una matriz:

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

Desafortunadamente, esto requeriría copiar toda la matriz, a menos que la implementación utilizara una estrategia de copia en escritura para evitarla cuando sea posible.

La buena noticia es que Data.Maptiene una función como esta, insertar :

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

Debido a que Mapse implementa internamente como un árbol binario equilibrado, insertsolo toma O (log n) tiempo y espacio, y conserva la copia original. Por lo tanto, Mapno solo proporciona una "matriz mutable" algo eficiente que es compatible con el modelo de programación funcional, sino que incluso le permite "retroceder en el tiempo" si así lo desea.

Aquí hay una solución para 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

Los patrones de explosión evitan un desbordamiento de la pila causado por los elementos del acumulador (cursor, número y mapa) que no se utilizan hasta el final. Para la mayoría de los códigos de golf, los casos de entrada no deberían ser lo suficientemente grandes como para necesitar esta disposición.

Joey Adams
fuente
9

La respuesta simplista es: no use matrices. La respuesta no tan fácil es: Intente repensar su problema para que no necesite matrices.

A menudo, un problema con algo de pensamiento se puede hacer sin ningún tipo de estructura como matriz. Por ejemplo, aquí está mi respuesta a Euler 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]

Lo que se expresa en el código aquí es el patrón de la secuencia de números a medida que crecen alrededor de la espiral rectangular. No había necesidad de representar realmente la matriz de números en sí.

La clave para pensar más allá de las matrices es pensar en lo que realmente significa el problema en cuestión, no en cómo podría representarlo como bytes en la RAM. Esto solo viene con la práctica (¡tal vez una razón por la que codifico tanto golf!)

Otro ejemplo es cómo resolví la búsqueda de rutas máximas de código-golf. Allí, el método de pasar las soluciones parciales como una onda a través de la matriz, fila por fila, se expresa directamente mediante la operación de plegado. Recuerde: en la mayoría de las CPU, no puede manejar la matriz como un todo a la vez: el programa tendrá que funcionar a través de ella con el tiempo. Es posible que no necesite toda la matriz a la vez en cualquier momento.

Por supuesto, algunos problemas se plantean de tal manera que están inherentemente basados ​​en matrices. Idiomas como> <>, Befunge o Brainfuck tienen arrays en su corazón. Sin embargo, incluso allí, las matrices a menudo se pueden prescindir. Por ejemplo, vea mi solución para interpretar Brainfuck , el núcleo real de su semántica es una cremallera . Para comenzar a pensar de esta manera, concéntrese en los patrones de acceso y en la estructura más cercana al significado del problema. A menudo, esto no necesita ser forzado a una matriz mutable.

Cuando todo lo demás falla y necesitas usar una matriz, los consejos de @ Joey son un buen comienzo.

MtnViewMark
fuente