Мотивация позади Phantom Типы?
Дон Стюарт Haskell in the Large, упомянутый Phantom Типы:
data Ratio n = Ratio Double
1.234 :: Ratio D3
data Ask ccy = Ask Double
Ask 1.5123 :: Ask GBP
Я прочитал над ними свои пункты, но я их не понял. Кроме того, я читал Haskell Wiki по этой теме. Тем не менее, я все еще теряю их точку.
Какова мотивация использования типа Phantom?
Ответы
Ответ 1
Чтобы ответить на вопрос "какая мотивация использовать тип phantom". Существует два момента:
- чтобы сделать недопустимые состояния inrepresentable, что хорошо объяснено в Ответ Aadit
- Перенесите часть информации о уровне уровня
Например, у вас могут быть расстояния, отмеченные блоком длины:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Distance a = Distance Double
deriving (Num, Show)
data Kilometer
data Mile
marathonDistance :: Distance Kilometer
marathonDistance = Distance 42.195
distanceKmToMiles :: Distance Kilometer -> Distance Mile
distanceKmToMiles (Distance km) = Distance (0.621371 * km)
marathonDistanceInMiles :: Distance Mile
marathonDistanceInMiles = distanceKmToMiles marathonDistance
И вы можете избежать Mars Climate Orbiter бедствия:
>>> marathonDistanceInMiles
Distance 26.218749345
>>> marathonDistanceInMiles + marathonDistance
<interactive>:10:27:
Couldn't match type ‘Kilometer’ with ‘Mile’
Expected type: Distance Mile
Actual type: Distance Kilometer
In the second argument of ‘(+)’, namely ‘marathonDistance’
In the expression: marathonDistanceInMiles + marathonDistance
В этом "шаблоне" есть небольшие изменения. Вы можете использовать DataKinds
для закрытия набора единиц:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
data LengthUnit = Kilometer | Mile
newtype Distance (a :: LengthUnit) = Distance Double
deriving (Num, Show)
marathonDistance :: Distance 'Kilometer
marathonDistance = Distance 42.195
distanceKmToMiles :: Distance 'Kilometer -> Distance 'Mile
distanceKmToMiles (Distance km) = Distance (0.621371 * km)
marathonDistanceInMiles :: Distance 'Mile
marathonDistanceInMiles = distanceKmToMiles marathonDistance
И он будет работать аналогично:
>>> marathonDistanceInMiles
Distance 26.218749345
>>> marathonDistance + marathonDistance
Distance 84.39
>>> marathonDistanceInMiles + marathonDistance
<interactive>:28:27:
Couldn't match type ‘'Kilometer’ with ‘'Mile’
Expected type: Distance 'Mile
Actual type: Distance 'Kilometer
In the second argument of ‘(+)’, namely ‘marathonDistance’
In the expression: marathonDistanceInMiles + marathonDistance
Но теперь Distance
может быть только в километрах или милях, мы не можем добавить больше единиц позже. Это может быть полезно в некоторых случаях использования.
Мы могли бы также сделать:
data Distance = Distance { distanceUnit :: LengthUnit, distanceValue :: Double }
deriving (Show)
В случае расстояния мы можем разработать дополнение, например, перевести на километры, если задействованы разные единицы. Но это не работает хорошо для валют, отношение которых не является постоянным с течением времени и т.д.
И вместо этого можно использовать GADT для этого, что может быть проще в некоторых ситуациях:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}
data Kilometer
data Mile
data Distance a where
KilometerDistance :: Double -> Distance Kilometer
MileDistance :: Double -> Distance Mile
deriving instance Show (Distance a)
marathonDistance :: Distance Kilometer
marathonDistance = KilometerDistance 42.195
distanceKmToMiles :: Distance Kilometer -> Distance Mile
distanceKmToMiles (KilometerDistance km) = MileDistance (0.621371 * km)
marathonDistanceInMiles :: Distance Mile
marathonDistanceInMiles = distanceKmToMiles marathonDistance
Теперь мы знаем блок также на уровне значения:
>>> marathonDistanceInMiles
MileDistance 26.218749345
Этот подход особенно упрощает Expr a
пример из Aadit answer:
{-# LANGUAGE GADTs #-}
data Expr a where
Number :: Int -> Expr Int
Boolean :: Bool -> Expr Bool
Increment :: Expr Int -> Expr Int
Not :: Expr Bool -> Expr Bool
Стоит отметить, что последние варианты требуют нетривиальных языковых расширений (GADTs
, DataKinds
, KindSignatures
), которые могут не поддерживаться в вашем компиляторе. Это может быть в случае с компилятором Mu, о котором упоминает Дон.
Ответ 2
Мотивация использования типов phantom заключается в том, чтобы специализировать возвращаемый тип конструкторов данных. Например, рассмотрим:
data List a = Nil | Cons a (List a)
Тип возврата как Nil
, так и Cons
по умолчанию равен List a
(который обобщен для всех списков типа a
).
Nil :: List a
Cons :: a -> List a -> List a
|____|
|
-- return type is generalized
Также обратите внимание, что Nil
является конструктором phantom (т.е. его возвращаемый тип не зависит от его аргументов, в этом случае невозможен, но тем не менее тот же).
Поскольку Nil
является конструктором phantom, мы можем специализировать Nil
для любого типа, который мы хотим (например, Nil :: List Int
или Nil :: List Char
).
Обычные алгебраические типы данных в Haskell позволяют вам выбирать тип аргументов конструктора данных. Например, мы выбрали тип аргументов для Cons
выше (a
и List a
).
Однако он не позволяет вам выбрать тип возвращаемого значения конструктора данных. Тип возврата всегда обобщен. Это нормально для большинства случаев. Однако есть исключения. Например:
data Expr a = Number Int
| Boolean Bool
| Increment (Expr Int)
| Not (Expr Bool)
Тип конструкторов данных:
Number :: Int -> Expr a
Boolean :: Bool -> Expr a
Increment :: Expr Int -> Expr a
Not :: Expr Bool -> Expr a
Как вы можете видеть, тип возврата всех конструкторов данных обобщен. Это проблематично, потому что мы знаем, что Number
и Increment
должны всегда возвращать Expr Int
, а Boolean
и Not
должны всегда возвращать Expr Bool
.
Обратные типы конструкторов данных неверны, потому что они слишком общие. Например, Number
не может вернуть Expr a
, но все же это делает. Это позволяет вам писать неправильные выражения, которые не проверяет тип проверки. Например:
Increment (Boolean False) -- you shouldn't be able to increment a boolean
Not (Number 0) -- you shouldn't be able to negate a number
Проблема заключается в том, что мы не можем указать возвращаемый тип конструкторов данных.
Обратите внимание, что все конструкторы данных Expr
являются конструкторами phantom (т.е. их возвращаемый тип не зависит от их аргументов). Тип данных, конструкторы которого являются конструкторами phantom, называется типом phantom.
Помните, что возвращаемый тип конструкторов phantom, таких как Nil
, может быть специализирован для любого типа, который мы хотим. Следовательно, мы можем создать интеллектуальные конструкторы для Expr
следующим образом:
number :: Int -> Expr Int
boolean :: Bool -> Expr Bool
increment :: Expr Int -> Expr Int
not :: Expr Bool -> Expr Bool
number = Number
boolean = Boolean
increment = Increment
not = Not
Теперь мы можем использовать интеллектуальные конструкторы вместо обычных конструкторов, и наша проблема решена:
increment (boolean False) -- error
not (number 0) -- error
Поэтому конструкторы phantom полезны, когда вы хотите специализировать тип возвращаемого значения конструктора данных, а типы phantom - это типы данных, конструкторами которых являются все конструкторы phantom.
Обратите внимание, что конструкторы данных, такие как Left
и Right
, также являются конструкторами phantom:
data Either a b = Left a | Right b
Left :: a -> Either a b
Right :: b -> Either a b
Причина в том, что, хотя тип возвращаемых данных конструкторов данных зависит от их аргументов, они все же обобщены, поскольку они лишь частично зависят от их аргументов.
Простой способ узнать, является ли конструктор данных конструктором phantom:
Все ли переменные типа, появляющиеся в возвращаемом типе конструктора данных, также отображаются в аргументах конструктора данных? Если да, это не конструктор phantom.
Надеюсь, что это поможет.
Ответ 3
В частности, для Ratio D3
мы используем такие богатые типы для управления кодом, ориентированным на тип, так, например, если у вас есть поле где-то в типе Ratio D3
, его редактор отправляется в текстовое поле, принимающее только числовые записи и показывающее точность 3 цифры. Это контрастирует, например, с newtype Amount = Amount Double
, где мы не показываем десятичные цифры, но используем тысячи запятых и вводим синтаксический анализ, например "10 м", как "10 000 000".
В базовом представлении оба значения остаются только Double
s.