Ответ 1
Я лично сделал бы тип для них, и если вы действительно хотите, чтобы все было в пределах их периодических ограничений, это отличная возможность обеспечить это.
Начните с создания простых конструкторов newtype
:
newtype Latitude = Latitude Double deriving (Eq, Show, Ord)
newtype Longitude = Longitude Double deriving (Eq, Show, Ord)
Обратите внимание, что я не использовал RealFloat
, потому что RealFloat
- это тип, а не конкретный тип, поэтому он не может использоваться как поле для конструктора. Затем напишите функцию для нормализации этих значений:
normalize :: Double -> Double -> Double
normalize upperBound x
| x > upperBound = normalize upperBound $ x - upperBound
| x < -upperBound = normalize upperBound $ x + upperBound
| otherwise = x
normLat :: Latitude -> Latitude
normLat (Latitude x) = Latitude $ normalize 90 x
normLong :: Longitude -> Longitude
normLong (Longitude x) = Longitude $ normalize 180 x
(Примечание: это не самое эффективное решение, но я хотел бы сохранить его простым в иллюстративных целях)
И теперь вы можете использовать их для создания "умных конструкторов". Это, по существу, то, что тип Data.Ratio.Ratio
делает с помощью функции %
, чтобы обеспечить предоставление аргументов Integral
и уменьшить эту долю, и не экспортирует фактический конструктор данных :%
.
mkLat :: Double -> Latitude
mkLat = normLat . Latitude
mkLong :: Double -> Longitude
mkLong = normLong . Longitude
Это функции, которые вы экспортировали из своего модуля, чтобы никто не злоупотреблял типами Latitude
и Longitude
. Затем вы можете написать экземпляры типа Num
, которые вызывают normLat
и normLong
внутренне:
instance Num Latitude where
(Latitude x) + (Latitude y) = mkLat $ x + y
(Latitude x) - (Latitude y) = mkLat $ x - y
(Latitude x) * (Latitude y) = mkLat $ x * y
negate (Latitude x) = Latitude $ negate x
abs (Latitude x) = Latitude $ abs x
signum (Latitude x) = Latitude $ signum x
fromInteger = mkLat . fromInteger
И аналогично для Longitude
.
Затем вы можете безопасно выполнять арифметику значений Latitude
и Longitude
, не беспокоясь о том, что они когда-либо выходят за пределы допустимых границ, даже если вы загружаете их в функции из других библиотек. Если это похоже на шаблонный, это так. Возможно, есть более эффективные способы сделать это, но после небольшой настройки у вас есть совместимый API, который сложнее сломать.
Одна действительно приятная функция, реализующая класс Num
typeclass, дает вам возможность конвертировать целочисленные литералы в ваши пользовательские типы. Если вы реализуете класс Fractional
со своей функцией fromRational
, вы получите полные числовые литералы для ваших типов. Предполагая, что вы оба реализованы правильно, вы можете делать такие вещи, как
> 1 :: Latitude
Latitude 1.0
> 91 :: Latitude
Latitude 1.0
> 1234.5 :: Latitude
Latitude 64.5
Конечно, вам нужно убедиться, что функция normalize
на самом деле является той, которую вы хотите использовать, существуют различные реализации, которые вы могли бы подключить там, чтобы получить полезные значения. Вы можете решить, что хотите Latitude 1 + Latitude 90 == Latitude 89
экземпляр Latitude 1
(где значения "отскакивают" назад после достижения верхней границы), или вы можете их обернуть на нижнюю границу, чтобы Latitude 1 + Latitude 90 == Latitude -89
, или вы можете сохраните его, как он есть здесь, где он просто добавляет или вычитает границу до тех пор, пока она не окажется в пределах диапазона. Это зависит от вас, какая реализация подходит для вашего случая использования.