Ответ 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
в этом случае. По моему мнению, небольшая работа на клавиатуре стоит всех преимуществ.