Аннотации программных типов в Haskell

При метапрограммировании может быть полезно (или необходимо) передать системную информацию типа Haskell о типах, известных вашей программе, но не выводимых в Hindley-Milner. Есть ли библиотека (или расширение языка и т.д.), Которая предоставляет возможности для этого - то есть аннотации программных типов - в Haskell?

Рассмотрим ситуацию, когда вы работаете с гетерогенным списком (например, с использованием библиотеки Data.Dynamic или экзистенциальной квантификации), и вы хотите отфильтровать список до стандартного, однородно типизированного списка Haskell. Вы можете написать такую ​​функцию, как

import Data.Dynamic
import Data.Typeable

dynListToList :: (Typeable a) => [Dynamic] -> [a]
dynListToList = (map fromJust) . (filter isJust) . (map fromDynamic)

и назовите его с помощью аннотации ручного типа. Например,

foo :: [Int]
foo = dynListToList [ toDyn (1 :: Int)
                    , toDyn (2 :: Int)
                    , toDyn ("foo" :: String) ]

Здесь foo - список [1, 2] :: [Int]; это прекрасно работает, и вы вернулись на твердую почву, где система типа Haskell может сделать свое дело.

Теперь представьте, что вы хотите сделать то же самое, но (а) в то время, когда вы пишете код, вы не знаете, какой должен быть тип списка, созданный вызовом dynListToList (b), ваша программа содержит информацию, необходимую для ее определения, только (c) она не доступна в форме, доступной для системы типов.

Например, скажите, что вы случайно выбрали элемент из своего гетерогенного списка и хотите отфильтровать список по этому типу. Используя средства проверки типов, предоставляемые Data.Typeable, ваша программа имеет всю информацию, необходимую для этого, но, насколько я могу судить, в этом суть вопроса: нет способа передать его вместе с типом система. Здесь некоторый псевдо-Haskell, который показывает, что я имею в виду:

import Data.Dynamic
import Data.Typeable

randList :: (Typeable a) => [Dynamic] -> IO [a]
randList dl = do
    tr <- randItem $ map dynTypeRep dl
    return (dynListToList dl :: [<tr>])  -- This thing should have the type
                                         -- represented by `tr`

(Предположим, что randItem выбирает случайный элемент из списка.)

Без аннотации типа в аргументе return компилятор скажет вам, что он имеет "неоднозначный тип" и просит вас его предоставить. Но вы не можете предоставить аннотацию типа вручную, потому что тип не известен во время записи (и может варьироваться); однако тип известен во время выполнения, хотя и в форме, которую система типов не может использовать (здесь требуемый тип представлен значением tr, a TypeRep -see Data.Typeable для деталей).

Псевдокод :: [<tr>] - это волшебство, которое я хочу совершить. Существует ли какой-либо способ программно указать систему типов с информацией о типе; то есть с информацией о типе, содержащейся в значении в вашей программе?

В основном я ищу функцию с (псевдо) типа ??? -> TypeRep -> a, которая принимает значение типа, неизвестного системе типа Haskell, и TypeRep и говорит: "Поверьте мне, компилятор, я знаю, что я" Это дело имеет значение, представленное этим TypeRep". (Обратите внимание, что это не то, что делает unsafeCoerce.)

Или есть что-то совершенно другое, что дает мне одно и то же место? Например, я могу представить языковое расширение, которое позволяет присваивать переменные типа, например, расширенную версию расширения, позволяющую использовать переменные типа.

(Если это невозможно или крайне нецелесообразно, например, он требует упаковки полного GHCi-подобного интерпретатора в исполняемый файл - попробуйте объяснить, почему.)

Ответы

Ответ 1

Нет, вы не можете этого сделать. Длинным и коротким является то, что вы пытаетесь написать зависимую функцию, и Haskell не является навязчивым языком; вы не можете поднять значение TypeRep до истинного типа, и поэтому нет способа записать тип требуемой функции. Чтобы объяснить это более подробно, я сначала покажу, почему так, как вы сформулировали тип randList, на самом деле не имеет смысла. Затем я объясню, почему вы не можете делать то, что хотите. Наконец, я кратко расскажу о нескольких мыслях о том, что на самом деле делать.

экзистенциалы

Ваша подпись типа для randList не может означать, что вы хотите, чтобы она имела в виду. Помня, что все переменные типа в Haskell универсально оцениваются, он читает

randList :: forall a. Typeable a => [Dynamic] -> IO [a]

Таким образом, я имею право называть его как, скажем, randList dyns :: IO [Int] где угодно; Я должен иметь возможность предоставить возвращаемое значение для всех a, а не просто для некоторого a. Размышляя об этом как о игре, она может выбрать a, а не сама функция. То, что вы хотите сказать (это неверно, синтаксис Haskell, хотя вы можете перевести его в действительный Haskell с помощью экзистенциального типа данных 1), больше похож на

randList :: [Dynamic] -> (exists a. Typeable a => IO [a])

Этот promises, что элементы списка имеют некоторый тип a, который является экземпляром Typeable, но не обязательно каким-либо таким типом. Но даже при этом у вас будут две проблемы. Во-первых, даже если бы вы могли создать такой список, что бы вы могли с ним сделать? И, во-вторых, оказывается, что вы даже не можете его построить.

Поскольку все, что вы знаете об элементах экзистенциального списка, состоит в том, что они являются экземплярами Typeable, что вы можете с ними сделать? Посмотрев на документацию, мы видим, что есть только две функции 2 которые принимают экземпляры Typeable:

Таким образом, все, что вы знаете о типе элементов в списке, состоит в том, что вы можете называть typeOf и cast на них. Поскольку мы никогда не сможем с пользой сделать что-нибудь еще с ними, наш экзистенциальный способ может быть таким же (опять же, недействительным Haskell)

randList :: [Dynamic] -> IO [(TypeRep, forall b. Typeable b => Maybe b)]

Это то, что мы получаем, если мы применим typeOf и cast к каждому элементу нашего списка, сохраним результаты и выбросим теперь бесполезное экзистенциально типизированное исходное значение. Очевидно, что часть TypeRep этого списка не полезна. И вторая половина списка тоже. Поскольку мы возвращаемся к универсально-квантованному типу, вызывающий объект randList снова имеет право запросить, чтобы они получили Maybe Int, a Maybe Bool или Maybe b для любого (настраиваемого) b по их выбору. (Фактически, они имеют немного больше мощности, чем раньше, поскольку они могут создавать разные элементы списка для разных типов.) Но они не могут понять, из какого типа они преобразуются, если они уже не знают об этом - вы все еще потеряли информацию о типе, которую вы пытались сохранить.

И даже отбросив тот факт, что они не полезны, вы просто не можете создать желаемый экзистенциальный тип здесь. Ошибка возникает при попытке вернуть экзистенциально типизированный список (return $ dynListToList dl). На какой конкретный тип вы звоните dynListToList? Напомним, что dynListToList :: forall a. Typeable a => [Dynamic] -> [a]; таким образом, randList отвечает за сбор, который будет использовать a dynListToList. Но он не знает, какой a выбрать; опять же, что источник вопроса! Таким образом, тип, который вы пытаетесь вернуть, не указан и, следовательно, неоднозначен. 3

Зависимые типы

ОК, так что бы сделать эту экзистенциальную полезную (и возможную)? Ну, у нас есть немного больше информации: мы не только знаем там a, у нас есть TypeRep. Поэтому, возможно, мы сможем упаковать это:

randList :: [Dynamic] -> (exists a. Typeable a => IO (TypeRep,[a]))

Это не совсем хорошо; TypeRep и [a] вообще не связаны. И это именно то, что вы пытаетесь выразить: каким-то образом связать TypeRep и a.

В принципе, ваша цель - написать что-то вроде

toType :: TypeRep -> *

Здесь * - тип всех типов; если вы раньше не видели видов, они должны вводить типы, которые относятся к значениям. * классифицирует типы, * -> * классифицирует конструкторы типа с одним аргументом и т.д. (например, Int :: *, Maybe :: * -> *, Either :: * -> * -> * и Maybe Int :: *.)

С этим вы могли бы написать (еще раз, этот код недействителен Haskell, на самом деле он действительно имеет только мимолетное сходство с Haskell, так как вы не можете его написать или что-то подобное в системе типа Haskell ):

randList :: [Dynamic] -> (exists (tr :: TypeRep).
                           Typeable (toType tr) => IO (tr, [toType tr]))
randList dl = do
  tr <- randItem $ map dynTypeRep dl
  return (tr, dynListToList dl :: [toType tr])
    -- In fact, in an ideal world, the `:: [toType tr]` signature would be
    -- inferable.

Теперь вы обещаете правильную вещь: не то, что существует какой-то тип, который классифицирует элементы списка, но существует некоторый TypeRep, так что его соответствующий тип классифицирует элементы списка. Если бы вы могли это сделать, вы были бы настроены. Но запись toType :: TypeRep -> * в Haskell полностью невозможна: для этого требуется язык, зависящий от языка, поскольку toType tr - это тип, который зависит от значения.

Что это значит? В Haskell это вполне приемлемо для значений, зависящих от других значений; это функция. Значение head "abc", например, зависит от значения "abc". Аналогично, у нас есть конструкторы типов, поэтому допустимо, чтобы типы зависели от других типов; рассмотрим Maybe Int и как это зависит от Int. Мы можем даже иметь значения, зависящие от типов! Рассмотрим id :: a -> a. Это действительно семейство функций: id_Int :: Int -> Int, id_Bool :: Bool -> Bool и т.д. Какое из них зависит от типа a. (Так что действительно, id = \(a :: *) (x :: a) -> x, хотя мы не можем записать это в Haskell, есть языки, где мы можем.)

Тем не менее, у нас никогда не может быть типа, который зависит от значения. Нам может понадобиться такая вещь: представьте Vec 7 Int, тип длин-7 списков целых чисел. Здесь Vec :: Nat -> * -> *: тип, первым аргументом которого должно быть значение типа Nat. Но мы не можем писать такие вещи в Haskell. 4 Языки, которые поддерживают это, называются зависимыми (и мы будем писать id, как мы это делали выше); примеры включают Coq и Agda. (Такие языки часто дублируются как помощники по коррекциям и обычно используются для исследовательской работы, а не для написания реального кода. Зависимые типы сложны, а их полезность для повседневного программирования - это активная область исследований.)

Таким образом, в Haskell мы можем сначала проверить все о наших типах, выбросить всю эту информацию и затем скомпилировать то, что относится только к значениям. Фактически, это именно то, что делает GHC; поскольку мы никогда не можем проверять типы во время выполнения в Haskell, GHC стирает все типы во время компиляции без изменения поведения во время выполнения программы. Вот почему unsafeCoerce легко реализовать (оперативно) и полностью небезопасно: во время выполнения он не работает, но лежит в системе типов. Следовательно, нечто вроде toType полностью невозможно реализовать в системе типа Haskell.

Фактически, как вы заметили, вы даже не можете записать нужный тип и использовать unsafeCoerce. Для некоторых проблем вы можете избежать этого; мы можем записать тип для функции, но только реализуем ее с помощью обмана. Именно так работает fromDynamic. Но, как мы видели выше, нет даже хорошего типа, чтобы дать эту проблему изнутри Haskell. Мнимая функция toType позволяет вам указать тип программы, но вы не можете записать тип toType!

Что теперь?

Итак, вы не можете этого сделать. Что вы должны сделать? Я предполагаю, что ваша общая архитектура не идеальна для Haskell, хотя я ее не видел; Typeable и Dynamic фактически не отображаются в программах Haskell. (Возможно, вы говорите Haskell с акцентом на Python), как говорится.) Если у вас есть только конечный набор типов данных, с которыми вы имеете дело, возможно, вы сможете связать вещи с простым старым алгебраическим типом данных:

data MyType = MTInt Int | MTBool Bool | MTString String

Затем вы можете написать isMTInt и просто использовать filter isMTInt или filter (isSameMTAs randomMT).

Хотя я не знаю, что это такое, вероятно, вы могли бы unsafeCoerce пройти через эту проблему. Но, честно говоря, это не очень хорошая идея, если вы действительно, действительно, действительно, действительно, действительно, действительно знаете, что делаете. И даже тогда, вероятно, нет. Если вам нужно unsafeCoerce, вы узнаете, что это будет не просто удобная вещь.

Я действительно согласен с комментарием Даниэля Вагнера: вы, вероятно, захотите пересмотреть свой подход с нуля. Опять же, поскольку я не видел вашей архитектуры, я не могу сказать, что это будет означать. Может быть, там есть еще один вопрос, если вы можете устранить конкретную трудность.


1 Это выглядит следующим образом:

{-# LANGUAGE ExistentialQuantification #-}
data TypeableList = forall a. Typeable a => TypeableList [a]
randList :: [Dynamic] -> IO TypeableList

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

2Технически есть некоторые другие функции, которые выглядят релевантными, например toDyn :: Typeable a => a -> Dynamic и fromDyn :: Typeable a => Dynamic -> a -> a. Тем не менее, Dynamic является более или менее экзистенциальной оболочкой вокруг Typeable s, полагаясь на typeOf и TypeRep, чтобы знать, когда на unsafeCoerce (GHC использует некоторые типы реализации и unsafeCoerce, но вы можете сделайте это, за исключением, возможно, dynApply/dynApp), поэтому toDyn не делает ничего нового. И fromDyn действительно не ожидает своего аргумента типа a; это всего лишь обертка вокруг cast. Эти функции и другие аналогичные функции не обеспечивают дополнительной мощности, недоступной только с помощью typeOf и cast. (Например, возврат к Dynamic не очень полезен для вашей проблемы!)

3 Чтобы увидеть ошибку в действии, вы можете попытаться скомпилировать следующую полную программу Haskell:

{-# LANGUAGE ExistentialQuantification #-}
import Data.Dynamic
import Data.Typeable
import Data.Maybe

randItem :: [a] -> IO a
randItem = return . head -- Good enough for a short and non-compiling example

dynListToList :: Typeable a => [Dynamic] -> [a]
dynListToList = mapMaybe fromDynamic

data TypeableList = forall a. Typeable a => TypeableList [a]

randList :: [Dynamic] -> IO TypeableList
randList dl = do
  tr <- randItem $ map dynTypeRep dl
  return . TypeableList $ dynListToList dl -- Error!  Ambiguous type variable.

Конечно, если вы попытаетесь скомпилировать это, вы получите сообщение об ошибке:

SO12273982.hs:17:27:
    Ambiguous type variable `a0' in the constraint:
      (Typeable a0) arising from a use of `dynListToList'
    Probable fix: add a type signature that fixes these type variable(s)
    In the second argument of `($)', namely `dynListToList dl'
    In a stmt of a 'do' block: return . TypeableList $ dynListToList dl
    In the expression:
      do { tr <- randItem $ map dynTypeRep dl;
           return . TypeableList $ dynListToList dl }

Но как и вся суть вопроса, вы не можете "добавить сигнатуру типа, которая исправляет эти переменные типа", потому что вы не знаете, какой тип вы хотите.

4 В основном. GHC 7.4 поддерживает подъем типов к видам и для полиморфизма видов; см. раздел 7.8, "Вид полиморфизма и продвижение" , в руководстве пользователя GHC 7.4. Это не приводит к тому, что Haskell будет набираться с наложением - что-то вроде TypeRep -> * пока не выйдет 5 но вы сможете написать Vec с помощью очень выразительных типов, которые выглядят как значения.

5 С технической точки зрения, теперь вы можете записать что-то похожее на желаемый тип: type family ToType :: TypeRep -> *. Однако для этого требуется тип продвигаемого вида TypeRep, а не значение типа TypeRep; и, кроме того, вы все равно не сможете его реализовать. (По крайней мере, я так не думаю, и я не понимаю, как вы это сделаете, но я не специалист в этом.) Но на данный момент мы довольно далеко.

Ответ 2

То, что вы наблюдаете, это то, что тип TypeRep на самом деле не несет на себе никакой информации на уровне уровня; только информация о терминах. Это позор, но мы можем добиться большего успеха, когда узнаем все конструкторы типов, о которых мы заботимся. Например, предположим, что мы заботимся только о Int s, списках и типах функций.

{-# LANGUAGE GADTs, TypeOperators #-}

import Control.Monad

data a :=: b where Refl :: a :=: a
data Dynamic where Dynamic :: TypeRep a -> a -> Dynamic
data TypeRep a where
    Int   :: TypeRep Int
    List  :: TypeRep a -> TypeRep [a]
    Arrow :: TypeRep a -> TypeRep b -> TypeRep (a -> b)

class Typeable a where typeOf :: TypeRep a
instance Typeable Int where typeOf = Int
instance Typeable a => Typeable [a] where typeOf = List typeOf
instance (Typeable a, Typeable b) => Typeable (a -> b) where
    typeOf = Arrow typeOf typeOf

congArrow :: from :=: from' -> to :=: to' -> (from -> to) :=: (from' -> to')
congArrow Refl Refl = Refl

congList :: a :=: b -> [a] :=: [b]
congList Refl = Refl

eq :: TypeRep a -> TypeRep b -> Maybe (a :=: b)
eq Int Int = Just Refl
eq (Arrow from to) (Arrow from' to') = liftM2 congArrow (eq from from') (eq to to')
eq (List t) (List t') = liftM congList (eq t t')
eq _ _ = Nothing

eqTypeable :: (Typeable a, Typeable b) => Maybe (a :=: b)
eqTypeable = eq typeOf typeOf

toDynamic :: Typeable a => a -> Dynamic
toDynamic a = Dynamic typeOf a

-- look ma, no unsafeCoerce!
fromDynamic_ :: TypeRep a -> Dynamic -> Maybe a
fromDynamic_ rep (Dynamic rep' a) = case eq rep rep' of
    Just Refl -> Just a
    Nothing   -> Nothing

fromDynamic :: Typeable a => Dynamic -> Maybe a
fromDynamic = fromDynamic_ typeOf

Все вышеизложенное довольно стандартно. Подробнее о стратегии разработки вы хотите прочитать о GADT и одноэлементных типах. Теперь функция, которую вы хотите написать, следует; тип будет выглядеть немного глупым, но медведь со мной.

-- extract only the elements of the list whose type match the head
firstOnly :: [Dynamic] -> Dynamic
firstOnly [] = Dynamic (List Int) []
firstOnly (Dynamic rep v:xs) = Dynamic (List rep) (v:go xs) where
    go [] = []
    go (Dynamic rep' v:xs) = case eq rep rep' of
        Just Refl -> v : go xs
        Nothing   ->     go xs

Здесь мы выбрали случайный элемент (я прокатил штамп, и он подошел 1) и извлек только те из них, которые имеют соответствующий тип из списка динамических значений. Теперь мы могли бы сделать то же самое с обычным скучным старым Dynamic из стандартных библиотек; однако то, что мы не могли сделать, используется TypeRep значимым образом. Теперь я продемонстрирую, что мы можем сделать это: мы будем сопоставлять соответствие по TypeRep, а затем использовать закрытое значение в определенном типе, о котором говорит TypeRep.

use :: Dynamic -> [Int]
use (Dynamic (List (Arrow Int Int)) fs) = zipWith ($) fs [1..]
use (Dynamic (List Int) vs) = vs
use (Dynamic Int v) = [v]
use (Dynamic (Arrow (List Int) (List (List Int))) f) = concat (f [0..5])
use _ = []

Заметим, что в правых частях этих уравнений мы используем обернутое значение для разных конкретных типов; совпадение с шаблоном на TypeRep фактически представляет информацию на уровне типа.

Ответ 3

Вам нужна функция, которая выбирает другой тип значений для возврата на основе данных времени выполнения. Хорошо, отлично. Но вся цель этого типа - рассказать вам, какие операции могут выполняться по значению. Когда вы не знаете, какой тип будет возвращен из функции, что вы делаете с возвращаемыми значениями? Какие операции вы можете выполнять на них? Существует два варианта:

  • Вы хотите прочитать тип и выполнить некоторое поведение, основанное на том, какой тип он есть. В этом случае вы можете обслуживать только ограниченный список типов, известных заранее, по существу, путем тестирования "это этот тип? Тогда мы делаем эту операцию...". Это легко возможно в текущей структуре Dynamic: просто верните объекты Dynamic, используя dynTypeRep, чтобы отфильтровать их, и оставьте приложение fromDynamic тем, кто хочет использовать ваш результат. Более того, это может быть возможно без Dynamic, если вы не возражаете устанавливать конечный список типов в коде производителя, а не ваш потребительский код: просто используйте ADT с конструктором для каждого типа, data Thing = Thing1 Int | Thing2 String | Thing3 (Thing,Thing). Этот последний вариант, безусловно, лучший, если это возможно.
  • Вы хотите выполнить некоторую операцию, которая работает по всему семейству типов, потенциально некоторые из которых вы еще не знаете, например. используя операции типа класса. Это сложнее, и это сложно также концептуально, потому что вашей программе не разрешено изменять поведение, основанное на том, существует ли какой-либо экземпляр класса типа - это важное свойство системы классов типов, что введение нового экземпляра может либо сделать проверить тип программы или остановить ее от проверки типов, но она не может изменить поведение программы. Следовательно, вы не можете выбросить ошибку, если ваш список ввода содержит неприемлемые типы, поэтому я действительно не уверен, что что-то, что вы можете сделать, по сути, не связано с тем, что в какой-то момент приходится возвращаться к первому решению.