ExistentialQuantification давайте скроем переменную типа. Это можно было бы сделать для единообразного управления разными типами значений.

Антипаттерн класса экзистенциального типа

В Haskell воссоздание антипаттерна класса экзистенциального типа является почти ритуалом. Вот как это происходит.

Вы хотите составить список вещей, которые можно показать, например:

map show [1, 'h', True] -- This won't compile

За исключением того, что Haskell не может напрямую создавать список разных типов. Однако вы можете составить список показываемых вещей. ExistentialQuantification поможет вам в этом:

data Showable = forall a. Show a => Showable a
instance Show Showable where
  show (Showable x) = show x

Showable - это тип, который скрывает тип a с помощью количественной оценки существования, которая должна иметь Show экземпляр. Теперь мы можем создать список из Showable вещей:

map show [Showable 1, Showable 'h', Showable True] -- compiles!

Так что все кажется хорошо, за исключением того, что это был очень косвенный способ написать:

[show 1, show 'h', show True]

Хуже того, типы с экзистенциальными типами не являются стандартными типами, поэтому работать с ними труднее, чем с типами Haskell 2010 (обратите внимание, что я не смог получить экземпляр Show).

Антипаттерн класса экзистенциального типа задерживает использование методов класса типа с дополнительным типом-оболочкой, но мы можем просто сделать вызовы функций заранее. Если нам действительно нужно отложить выполнение метода, мы можем без особого труда: Haskell ленив.

Шаблон 1: Промежуточное значение

Как только вы поймете, что создание унифицированного типа для приложения метода класса типов не является хорошим использованием классов типов, возникает вопрос о том, что такое хорошее использование.

Шаблон «Промежуточное значение» - это способ скрыть ненужный тип, который используется внутри функции (-ями).

Хороший пример - тип Fold из пакета foldl:

data Fold a b
  -- | @Fold @ @ step @ @ initial @ @ extract@
  = forall x. Fold (x -> a -> x) x (x -> b)

Тип Fold содержит начальное значение x (второй аргумент конструктора). При первом вызове «шага» (первый аргумент) ему передается начальное x, и результатом «шага» является обратная связь с самим собой снова и снова. Наконец, в конце вычисления результат «шага» (x) преобразуется в b последним аргументом «извлечение».

После создания Fold невозможно определить тип x. Хотя это нормально. Важно только то, что x того же типа, что и для функций «step» и «extract».

Тип x выставлять незачем. Если бы Fold был Fold x a b вместо Fold a b, это сделало бы библиотеку излишне сложной в использовании (и в некоторых случаях невозможной для написания) без реальной пользы.

Другой пример этого паттерна - свободный аппликатив (шляпа Дэвида Ривера).

Паттерн 2: скрытые типы, которые можно скрыть

Основное правило состоит в том, что, когда кто-то скрывает тип с экзистенциальным, его нельзя получить. Однако это еще не все.

В GHC есть функция, называемая равенством типов, которая включается несколькими расширениями, одним из которых является GADTs.

При равенстве типов я могу написать конструктор:

data Hideable a = a ~ Int  => IntHideable a 
                | a ~ Char => CharHideable

Тогда я могу скрыть a новым типом и экзистенциальным:

data AnyHideable = forall a. AnyHideable (Hideable a)

Если я сопоставлю образец на AnyHideable, я не буду знать, какой у меня тип Hideable.

incHideable :: Hideable Int -> Int
incHideable (IntHideable x) = x + 1
foo :: AnyHideable -> IO ()
foo (AnyHideable x) = case x of -- x is of type Hideable a
...

Однако, когда я сопоставляю шаблон в конструкторе IntHideable, он переносит ограничение a ~ Int в область видимости.

incHideable :: Hideable Int -> Int
incHideable (IntHideable x) = x + 1
foo :: AnyHideable -> IO ()
foo (AnyHideable x) = case x of
  IntHideable {} -> print $ incHideable x -- x is a (Hideable Int)
  ... 

На этом этапе компилятор будет знать, что Hideable a на самом деле Hideable Int, и я могу использовать на нем incHideable.

Хорошо, это надуманный пример. Дело в том, что мы можем перейти от унифицированного типа AnyHideable и восстановить информацию о типе Hideable a. Экзистенциально количественно определенный AnyHideable может быть приятным, потому что мы относимся к нему единообразно, но если мы хотим работать с конкретным Hideable, мы можем сделать это с помощью сопоставления с образцом.

Большинство Haskeller не видели, чтобы равенство типов использовалось таким образом, но это потому, что GHC имеет хороший синтаксис для таких типов, в основном синтаксис GADT. Тот же тип, написанный с синтаксисом GADT, выглядит так:

data Hideable a where
  IntHideable  :: Int  -> Hideable Int
  CharHideable :: Char -> Hideable Char

Под капотом творится такая же магия равенства.

Обновление от 29 мая: Дэвид Фойер указал на Reddit, что есть еще несколько примеров, которые я обсуждаю ниже.

Паттерн 3: Структуры данных, выровненные по типу

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

Здесь я создаю выровненный по типу список для хорошо типизированных путей между состояниями TCP. Я создаю вид и отдельные типы для состояний (State), и каждый переход или стрелка на диаграмме состояний является конструктором в GADT (тип Path).

data State = Closed | Listen | SynSent | SynReceived | Established
           | CloseWait | LastAck | FinWait1 | FinWait2 | Closing
           | TimeWait
data Path :: State -> State -> * where
  ServerStart          :: Path Closed      Closed
  PassiveOpen          :: Path Closed      Listen
  ServerClose          :: Path Listen      Closed
  ServerSynRecieved    :: Path Listen      SynReceived
  ClientSynRecieved    :: Path SynReceived SynReceived
  SynResponded         :: Path SynReceived Established
  ToFinWait1           :: Path Established FinWait1
  ActiveOpen           :: Path Closed      SynSent
  PassiveToActive      :: Path Listen      SynSent
  ClientClose          :: Path SynSent     Closed
  FinishHandshake      :: Path SynSent     Established
  PassiveClose         :: Path Established CloseWait
  ToLastAck            :: Path CloseWait   LastAck
  EarlyClose           :: Path SynReceived FinWait1
  ActiveClose          :: Path Established FinWait1
  SimulatanousClose    :: Path FinWait1    Closing
  SimulatanousCloseAck :: Path Closing     TimeWait
  FastClose            :: Path FinWait1    TimeWait
  CloseAcknowledge     :: Path FinWait1    FinWait2
  FinishClose          :: Path FinWait2    TimeWait
  Waited               :: Path TimeWait    Closed
  (:::)                :: Path a b -> Path b c -> Path a c
  
connectAndClose :: Path Closed Closed
connectAndClose = ServerStart ::: ActiveOpen ::: ClientClose

Количественная оценка существования неявно используется в конструкторе :::, чтобы скрыть промежуточные типы (b). connectAndClose - это пример действительного пути, который проходит через Closed, SynSent и обратно в Closed состояния. Промежуточное состояние SynSent должно совпадать, но оно не отображается в типе connectAndClose.

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

Отношение к RankNTypes

В квантификации существования используется синтаксис forall почти неотличимым отRankNTypes способом. Это приводит многих хаскеллеров к выводу, что они похожи на расширения. Хотя экзистенциальная количественная оценка использует один и тот же синтаксис, она используется для разных целей (даже если да, полиморфизм более высокого ранга может кодировать экзистенциальную количественную оценку).

Вывод

ExistentialQuantification - это способ GHC скрыть информацию о типе. В некоторых ситуациях эту информацию можно восстановить или можно скрыть информацию вместе с функциями для работы со скрытым типом. Если держаться подальше от антипаттерна класса экзистенциального типа, есть шанс, что ExistentialQuantification не будут так часто использоваться. Это нормально.