Haskell Iterate через 2d список, фильтр, вывод 1d список

Я думал, что в своих исследованиях в Haskell я плавно плаваю, пока...

У меня есть [[Int]]

tiles = [[1,0,0]
        ,[0,1,0]
        ,[0,1,0]
        ]

и тип данных:

data Coord = Coord
    { x :: Int
    , y :: Int 
    } deriving (Eq)

На основе ввода tiles я пытаюсь вывести [Coord], так что Coord генерируется только тогда, когда значение tiles равно 1, а Coord будет хранить его позиция в списке 2d:

blackBox :: [[Int]] -> [Coord]
blackBox tiles = <magic> 
-- given the above example I would expect:
-- [(Coord 0 0),(Coord 1 1),(Coord 1 2)]

Я пробовал такие вещи, как первая конвертация [[Int]] в [Int], через:

foldTiles :: [[Int]] -> [Int]
foldTiles tiles = foldr (++) [] tiles

но после этого я не уверен, как передавать индексы. Я полагаю, что если бы я мог отображать "сложенные плитки", выводя кортеж (значение, индекс), я мог бы легко выяснить остальное.

update В случае, если кто-то заинтересован, я получил его работу, и вот демонстрация его (с исходным кодом и ссылкой на GitHub)! Мне нужно будет потратить больше времени, чтобы понять каждый из ответов, поскольку это мой первый раз, когда я программировал игру с использованием FP. Большое спасибо!

http://kennycason.com/posts/2013-10-10-haskell-sdl-gameboy-boxxle.html

Ответы

Ответ 1

Это место, где блестят списки.

blackBox tiles =
  [Coord x y                         -- generate a Coord pair
    | (y, row) <- enumerate tiles    -- for each row with its coordinate
    , (x, tile) <- enumerate row     -- for each tile in the row (with coordinate)
    , tile == 1]                     -- if the tile is 1

Или вы могли бы использовать эквивалентную нотацию do (поскольку список является монадой), для которой требуется импортировать Control.Monad (для guard.)

blackBox tiles = do
  (y, row) <- enumerate tiles    -- for each row with its coordinate
  (x, tile) <- enumerate row     -- for each tile in the row (with coordinate)
  guard $ tile == 1              -- as long as the tile is 1
  return $ Coord x y             -- return a coord pair

Чтобы помочь с пониманием, эта последняя функция работает как следующая функция Python.

def black_box(tiles):
    for y, row in enumerate(tiles):
        for x, tile in enumerate(row):
            if tile == 1:
                 yield Coord(x, y)

do Обозначение для монады списка невероятно удобно для обработки списков, я думаю, поэтому стоит обволакивать вашу голову!


В обоих этих примерах я использовал определение

enumerate = zip [0..]

Ответ 2

Здесь простое решение (не гарантируйте, что оно жизнеспособно для tiles размером 10000x10000, что-то для вас проверить;)

Подход, как обычно, в Haskell, является нисходящим развитием. Вы думаете: что делать blackBox? Для каждой строки tiles она должна собрать Coord из фрагментов с 1 для этой строки и объединить их.

Это дает вам другую функцию blackBoxRow, только для строк. Что делать? Удалите нули из строки и оберните остальные в Coord s, так что там filter, а затем map. Также вы хотите сохранить номера строк и столбцов, чтобы вы наносили плитки, связанные с их соответствующими координатами.

Это дает вам:

tiles :: [[Int]]
tiles = [[1,0,0]
        ,[0,1,0]
        ,[0,1,0]
        ]

data Coord = Coord {
    x :: Int
    ,y :: Int
} deriving (Eq, Show)

blackBox :: [[Int]] -> [Coord]
blackBox tiles2d = concat (map blackBoxRow (zip [0..] tiles2d))

blackBoxRow :: (Int, [Int]) -> [Coord]
blackBoxRow (row, tiles1d) = map toCoord $ filter pickOnes (zip [0..] tiles1d) where
    pickOnes (_, value) = value == 1
    toCoord (col, _) = Coord {x=col, y=row}


main = print $ blackBox tiles

Результаты в:

~> runhaskell t.hs
[Coord {x = 0, y = 0},Coord {x = 1, y = 1},Coord {x = 1, y = 2}]

Ответ 3

Как я вижу это, вы можете поместить свой 2D-список в ряд преобразований. Первый, который нам понадобится, - это тот, который может заменить 1 в вашем списке чем-то более полезным, например его строку:

assignRow :: Int -> [Int] -> [Int]
assignRow n xs = map (\x -> if x == 1 then n else x) xs

Теперь мы можем использовать zipWith и [1..] для выполнения первого шага:

assignRows :: [[Int]] -> [[Int]]
assignRows matrix = zipWith assignRow [1..] matrix

Какое удобство в этом состоит в том, что он будет работать, даже если матрица не является квадратной, и завершается, как только матрица делает.

Далее нам нужно назначить номер столбца, и здесь я сделаю несколько шагов сразу. Это делает кортежи координат, но есть недопустимые те, где r == 0 (поэтому я использовал [1..], в противном случае вы потеряете первую строку), поэтому мы отфильтровываем их. Затем мы uncurry Coord создаем функцию, которая вместо этого берет кортеж, а затем мы используем flip на ней, а затем переводим эту вещь по списку кортежей.

assignCol :: [Int] -> [Coord]
assignCol xs = map (uncurry (flip Coord)) $ filter (\(c, r) -> r /= 0) $ zip [1..] xs

И мы можем построить наш assignCols:

assignCols :: [[Int]] -> [Coord]
assignCols matrix = concatMap assignCol matrix 

что позволяет нам построить конечную функцию

assignCoords :: [[Int]] -> [Coord]
assignCoords = assignCols . assignRows

Вы могли бы сжать это довольно немного с некоторым уменьшением eta.

Если вы хотите 0-индексированные координаты, я оставлю вас для изменения этого решения.

Ответ 4

Быстрое и грязное решение:

import Data.Maybe (mapMaybe)

data Coord = Coord {
    x :: Int
    ,y :: Int
} deriving (Eq, Show)

blackBox :: [[Int]] -> [Coord]
blackBox = concatMap (\(y, xks) -> mapMaybe (toMaybeCoord y) xks)
    . zip [0..] . map (zip [0..])
    where
    toMaybeCoord :: Int -> (Int, Int) -> Maybe Coord
    toMaybeCoord y (x, k) = if k == 1
        then Just (Coord x y)
        else Nothing

Параметр zip сопоставляет значения tile (которые я имею в виду как k) с координатами x и y (мы имеем дело со списками, поэтому нам нужно добавить индексы, если они нам понадобятся). mapMaybe удобно, так что мы можем сопоставить (для построения Coords) и фильтра (для удаления нулевых плит) за один шаг. concatMap также выполняет две функции: он отображает функцию (анонимную функцию в круглых скобках), генерирующую список списков, а затем сглаживает ее. Обязательно проверьте типы промежуточных функций и результаты, чтобы получить более четкое изображение преобразований.

Ответ 5

Вот он, используя списки.

blackBox :: [[Integer]] -> [Coord]
blackBox ts = [Coord x y | (t,y) <- zip ts [0..], (e,x) <- zip t [0..], e == 1]

Ответ 6

Пока мы собираем ответы, здесь другое:

blackBox :: [[Int]] -> [Coord]
blackBox ts = map (uncurry Coord) xsAndYs
    where
        xsAndYs = concat $ zipWith applyYs [0..] x1s
        applyYs i = map (flip (,) i)
        x1s = map (map fst . filter ((==1) . snd)) xs
        xs = map (zip [0..]) ts      

Объяснение:

Это присваивает индексы x в каждой строке:

xs = map (zip [0..]) ts

Затем я фильтрую каждую строку, чтобы сохранить только элементы с 1, а затем я отбрасываю 1 (так как это уже не полезно):

x1s = map (map fst . filter ((==1) . snd)) xs

В результате получается что-то типа [[Int]], которые являются строками с x, где раньше был 1. Затем я сопоставляю y в каждой строке, переворачивая пары, поэтому я оставляю (x,y) вместо (y,x). В качестве последнего шага я выравниваю строки в один список, так как мне больше не нужно их разделять:

xsAndYs = concat $ zipWith applyYs [0..] x1s
applyYs i = map (flip (,) i)

Наконец, я преобразую каждый элемент через map ping Coord поверх него. uncurry необходимо, потому что Coord не принимает кортеж в качестве аргумента.