Quelle est la bonne façon de gérer les tâches qui nécessitent des tableaux à l'aide de Haskell?

11

Souvent, une tâche nécessite de vrais tableaux. Prenez par exemple la tâche d'implémenter Befunge ou> <>. J'ai essayé d'utiliser le Arraymodule pour cela, mais c'est vraiment lourd, car j'ai l'impression de coder beaucoup trop verbeux. Quelqu'un peut-il m'aider à résoudre ces tâches de code-golf moins verbeuses et plus fonctionnelles?

FUZxxl
la source
AFAIK, ce site est uniquement pour le golf de code lui-même, pas pour les questions connexes. Je suppose que cela appartient à Stackoverflow.
Jesse Millikan
@Jesse Millikan: Veuillez également voir cette question et lire la FAQ. Cela ne dit pas que vous n'êtes pas autorisé à poser des questions sur la façon de jouer au golf. Ce type de questions était également une grande partie de la question "sur le sujet" dans la phase de définition de ce site. S'il vous plaît, réfléchissez à votre vote négatif et supprimez-le si vous comprenez pourquoi je pose cette question ici.
FUZxxl
Hmm, mon mauvais je suppose.
Jesse Millikan
@Jesse Millikan: Errare humanum est.
FUZxxl
La FAQ n'est cependant pas très claire à ce sujet.
Jesse Millikan

Réponses:

5

Tout d'abord, je recommande de regarder Data.Vector , une alternative plus agréable à Data.Array dans certains cas.

Arrayet Vectorsont idéales pour certains cas de mémorisation, comme démontré dans ma réponse à "Trouver des chemins maximaux" . Cependant, certains problèmes ne sont tout simplement pas faciles à exprimer dans un style fonctionnel. Par exemple, le problème 28 du projet Euler appelle à additionner les nombres sur les diagonales d'une spirale. Bien sûr, il devrait être assez facile de trouver une formule pour ces nombres, mais la construction de la spirale est plus difficile.

Data.Array.ST fournit un type de tableau mutable. Cependant, la situation de type est un gâchis: il utilise une classe MArray pour surcharger chacune de ses méthodes à l'exception de runSTArray . Donc, à moins que vous ne prévoyiez de retourner un tableau immuable à partir d'une action de tableau mutable, vous devrez ajouter une ou plusieurs signatures de type:

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

Néanmoins, ma solution à Euler 28 s'est avérée assez bien et ne nécessitait pas cette signature de type parce que je l'ai utilisé runSTArray.

Utilisation de Data.Map comme un "tableau mutable"

Si vous cherchez à implémenter un algorithme de tableau mutable, une autre option consiste à utiliser Data.Map . Lorsque vous utilisez un tableau, vous souhaitez en quelque sorte avoir une fonction comme celle-ci, qui change un seul élément d'un tableau:

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

Malheureusement, cela nécessiterait de copier l'intégralité du tableau, à moins que l'implémentation n'utilise une stratégie de copie sur écriture pour l'éviter lorsque cela est possible.

La bonne nouvelle est, Data.Mapa une fonction comme celle-ci, insérez :

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

Parce qu'il Mapest implémenté en interne comme un arbre binaire équilibré, insertne prend que du temps et de l'espace O (log n) et conserve la copie d'origine. Par conséquent, Mapnon seulement fournit un "tableau mutable" quelque peu efficace qui est compatible avec le modèle de programmation fonctionnelle, mais il vous permet même de "remonter dans le temps" si vous le souhaitez.

Voici une solution pour Euler 28 utilisant 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

Les modèles de bang empêchent un débordement de pile provoqué par les éléments de l'accumulateur (curseur, nombre et carte) qui ne sont pas utilisés jusqu'à la fin. Pour la plupart des golfs de code, les cas d'entrée ne devraient pas être assez grands pour avoir besoin de cette disposition.

Joey Adams
la source
9

La réponse simple est: n'utilisez pas de tableaux. La réponse n'est pas si simple: essayez de repenser votre problème afin qu'il n'ait pas besoin de tableaux.

Souvent, un problème avec une réflexion peut être fait sans aucune structure de type tableau du tout. Par exemple, voici ma réponse à 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]

Ce qui est exprimé dans le code ici, c'est le modèle de la séquence des nombres qui se développent autour de la spirale rectangulaire. Il n'était pas nécessaire de représenter réellement la matrice des nombres elle-même.

La clé pour penser au-delà des tableaux est de réfléchir à la signification réelle du problème, et non à la façon dont vous pouvez le représenter sous forme d'octets dans la RAM. Cela vient juste avec la pratique (peut-être une raison pour laquelle je code tellement au golf!)

Un autre exemple est la façon dont j'ai résolu la recherche de codes maximaux de code-golf. Là, la méthode de passage des solutions partielles sous forme d'onde dans la matrice, ligne par ligne, s'exprime directement par l'opération de pliage. Rappelez-vous: Sur la plupart des CPU, vous ne pouvez pas traiter le tableau dans son ensemble à la fois: le programme devra fonctionner à travers lui au fil du temps. Il peut ne pas avoir besoin de l'ensemble du tableau à la fois à tout moment.

Bien sûr, certains problèmes sont énoncés de telle manière qu'ils sont intrinsèquement basés sur des tableaux. Des langues comme> <>, Befunge ou Brainfuck ont ​​des tableaux à leur cœur. Cependant, même là, les tableaux peuvent souvent être supprimés. Par exemple, voir ma solution pour interpréter Brainfuck , le véritable noyau de sa sémantique est une fermeture éclair . Pour commencer à penser de cette façon, concentrez-vous sur les modèles d'accès et la structure plus proche de la signification du problème. Souvent, cela n'a pas besoin d'être forcé dans un tableau mutable.

Lorsque tout le reste échoue et que vous devez utiliser un tableau - les conseils de @ Joey sont un bon début.

MtnViewMark
la source