Подтвердить что ты не робот

Как определить тип данных, который принимает только числа?

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

data Point = Point Double Double Double

но я столкнулся с некоторыми проблемами, когда ожидали некоторые фрагменты кода Int s.

Итак, я изменил его на

data Point a = Point a a a

но теперь я хотел бы указать, что a является экземпляром (?) of Num - я хочу только принимать числа в конструкторе.

Возможно ли это? Если нет, то какая принятая практика? Сколько раз я использовал неправильное слово для описания чего-то?

4b9b3361

Ответ 1

Да! По крайней мере, если вы позволите себе некоторые расширения языка GHC. У вас в основном есть четыре варианта, где один плохой, один лучше, один не такой очевидный, как два других, а один - правильный путь.

1. Плохой

Вы можете написать

{-# LANGUAGE DatatypeContexts #-}
data Num a => Point a = Point a a a

Это сделает так, что конструктор Point может быть вызван только с значениями Num a. Однако это не ограничивает содержание значения Point значением Num a. Это означает, что если вы продвигаетесь по дороге, хотите добавить две точки, вам все равно придется делать

addPoints :: Num a => Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}

Вы видите дополнительное объявление Num a? Это не обязательно, поскольку мы знаем, что Point может содержать только Num a в любом случае, но что работает DatatypeContexts! Вы все равно должны устанавливать ограничения на каждую функцию, требующую ее.

Вот почему, если вы включите DatatypeContexts, GHC будет кричать на вас немного для использования "неправильной настройки".

2. Лучше

Решение включает включение GADT. Обобщенные алгебраические типы данных позволяют делать то, что вы хотите. Тогда ваше объявление будет выглядеть как

{-# LANGUAGE GADTs #-}
data Point a where
  Point :: Num a => a -> a -> a -> Point a

При использовании GADT вы объявляете конструкторы, указав вместо них свою подпись типа, почти как при создании типов.

Ограничения для конструкторов GADT имеют преимущество, которое они переносят на созданное значение - в этом случае это означает, что и вы, и компилятор знаете, что только существующие Point a имеют членов Num a s. Поэтому вы можете написать свою функцию addPoint как просто

addPoints :: Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}

без раздражающего дополнительного ограничения.

Боковое примечание: Получение классов для GADT

Вывод классов с помощью GADT (или любого типа, отличного от Haskell-98) требует дополнительного языкового расширения, и это не так гладко, как с обычными ADT. Принцип

{-# LANGUAGE StandaloneDeriving #-}
deriving instance Show (Point a)

Это просто слепо сгенерирует код для класса Show, и вы должны убедиться, что код typechecks.

3. Obscure

Как отмечает shachaf в комментариях к этому сообщению, вы можете получить соответствующие части поведения GADT, сохранив традиционный синтаксис data, включив ExistentialQuantification в GHC. Это делает объявление data простым, как

{-# LANGUAGE ExistentialQuantification #-}
data Point a = Num a => Point a a a

4. Правильный

Однако ни одно из решений выше не является консенсусом в сообществе. Если вы спросите знающих людей (спасибо edwardk и поразительные в канале #haskell для обмена своими знаниями), они скажут вам не ограничивать типы на всех. Они скажут вам, что вы должны определить свой тип как

data Point a = Point a a a

а затем ограничить любые функции, действующие на Point s, например, например, чтобы добавить две точки вместе:

addPoints :: Num a => Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}

Причина не ограничивать ваши типы заключается в том, что при этом вы серьезно ограничиваете свои варианты использования типов позже, как вы, вероятно, не ожидаете. Например, создание экземпляра Functor для вашей точки может быть полезным, например:

instance Functor Point where
  fmap f (Point x y z) = Point (f x) (f y) (f z)

а затем вы можете сделать что-то вроде аппроксимации Point Double с помощью Point Int, просто оценив

round <$> Point 3.5 9.7 1.3

который будет производить

Point 4 10 1

Это было бы невозможно, если бы вы ограничили ваш Point a до Num a только потому, что вы не можете определить экземпляр Functor для такого ограниченного типа. Вам нужно создать свою собственную функцию pointFmap, которая будет противоречить всем возможностям повторного использования и модульности, которые Haskell означает.

Возможно, даже более убедительно, если вы попросите пользователя ввести координаты, но пользователь только входит в два из них, вы можете моделировать это как

Point (Just 4) (Just 7) Nothing

и легко преобразовать его в точку на плоскости XY в трехмерном пространстве путем отображения

fromMaybe 0 <$> Point (Just 4) (Just 7) Nothing

который вернет

Point 4 7 0

Обратите внимание, что этот последний пример не будет работать по двум причинам, если у вас есть ограничение Num a на вашу точку:

  • Вы не сможете определить экземпляр Functor для своей точки и
  • Вы бы не смогли хранить координаты Maybe a в вашей точке.

И это всего лишь один полезный пример того, что вы бы отказались, если вы применили ограничение Num a в точке.

С другой стороны, что вы получаете, ограничивая свои типы? Я могу думать о трех причинах:

  • "Я не хочу случайно создавать Point String и пытаться манипулировать им как число". Вы не сможете. Система типов все равно остановит вас.

  • "Но это для целей документации! Я хочу показать, что точка представляет собой набор числовых значений".... кроме случаев, когда это не так, например Point [-3, 3] [5] [2, 6], который выражает альтернативные координаты по осям, которые могут или не могут быть действительными.

  • "Я не хочу добавлять ограничения Num ко всем моим функциям!" Справедливо. Вы можете скопировать и вставить их из ghci в этом случае. По моему мнению, небольшая работа на клавиатуре стоит всех преимуществ.

Ответ 2

Вы можете использовать GADT для указания ограничения:

{-# Language GADTs #-}

data Point a where
  Point :: (Num a) => a -> a ->  a -> Point a 

Ответ 3

Вы можете использовать класс Num для принудительного ограничения Num для вашего типа данных. Обычный синтаксис будет выглядеть примерно так:

data MyTypeClass a => MyDataType a = MyDataTypeConstructor1 a|MyDataTypeConstructor2 a a|{- and so on... -}

В вашем случае вы можете сделать

data Num a => Point a = Point a a a 

Подробнее о спецификации типа данных и LYAH. Real World Haskell также упоминает об этом.

ИЗМЕНИТЬ

Как упоминалось shachaf, это не совсем допустимо haskell2010, хотя в спецификации упоминается об этом. Следует также отметить, что эта форма редко используется и предпочтительный способ принудительного применения этих ограничений через функции, а не через типы типов/типы данных, поскольку они вносят дополнительную зависимость от типов.