Codewars - Land Perimeter

The Land-Perimeter challenge from Codewars is slightly different from the others I've mentioned; it effectively needs you to produce results from a list of lists, where the relationship between a given item and its peers is important. I tried this one in Haskell first, since the approach in that language is somewhat new to me.

One more round of spoilers?


landPerimeter :: [String] -> String
landPerimeter xs = (++) "Total land perimeter: "
                        (show $ sum $ map (\p -> pointPerimeter (fst p) (snd p) xs) $ points xs)

-- | Produce the X or O character of a given x,y coordinate and islands map.
--   Defaults to O if the coordinate is not on the map.
geography :: Int -> Int -> [String] -> Char
geography x y islands
  | y < 0 || x < 0 = 'O'
  | y >= length islands || x >= length (head islands) = 'O'
  | otherwise = (islands !! y) !! x

-- | Produce the perimeter of a given point on an island map.
pointPerimeter :: Int -> Int -> [String] -> Int
pointPerimeter x y islands
  | geography x y islands == 'O' = 0
  | otherwise = length . filter (\g -> g == 'O')
                       $ zipWith (\x y -> geography x y islands)
                                 [(x - 1), (x + 1), x, x]
                                 [y, y, (y - 1), (y + 1)]

-- | Produce a list of possible (x, y) coordinates for an islands map.
points :: [String] -> [(Int, Int)]
points xs = go 0 0 xs where
  go _ _ [] = []
  go x y (a:as) = map (\x -> (x, y)) [0 .. (length a) - 1] ++ go 0 (y + 1) as

It was tricky to get the coordinate traversal right. My first pass tried to incorporate it as part of the landPerimeter function - it didn't work until I made it a separate procedure and thought it through with independent tests. There may be cleaner ways to get at what amounts to a nested for loop.


def land_perimeter(islands):
  """Given a map of islands, return the perimeter of land among them."""
  total = 0
  for y in range(len(islands)):
    for x in range(len(islands[y])):
      total += perimeter(x, y, islands)

  return "Total land perimeter: %s" % (total,)

def geography(x, y, islands):
  """Given an x, y position and island map, return the X or O character at the
     position.  Default to O for positions out of range."""

  if y < 0 or x < 0: return "O"
  if y >= len(islands) or x >= len(islands[0]): return "O"
  return islands[y][x]

def perimeter(x, y, islands):
  """Given an x, y positon and island map, return the perimeter of position."""

  if geography(x, y, islands) == "O": return 0

  neigbors = [geography(g[0], g[1], islands) for g in [(x, y - 1), (x, y + 1),
                                                       (x - 1, y), (x + 1, y)]]
  return neigbors.count("O")

Prior to learning about functional programming I might have implemented this as one long-ish function.

Other solutions ...

... were okay; there are a few in each language that require far fewer lines of code, but for once I'm not taken with any of them. Typically I find several that show me ideas I want to learn from, but in this case the "code golf" answers left me thinking "good luck to whomever has to maintain that."


Still much easier time writing the Python, though it feels like function composition in Haskell is becoming more familiar.

Bottom line - it feels like there is a lot of depth to be explored in Haskell; it's also been pretty challenging to bring it up to speed with what I can handle in Python. Chances are good I'll keep exploring it, though it's starting to feel like time to move off of exercises and think about where I could use it for real projects.