Упрощение функции haskell

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

мне понадобилось почти 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]] (представляющая ландшафтную карту) и ищет полностью связанную область вокруг точки, которая не равна заданному нулевому значению.

например.:

Если функция вызывается следующим образом:

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 maps). Начните с точки (координат) (1,1), я хочу получить все координаты элементов, которые образуют связанную область с данной точкой.

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

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

и corresponting возвращаемого значения (список координат смелый 1С):

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

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. Может быть!--11-->?)

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


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

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 Как только на верхнем уровне (уточнение того, что делает рекурсивная работа), удалите косвенность в предикатах, используйте camel-case (underdashes затемняют, где происходит применение функции), замените generic_type С a (использование шумной переменной здесь фактически имеет противоположный эффект от одного вы намеревались; я в конечном итоге пытаюсь выяснить, какую специальную семантику вы пытаетесь вызвать, когда интересная вещь заключается в том, что тип не имеет значения (если его можно сравнить для равенства)).

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

  • представьте, что мы пишем реальный код и беспокоимся об асимптотике обработки списков как массивов (!! и length) и наборы (elem), и использовать правильный массив и установить структуры данных вместо
  • переместите проверку границ (и возможную проверку нулевого значения) в новую функцию поиска (цель состоит в том, чтобы иметь только один ... = area пункт если это возможно
  • рассмотрим улучшения алгоритма: можем ли мы избежать рекурсивной проверки ячейки, из которой мы только что пришли алгоритмически? можем ли мы избежать прохождения area полностью (что делает наш поиск приятно ленивым/ "продуктивным")?

вот мое мнение:

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

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

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

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

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 тип данных-index 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 значение в матрице.