Вы попросили более функциональный способ думать об этом, поэтому я попытаюсь представить это. Вы сказали, что вы новичок в 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
Я приглашаю вас проанализировать это и сообщить, если у вас есть какие-либо вопросы. Функциональное программирование - это сложное мышление, потому что оно сильно отличается от других программ, но это удобный набор техник, которые должны быть в вашем наборе инструментов.