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

Мотивация позади 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?

4b9b3361

Ответ 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.