Вспоминая старые данные - PullRequest
0 голосов
/ 30 июня 2018

Вспоминая свой первый раз (в цикле Хаскеля)

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

Вопрос, который я рассматриваю, включает чтение в наборах координат (x1, y1) и (x2, y2) и Определение периметра многоугольника, нарисованного этими координатами.

Вот мой код:

-- Calculate length of line given points x1,y2, x2,y2
calc_length:: Int -> Int -> Int -> Int -> Float 
calc_length x1 y1 x2 y2 =
       sqrt ( fromIntegral (height ^2 + width ^2)  )
       where height = abs( y2 - y1) 
             width  = abs( x2 - x1) 


main = do
        x <- readLn :: IO Double
        forM_ [1,2..(x / 2)] $ \lc -> do
           line1 <- getLine
           let wds1 = map (\str -> read str::Int) (words $ line1)
           line2 <- getLine
           let wds2 = map (\str -> read str::Int) (words $ line2)
           print ( wds1, wds2)

У меня проблема в том, что мне нужно вычислить расстояние между первой и последней координатами, т.е. ПОМНИТЕ первую введенную пару чисел (сохранено в строке 1). Но после многократных итераций первая пара будет потеряна. Я пытался использовать глобальные переменные для хранения первого вызова getLine (без особого успеха, и даже если бы это сработало, я не думаю, что это поможет.)

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

Я не ищу полностью закодированное решение, а просто подход, который указывает мне лучшее направление.

Есть мысли?

Ответы [ 3 ]

0 голосов
/ 30 июня 2018

Вы попросили более функциональный способ думать об этом, поэтому я попытаюсь представить это. Вы сказали, что вы новичок в Haskell, поэтому я заранее извиняюсь, если это касается вещей, которые вы еще не исследовали. Не стесняйтесь просить разъяснения в любой его части.

Прежде всего, давайте сегментируем вашу calcLength функцию немного больше. Мы передаем ему две точки, поэтому вместо того, чтобы передавать четыре аргумента, давайте передадим только два.

data Point a = Point a a

calcLength :: Floating a => Point a -> Point a -> a
calcLength (Point x1 y1) (Point x2 y2) = sqrt (height ^ 2 + width ^ 2)
    where height = abs (y2 - y1)
          width  = abs (x2 - x1)

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

readPoint :: (Floating a, Read a) => IO (Point a)
readPoint = Point <$> readLn <*> readLn

Я использую здесь аппликативный синтаксис. Если вы более знакомы с do-обозначениями, эквивалент будет

readPoint :: (Floating a, Read a) => IO (Point a)
readPoint = do
  x <- readLn
  y <- readLn
  return $ Point x y

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

-- We're going to take a list of things and produce a list of pairs of those things
loopedPairs :: [a] -> [(a, a)]
-- If the original list is empty, return the empty list
loopedPairs [] = []
-- Otherwise, start recursing
loopedPairs (x:xs) = go x xs
    -- Here, we're pairing off all the elements
    where go x' (y:ys) =  (x', y) : go y ys
          -- Because we defined this as an inner function, we can still access
          -- the original first element, effectively "remembering" it like you
          -- were asking about. Note that we never use any "global" storage or
          -- mutable state to do this, just a bit of variable scope.
          go x' []     = [(x', x)]

Теперь мы напишем функцию периметра. Хорошо отделить как можно большую часть вашей «чистой» логики без ввода-вывода от работы ввода-вывода, поэтому мы хотим вычленить это из main.

newtype Polygon a = Polygon [Point a]

perimeter :: Floating a => Polygon a -> a
perimeter (Polygon xs) = sum . map (\(a, b) -> calcLength a b) $ loopedPairs xs

Мы берем многоугольник, который на самом деле представляет собой просто список точек, соединяем все наши точки, используя loopedPairs, затем вычисляем длину между каждой из них и суммируем результаты.

Имея это в виду, main довольно короткий.

main :: IO ()
main = do
  n <- readLn :: IO Int
  points <- replicateM n (readPoint :: IO (Point Double))
  let result = perimeter (Polygon points)
  print result

Мы читаем по количеству точек, затем читаем каждую точку (replicateM означает, по сути, «сделать это n раза и накапливать результат в списке). Затем мы вычисляем периметр и распечатываем его.

Работоспособное решение:

import Control.Monad

data Point a = Point a a

newtype Polygon a = Polygon [Point a]

calcLength :: Floating a => Point a -> Point a -> a
calcLength (Point x1 y1) (Point x2 y2) = sqrt (height ^ 2 + width ^ 2)
    where height = abs (y2 - y1)
          width  = abs (x2 - x1)

readPoint :: (Floating a, Read a) => IO (Point a)
readPoint = Point <$> readLn <*> readLn

loopedPairs :: [a] -> [(a, a)]
loopedPairs [] = []
loopedPairs (x:xs) = go x xs
    where go x' (y:ys) =  (x', y) : go y ys
          go x' []     = [(x', x)]

perimeter :: Floating a => Polygon a -> a
perimeter (Polygon xs) = sum . map (\(a, b) -> calcLength a b) $ loopedPairs xs

main :: IO ()
main = do
  n <- readLn :: IO Int
  points <- replicateM n (readPoint :: IO (Point Double))
  let result = perimeter (Polygon points)
  print result

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

0 голосов
/ 01 июля 2018

После помощи everbodys: мое окончательное решение:

import Data.List
import Data.Foldable
import Data.Traversable
import Data.List.Split

          -- Calculate length of line given points x1,y2, x2,y2
          calc_length:: Int -> Int -> Int -> Int -> Float 
         calc_length x1 y1 x2 y2 =
               sqrt ( fromIntegral (height ^2 + width ^2)  )
               where height = abs( y2 - y1) 
                     width  = abs( x2 - x1)


        -- Calculate the distances between vertex points (except the last)
        getResults list = 
            sum [ calc_length (head f)  (last f) (head s)  (last s)   |  (f,s)  <-  (zip list (tail list)) ]  


       -- Calculate the last vertex distance between points
       headAndTail list =
           calc_length (z!!0) (z!!1) (z!!2) (z!!3)
           where z  = head list ++ last list 

        -- Prompt the user for co-ordinate pairs
        main = do
                x <- readLn :: IO Double
                result <- forM [1,2..x ] ( \lc -> do
                   line1 <- getLine
                   let wds1 = map (\str -> read str::Int) (words $ line1)
                   return wds1)

                print (  (getResults result) + (headAndTail result)   ) 
0 голосов
/ 30 июня 2018

Если вам нужно по-разному относиться к первой итерации, она должна быть отделена (если я правильно понимаю вашу проблему). Вы можете уменьшить дублирование с помощью вспомогательной функции:

getNumLine :: IO [Int]
getNumLine = do
    line <- getLine
    return (map read (words line))

main = do
    x <- readLn :: IO Int   -- Double seemed wrong, use integer `div` below instead
    firstline <- getNumLine
    forM_ [2..x `div` 2] $ \lc -> do
        ...

И да, вы могли бы быть намного более "функциональными" в этом, но я думаю, что лучше учиться в маленьких шагах.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...