Apa cara yang baik untuk menangani tugas-tugas yang membutuhkan array menggunakan Haskell?

11

Seringkali tugas membutuhkan array nyata. Ambil contoh tugas untuk mengimplementasikan Befunge atau> <>. Saya mencoba menggunakan Arraymodul untuk ini, tetapi itu benar-benar rumit, karena rasanya saya terlalu banyak mengode. Adakah yang bisa membantu saya bagaimana menyelesaikan tugas kode-golf seperti kurang verbose dan lebih fungsional?

FUZxxl
sumber
AFAIK, situs ini hanya untuk golf kode itu sendiri, bukan untuk pertanyaan terkait. Saya menduga ini milik Stackoverflow.
Jesse Millikan
@Jesse Millikan: Silakan juga lihat pertanyaan ini dan baca faq. Itu tidak menyatakan, bahwa Anda tidak diizinkan untuk mengajukan pertanyaan tentang cara bermain golf. Pertanyaan semacam ini juga merupakan bagian besar dari pertanyaan "tentang topik" pada fase definisi situs ini. Pikirkan dua kali downvote Anda dan hapus jika Anda mengerti mengapa saya menanyakan ini di sini.
FUZxxl
Hmm, sayang sekali saya.
Jesse Millikan
@Jesse Millikan: Errare humanum est.
FUZxxl
Namun, FAQnya tidak terlalu jelas.
Jesse Millikan

Jawaban:

5

Pertama-tama, saya sarankan melihat Data.Vector , alternatif yang lebih bagus untuk Data.Array dalam beberapa kasus.

Arraydan Vectorsangat ideal untuk beberapa kasus memoisasi, seperti yang ditunjukkan dalam jawaban saya untuk "Menemukan jalur maksimum" . Namun, beberapa masalah tidak mudah diungkapkan dalam gaya fungsional. Misalnya, Masalah 28 dalam Project Euler menyerukan penjumlahan angka pada diagonal spiral. Tentu, cukup mudah untuk menemukan formula untuk angka-angka ini, tetapi membuat spiral lebih menantang.

Data.Array.ST menyediakan tipe array yang bisa berubah. Namun, tipe situasi berantakan: ia menggunakan kelas MArray untuk membebani setiap metode kecuali untuk runSTArray . Jadi, kecuali jika Anda berencana mengembalikan array yang tidak dapat diubah dari aksi array yang bisa berubah, Anda harus menambahkan satu atau lebih jenis tanda tangan:

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

Namun demikian, solusi saya untuk Euler 28 ternyata cukup bagus, dan tidak memerlukan tanda tangan jenis itu karena saya gunakan runSTArray.

Menggunakan Data.Map sebagai "array yang bisa berubah-ubah"

Jika Anda ingin menerapkan algoritme array yang dapat diubah, opsi lain adalah menggunakan Data.Map . Saat Anda menggunakan array, Anda berharap memiliki fungsi seperti ini, yang mengubah satu elemen array:

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

Sayangnya, ini akan memerlukan menyalin seluruh array, kecuali jika implementasi menggunakan strategi copy-on-write untuk menghindarinya jika memungkinkan.

Berita baiknya adalah, Data.Mapmemiliki fungsi seperti ini, masukkan :

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

Karena Mapdiimplementasikan secara internal sebagai pohon biner seimbang, inserthanya membutuhkan O (log n) waktu dan ruang, dan mempertahankan salinan asli. Oleh karena itu, Maptidak hanya menyediakan "array yang dapat diubah" yang agak efisien yang kompatibel dengan model pemrograman fungsional, tetapi bahkan memungkinkan Anda "kembali ke masa lalu" jika Anda mau.

Berikut adalah solusi untuk Euler 28 menggunakan 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

Pola bang mencegah stack overflow yang disebabkan oleh item akumulator (kursor, angka, dan Peta) tidak digunakan sampai akhir. Untuk sebagian besar kode golf, kotak masukan tidak boleh cukup besar untuk memerlukan ketentuan ini.

Joey Adams
sumber
9

Jawaban glib adalah: Jangan gunakan array. Jawaban tidak-terlalu-glib adalah: Cobalah untuk memikirkan kembali masalah Anda sehingga tidak perlu array.

Seringkali, masalah dengan beberapa pemikiran dapat dilakukan tanpa struktur seperti array sama sekali. Misalnya, inilah jawaban saya untuk 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]

Apa yang diungkapkan dalam kode di sini adalah pola urutan angka ketika mereka tumbuh di sekitar spiral persegi panjang. Tidak perlu benar-benar mewakili matriks angka itu sendiri.

Kunci untuk berpikir di luar array adalah berpikir tentang apa sebenarnya masalah yang ada, bukan bagaimana Anda dapat merepresentasikannya sebagai byte dalam RAM. Ini hanya datang dengan latihan (mungkin alasan saya begitu banyak bermain golf!)

Contoh lain adalah bagaimana saya memecahkan pencarian jalur maksimal kode-golf. Di sana, metode melewatkan solusi parsial sebagai gelombang melalui matriks, baris demi baris, diekspresikan langsung oleh operasi lipatan. Ingat: Pada sebagian besar CPU, Anda tidak dapat menangani array secara keseluruhan sekaligus: Program harus beroperasi melaluinya seiring waktu. Mungkin tidak perlu seluruh array sekaligus kapan saja.

Tentu saja, beberapa masalah dinyatakan sedemikian rupa sehingga secara inheren berbasis array. Bahasa seperti> <>, Befunge, atau Brainfuck memiliki susunan hati. Namun, bahkan di sana, array sering dapat ditiadakan. Sebagai contoh, lihat solusi saya untuk menafsirkan Brainfuck , inti sebenarnya dari semantiknya adalah ritsleting . Untuk mulai berpikir dengan cara ini, fokuslah pada pola akses, dan struktur yang lebih dekat dengan makna masalah. Seringkali ini tidak perlu dipaksa menjadi array yang bisa berubah.

Ketika semuanya gagal, dan Anda perlu menggunakan array - tips @ Joey adalah awal yang baik.

MtnViewMark
sumber