переменная проблема жесткого типа / подозреваемый - PullRequest
7 голосов
/ 07 марта 2019

Исходя из этого q о GADT , я пытаюсь построить EDSL (для примера в статье), но без GADT.У меня есть кое-что, что позволяет избежать дублирования типов данных для AST;но вместо этого кажется удвоение кода.Поэтому я попытался урезать код, из-за чего у меня возникли проблемы ...

Вместо GADT, подобного этому

data Term a where
  Lit ::      Int -> Term Int
  Inc :: Term Int -> Term Int
  IsZ :: Term Int -> Term Bool
  -- etc

объявляет каждый конструктор как отдельный тип данных

data Lit   = Lit Int  deriving (Show, Read)
data Inc a = Inc a    deriving (Show, Read)
data IsZ a = IsZ a    deriving (Show, Read)
-- etc

Затем пользователь EDSL может ввести и show условия

aTerm = IsZ (Inc (Lit 5))

illtypedTerm = Inc (IsZ (Lit 5))    -- accepted OK and can show
                                    -- but can't eval [good!]

Затем, чтобы понять, что все они Term s и должны быть хорошо напечатаны

data Term a = ToTerm { fromTerm :: a} deriving (Show, Eq)

class IsTerm a c  | a -> c 
instance IsTerm Lit (Term Int) 
-- etc

FunDep захватывает тип возврата из исходного GADT.Тогда eval может использовать этот Term тип

class Eval a   
  where eval :: (IsTerm a c) => a -> c 
                             -- hmm this makes 'c' a rigid tyvar
instance Eval Lit  where    
         eval (Lit i) = -- undefined     -- accepted OK, infers :: Term Int
                        ToTerm i         -- rejected

Уравнение eval (Lit i) = undefined (закомментировано) компилируется нормально, и GHC выводит eval (Lit 5) :: Term Int.Но если я поставлю = ToTerm i:

* Couldn't match expected type `c' with actual type `Term Int'
  `c' is a rigid type variable bound by
    the type signature for:
      eval :: forall c. IsTerm Lit c => Lit -> c
* Relevant bindings include
    eval :: Lit -> c

Если GHC может сделать вывод (через FunDep), что c должно быть Term Int для = undefined, почему нельзя для = ToTerm i?Является ли специализированный тип сигнала, который он выводит eval :: forall c. IsTerm Lit c => Lit -> c Impredicative?Но c - это тип возвращаемого значения, поэтому это не RankN (?)

Что позволяет избежать этой ошибки?У меня действительно работает

  • class (IsTerm a c) => Eval a c | a -> c where ..., это просто дублирует все головки экземпляров из IsTerm, поэтому ограничение суперкласса действует только как пояс и фигурные скобки.(Это удвоение, которое я пытался избежать.)

  • type family ToTerm a ...; class Eval a where eval :: a -> ToTerm a.Но опять же экземпляры для Eval удваивают все экземпляры для ToTerm, и, кроме того, нужны большие контексты со многими ~ ограничениями между ToTerm вызовами.

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

Addit: [14 марта]

В документе 2011 года Система F с приведениями к равенству типов , раздел 2.3, приведен этот пример (при обсуждении функциональных зависимостей)

class F a b | a -> b
instance F Int Bool

class D a where { op :: F a b => a -> b }
instance D Int where { op _ = True }

При использовании FC эта проблема [при наборе текстаопределение op в экземпляре D Int] легко решается: приведение в словаре для F позволяет приводить результат op к типу b по мере необходимости.

  • Этот пример похож на q, с классом F, с FunDep, равным IsTerm, и классом D, равным Eval.

    *, 1086 * * 1087.*

    Этот пример не компилируется: выдает тот же отказ, что и IsTerm/Eval.

Ответы [ 3 ]

1 голос
/ 07 марта 2019

Если GHC может сделать вывод (через FunDep), что c должно быть Term Int для = undefined

Не может. Если вы попробуете undefined :: Term Int, вы получите ту же ошибку переменной жесткого типа. Если вы используете напечатанное отверстие = _undefined, вы увидите, что оно выводит undefined :: c. Я не знаю почему, но функциональная зависимость, кажется, используется только при применении eval к Lit, а не при ее определении.

А как насчет этого?

class IsTerm a where
  type TermType a :: *
instance IsTerm Lit where
  type TermType Lit = Int
instance IsTerm a => IsTerm (Inc a) where
  type TermType (Inc a) = TermType a

class IsTerm a => Eval a   
  where eval :: a -> Term (TermType a) 

instance Eval Lit  where    
         eval (Lit i) = ToTerm i

-- needs UndecidableInstances
instance (Eval a, Num (TermType a)) => Eval (Inc a)  where    
         eval (Inc i) = ToTerm (fromTerm (eval i) + 1)
0 голосов
/ 24 марта 2019

Я отправляю другой ответ, потому что это довольно удивительно / редкий зверь: код, который компилируется под Hugs и работает; но не компилируется под GHC (8.6.4). Это если кто-то здесь не увидит, что я ошибся, и не сможет это исправить.

Сначала рассмотрим пример Addit: из статьи 2011 года. Это достигается только в Hugs (последний выпуск WinHugs, сентябрь 2006 г.)

class F a b | a -> b
instance F Int Bool

class D a where { op :: (F a b) => a -> b } 
instance (TypeCast Bool b') => D Int where { op _ = typeCast True }

Этот код такой же, как на бумаге, за исключением instance D Int. Я добавил ограничение TypeCast (и вызов его метода), что выглядит странно, потому что переменная типа b' нигде не используется.

Я пробовал разные способы для компиляции в GHC:

  • TypeCast приблизительно равен ограничению ~ GHC, которое не является функцией Hugs; но изменение его на ~ не позволит GHC принять код.
  • Я пробовал различные типы аннотаций, в том числе с ScopedTypeVariables. Нада.

См. Trac # 16430 для получения более подробной информации, включая определение для TypeCast - которое является полностью стандартным.

Тогда код для исходного q здесь выглядит следующим образом (только для объятий)

class    Eval a   where
         eval :: (IsTerm a c) => a -> c 

instance (TypeCast (Term Int) c') => Eval Lit  where
         eval (Lit i) = typeCast (ToTerm i)       -- rejected without typeCast

instance (Eval a, IsTerm a (Term Int), TypeCast (Term Int) c')
         => Eval (Inc a)  where
         eval (Inc i) = typeCast (ToTerm $ 1 + fromTerm (eval i))
instance (Eval a, IsTerm a (Term Int), TypeCast (Term Bool) c') 
         => Eval (IsZ a)  where
         eval (IsZ i) = typeCast (ToTerm $ 0 == fromTerm (eval i))

eval aTerm (см. Вопрос выше) теперь возвращает (ToTerm False); eval illtypedTerm выдает сообщение об отклонении, как и ожидалось.

Все эти экземпляры имеют значение TypeCast для переменной висячего типа (если слово «feature» - правильное слово). Это спорный вопрос есть ли у каких-либо достичь экономии дублированного кода против решений Eval класс с двумя параметрами, которые только повторы IsTerm.

0 голосов
/ 11 марта 2019

Обратите внимание, что ограничение IsTerm относится к методу eval, а не к ограничению суперкласса для Eval - его там быть не может, поскольку тип возвращаемого значения c для eval не является параметром длякласс.

Тогда вводить в заблуждение instance Eval Lit в отдельности вводит в заблуждение.Рассмотрим экземпляр для Inc и его экземпляр для IsTerm:

instance ( {-# tba #-} ) => Eval (Inc a)  where
  eval (Inc i) = ToTerm $ 1 + fromTerm (eval i)

instance (IsTerm a (Term int)) => IsTerm (Inc a) (Term Int)

Что должно включать это ограничение tba?

  • Тело метода имеет рекурсивный вызовна eval, поэтому для этого ему нужно Eval a.
  • Для тела метода 1 + ... eval ... необходимо eval для возврата Int, но ограничение Eval этого не обеспечивает (посколькутип возврата не упоминается).
  • Может ли ограничение IsTerm a (Term Int) предоставить его?Нет: IsTerm (в лучшем случае) подразумевается Eval, а не наоборот.

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

  • Компилятор увидел рекурсивный вызов eval;волшебным образом добавил ограничение IsTerm a c1 для него;нашел применимый экземпляр IsTerm для a;и затем выяснил, что IsTerm экземпляр дал c1 ~ Term Int.
  • Но если все, что у него есть, это a, то нет никакого способа узнать, какой экземпляр.Так что оставьте ограничение как Wanted в ожидании получения eval примененного к (Inc x) для некоторого термина x?

Сравните, что в определении GADT для Inc его аргумент должен быть Term Int.Это смирительная рубашка, которая не позволяет пользователю EDSL вводить неверно введенный термин:

  Inc :: Term Int -> Term Int

Что касается конкретного сообщения об отклонении для instance Eval Lit, я думаю, что формулировка Хагса делает его более понятным:

- Inferred type is not general enough
*** Expression    : eval
*** Expected type : (Eval Lit, IsTerm Lit a) => Lit -> a
*** Inferred type : (Eval Lit, IsTerm Lit (Term Int)) => Lit -> Term Int

«Ожидаемый тип» - это сигнатура метода с подставленной в него головкой экземпляра. Для проверки типа тела метода, тип которого не является «достаточно общим», потребуется:

  1. с учетом ограничения методакак часть проверки тела метода;
  2. , увидев, что класс для этого ограничения имеет FunDep, и что он улучшает тип возвращаемого значения из типа аргумента;
  3. находит соответствующий экземпляр IsTerm для экземпляра Eval (и экземпляров для любых ограничений на этот экземпляр, рекурсивно);
  4. повышение c до Term int с помощью FunDep.

Шаг 3в частности, это слишком много, чтобы ожидать: применимые экземпляры могут быть объявлены в отдельном модуле, который находится за пределами области видимости;в общем (в отличие от этого примера) может потребоваться экземпляр для схемы типов / доступные экземпляры могут быть более конкретными или более общими / не могут быть выбраны, пока типы не будут более уточнены.

Но у меня естьНерешенный вопрос:

  • Если тип тела метода более специфичен, чем вывод из подстановки в головке экземпляра, что идет не так?
  • Тип вывода в сообщении Hugs болееспецифические, но также содержат ограничения, поэтому они должны быть сняты, чтобы программа была принята.
  • Если вызывающая сторона eval ожидает более общий тип, более конкретный результат может быть заменен на.
...