Я действительно борюсь с Аскеллом.

Мне потребовалось почти 6 часов, чтобы написать функцию, которая делает то, что я хочу. К сожалению, меня это не устраивает.

Может кто-нибудь дать мне какие-нибудь советы, как переписать это?

get_connected_area :: Eq generic_type => [[generic_type]] -> (Int, Int) -> [(Int,Int)] -> generic_type -> [(Int,Int)]
get_connected_area habitat point area nullValue
  | elem point area = area
  | not ((fst point) >= 0) = area
  | not ((snd point) >= 0) = area
  | not ((fst point) < (length habitat)) = area
  | not ((snd point) < (length (habitat!!0))) = area
  | (((habitat!!(fst point))!!(snd point))) == nullValue = area
  | otherwise = 
    let new_area = point : area
    in 
    get_connected_area habitat (fst point+1, snd point) (
        get_connected_area habitat (fst point-1, snd point) (
            get_connected_area habitat (fst point, snd point+1) (
                get_connected_area habitat (fst point, snd point-1) new_area nullValue
                ) nullValue
            ) nullValue
    ) nullValue

Функция get представляет собой [[generic_type]] (представляющий карту ландшафта) и ищет полностью соединенную область вокруг точки, которая не равна заданному значению nullValue.

Например.:

Если функция вызывается так:

get_connected_area [[0,1,0],[1,1,1],[0,1,0],[1,0,0]] (1,1) [] 0

Это буквально означает

0 1 0
1 1 1
0 1 0
1 0 0

Представляет карту (например, карты Google). Начать с точки (координаты) (1,1) Я хочу получить все координаты элементов, которые образуют связанную область с заданной точкой.

Поэтому результат должен быть:

0 1 0
1 1 1
0 1 0
1 0 0

И соответствующее возвращаемое значение (список координат жирным шрифтом 1с):

[(2,1),(0,1),(1,2),(1,0),(1,1)]

8
infotoni91 3 Сен 2017 в 19:37

4 ответа

Лучший ответ

Небольшое изменение заключается в том, что вы можете использовать сопоставление с образцом для переменной point. Это означает, что вы можете использовать (x, y) вместо point в объявлении функции:

get_connected_area habitat (x, y) area nullValue = ...

Теперь везде, где у вас есть fst point, просто положите x, и везде, где у вас есть snd point, положите y.

Другая модификация заключается в использовании большего количества переменных для подвыражений. Это может помочь с вложенными рекурсивными вызовами. Например, создайте переменную для самого внутреннего вложенного вызова:

....
where foo = get_connected_area habitat (x, y-1) new_area nullValue

Теперь просто поставьте foo вместо вызова. Теперь эту технику можно повторить для «нового» внутреннего звонка. (Обратите внимание, что вы должны выбрать более описательное имя, чем foo. Может быть, down?)

Обратите внимание, что not (x >= y) совпадает с x < y. Используйте это, чтобы упростить все условия. Поскольку эти условия проверяют, находится ли точка внутри ограничивающего прямоугольника, большая часть этой логики может быть преобразована в функцию isIn :: (Int, Int) -> (Int, Int) -> (Int, Int) -> Bool, которая сделает get_connected_area более читабельным.

8
Code-Apprentice 4 Сен 2017 в 05:12

Вот мое взятие:

import qualified Data.Set as Set

type Point = (Int, Int)

getConnectedArea :: (Point -> Bool) -> Point -> Set.Set Point
getConnectedArea habitat = \p -> worker p Set.empty  
          -- \p is to the right of = to keep it out of the scope of the where clause
    where
    worker p seen
      | p `Set.member` seen = seen
      | habitat p = foldr worker (Set.insert p seen) (neighbors p)
      | otherwise = seen

    neighbors (x,y) = [(x-1,y), (x+1,y), (x,y-1), (x,y+1)]

Что я сделал

  • foldr над соседями, как предлагали некоторые комментаторы.
  • Поскольку порядок точек не имеет значения, я использую Set вместо списка, так что семантически он лучше подходит и быстрее загружается.
  • Названы некоторые полезные промежуточные абстракции, такие как Point и neighbors.
  • Лучшая структура данных для среды обитания также была бы хороша, поскольку списки имеют линейное время доступа, возможно, 2D Data.Array - но для этой функции все, что вам нужно, - это функция индексирования Point -> Bool ( выход за границы и нулевое значение обрабатываются одинаково), поэтому я заменил параметр структуры данных на саму функцию индексации (это обычное преобразование в FP).

Мы можем видеть, что также можно абстрагировать функцию neighbors, и тогда мы придем к очень общему методу обхода графа.

traverseGraph :: (Ord a) => (a -> [a]) -> a -> Set.Set a

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


< Сильный > ИЗМЕНИТЬ

Вот пример того, как вызвать функцию в терминах (почти) вашей старой функции:

import Control.Monad ((<=<))

-- A couple helpers for indexing lists.

index :: Int -> [a] -> Maybe a
index _ [] = Nothing
index 0 (x:_) = x
index n (_:xs) = index (n-1) xs

index2 :: (Int,Int) -> [[a]] -> Maybe a
index2 (x,y) = index x <=< index y
-- index2 uses Maybe's monadic structure, and I think it's quite pretty. 
-- But if you're not ready for that, you might prefer
index2' (x,y) xss
  | Just xs <- index y xss = index x xs
  | otherwise = Nothing

getConnectedArea' :: (Eq a) => [[a]] -> Point -> a -> [a]
getConnectedArea' habitat point nullValue = Set.toList $ getConnectedArea nonnull point
    where
    nonnull :: Point -> Bool
    nonnull p = case index2 p habitat of
                  Nothing -> False
                  Just x -> x /= nullValue
2
luqui 4 Сен 2017 в 22:54

Это был бы мой первый быстрый проход через функцию и своего рода минимум, который мог бы пройти проверку кода (только с точки зрения стиля):

getConnectedArea :: Eq a => [[a]] -> a -> (Int, Int) -> [(Int,Int)] -> [(Int,Int)]
getConnectedArea habitat nullValue = go where
  go point@(x,y) area
      | elem point area = area
      | x < 0 = area
      | y < 0 = area
      | x >= length habitat = area
      | y >= length (habitat!!0) = area
      | ((habitat!!x)!!y) == nullValue = area
      | otherwise = 
          foldr go (point : area) 
            [ (x+1, y), (x-1, y), (x, y+1), (x, y-1) ]

Мы связываем habitat и nullValue один раз на верхнем уровне (уточняем, что делает рекурсивная работа), удаляем косвенность в предикатах, используем случай верблюда (непрозрачные штрихи, где происходит применение функции), заменяем { {X2}} с a (использование здесь шумовой переменной на самом деле имеет эффект, противоположный тому, который вы намеревались; в итоге я пытаюсь выяснить, какую особую семантику вы пытаетесь вызвать, когда интересно то, что тип не имеет значения (если его можно сравнить на равенство)).

На данный момент есть много вещей, которые мы можем сделать:

  • притворимся, что мы пишем реальный код, и беспокоимся об асимптотике обработки списков как массивов (!! и length) и множеств (elem), и вместо этого используем надлежащий массив и устанавливаем структуры данных
  • перенести проверку границ (и возможную проверку нулевых значений) в новую функцию поиска (цель состоит в том, чтобы по возможности иметь только одно предложение ... = area
  • Рассмотрим улучшения алгоритма: можем ли мы избежать рекурсивной проверки ячейки, из которой мы только что пришли, алгоритмически? Можем ли мы избежать прохождения area целиком (делая наш поиск довольно ленивым / «продуктивным»)?
5
jberryman 3 Сен 2017 в 22:15

ОК, я постараюсь упростить ваш код. Однако, уже есть хорошие ответы, и поэтому я буду решать это с немного более концептуальным подходом.

Я считаю, что вы могли бы выбрать лучшие типы данных. Например, кажется, что Data.Matrix предоставить идеальный тип данных вместо вашего [[generic_type]] типа. Также для координат я бы не выбрал тип кортежа, так как там есть тип кортежа для упаковки разных типов. Это экземпляры функтора и монады не очень полезны, когда они выбраны в качестве системы координат. Тем не менее, поскольку кажется, что Data.Matrix просто доволен кортежами в качестве координат, я буду их хранить.

ОК, ваш перефразированный код выглядит следующим образом;

import Data.Matrix

gca :: Matrix Int -> (Int, Int) -> Int -> [(Int,Int)]
gca fld crd nil = let nbs = [id, subtract 1, (+1)] >>= \f -> [id, subtract 1, (+1)]
                                                    >>= \g -> return (f,g)
                                                     >>= \(f,g) -> return ((f . fst) crd, (g . snd) crd)
                  in filter (\(x,y) -> fld ! (x,y) /= nil) nbs

*Main> gca (fromLists [[0,1,0],[1,1,1],[0,1,0],[1,0,0]]) (2,2) 0
[(2,2),(2,1),(2,3),(1,2),(3,2)]

Первое, на что нужно обратить внимание, это то, что тип данных Matrix основан на индексе 1. Итак, у нас есть центральная точка в (2,2).

Во-вторых, у нас есть список функций из трех элементов, определенный как [id, subtract 1, (+1)]. Все содержащиеся функции имеют тип Num a => a -> a, и мне нужно, чтобы они определяли окружающие пиксели заданной координаты, включая заданную координату. Таким образом, у нас есть линия, как если бы мы сделали;

[1,2,3] >>= \x -> [1,2,3] >>= \y -> return [x,y] приведет к [[1,1],[1,2],[1,3],[2,1],[2,2],[2,3],[3,1],[3,2],[3,3]], что в нашем случае даст 2 комбинации всех функций вместо чисел 1,2 и 3.

Который затем мы применяем к нашей заданной координате один за другим с помощью каскадной инструкции

>>= \[f,g] -> return ((f . fst) crd, (g . snd) crd)

Который дает все соседние координаты.

Тогда это не более чем фильтрация соседних фильтров, проверяя, не равны ли они значению nil внутри матрицы.

1
Redu 5 Сен 2017 в 19:58