#haskell #gadt
Вопрос:
Я не могу реализовать экземпляр Eq
для следующего типобезопасного DSL для выражений, реализованных с помощью GADTs.
data Expr a where
Num :: Int -> Expr Int
Bool :: Bool -> Expr Bool
Plus :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Equal :: Eq a => Expr a -> Expr a -> Expr Bool
Выражения могут быть либо типа Bool
, либо Int
. Существуют конструкторы для литералов Bool
, Num
которые имеют соответствующие типы. Int
Могут быть добавлены только выражения (конструктор Plus
). Условие в If
выражении должно иметь тип Bool
, в то время как обе ветви должны иметь один и тот же тип. Существует также выражение равенства Equal
, операнды которого должны иметь один и тот же тип, и тип выражения равенства таков Bool
.
У меня нет проблем с реализацией интерпретатора eval
для этого DSL. Он компилируется и работает как заклинание:
eval :: Expr a -> a
eval (Num x) = x
eval (Bool x) = x
eval (Plus x y) = eval x eval y
eval (If c t e) = if eval c then eval t else eval e
eval (Equal x y) = eval x == eval y
Тем не менее, я изо всех сил пытаюсь реализовать экземпляр Eq
для DSL. Я попробовал простое синтаксическое равенство:
instance Eq a => Eq (Expr a) where
Num x == Num y = x == y
Bool x == Bool y = x == y
Plus x y == Plus x' y' = x == x' amp;amp; y == y'
If c t e == If c' t' e' = c == c' amp;amp; t == t' amp;amp; e == e'
Equal x y == Equal x' y' = x == x' amp;amp; y == y'
_ == _ = False
Он не проверяет тип (с ghc 8.6.5
), ошибка заключается в следующем:
[1 of 1] Compiling Main ( Main.hs, Main.o )
Main.hs:17:35: error:
• Could not deduce: a2 ~ a1
from the context: (a ~ Bool, Eq a1)
bound by a pattern with constructor:
Equal :: forall a. Eq a => Expr a -> Expr a -> Expr Bool,
in an equation for ‘==’
at Main.hs:17:3-11
‘a2’ is a rigid type variable bound by
a pattern with constructor:
Equal :: forall a. Eq a => Expr a -> Expr a -> Expr Bool,
in an equation for ‘==’
at Main.hs:17:16-26
‘a1’ is a rigid type variable bound by
a pattern with constructor:
Equal :: forall a. Eq a => Expr a -> Expr a -> Expr Bool,
in an equation for ‘==’
at Main.hs:17:3-11
Expected type: Expr a1
Actual type: Expr a2
• In the second argument of ‘(==)’, namely ‘x'’
In the first argument of ‘(amp;amp;)’, namely ‘x == x'’
In the expression: x == x' amp;amp; y == y'
• Relevant bindings include
y' :: Expr a2 (bound at Main.hs:17:25)
x' :: Expr a2 (bound at Main.hs:17:22)
y :: Expr a1 (bound at Main.hs:17:11)
x :: Expr a1 (bound at Main.hs:17:9)
|
17 | Equal x y == Equal x' y' = x == x' amp;amp; y == y'
|
I believe the reason is that the constructor Equal
«forgets» the value of the type parameter a
of its subexpressions and there is no way for the typechecker to ensure subexpressions x
and y
both have the same type Expr a
.
I tried adding one more type parameter to Expr a
to keep track of the type of subexpressions:
data Expr a b where
Num :: Int -> Expr Int b
Bool :: Bool -> Expr Bool b
Plus :: Expr Int b -> Expr Int b -> Expr Int b
If :: Expr Bool b -> Expr a b -> Expr a b -> Expr a b
Equal :: Eq a => Expr a a -> Expr a a -> Expr Bool a
instance Eq a => Eq (Expr a b) where
-- same implementation
eval :: Expr a b -> a
-- same implementation
Этот подход мне не кажется масштабируемым, еще раз добавляются конструкторы с подвыражениями разных типов.
Все это заставляет меня думать, что я неправильно использую GADTs для реализации такого рода DSL. Есть ли способ реализации Eq
для этого типа? Если нет, то каков идиоматический способ выражения такого рода ограничений типа для выражений?
Полный код:
{-# LANGUAGE GADTs #-}
module Main where
data Expr a where
Num :: Int -> Expr Int
Bool :: Bool -> Expr Bool
Plus :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Equal :: Eq a => Expr a -> Expr a -> Expr Bool
instance Eq a => Eq (Expr a) where
Num x == Num y = x == y
Bool x == Bool y = x == y
Plus x y == Plus x' y' = x == x' amp;amp; y == y'
If c t e == If c' t' e' = c == c' amp;amp; t == t' amp;amp; e == e'
Equal x y == Equal x' y' = x == x' amp;amp; y == y'
_ == _ = False
eval :: Expr a -> a
eval (Num x) = x
eval (Bool x) = x
eval (Plus x y) = eval x eval y
eval (If c t e) = if eval c then eval t else eval e
eval (Equal x y) = eval x == eval y
main :: IO ()
main = do
let expr1 = If (Equal (Num 13) (Num 42)) (Bool True) (Bool False)
let expr2 = If (Equal (Num 13) (Num 42)) (Num 42) (Num 777)
print (eval expr1)
print (eval expr2)
print (expr1 == expr1)
Ответ №1:
Ваша проблема в том, что в
Equal x y == Equal x' y' = ...
вполне возможно, что x
и x'
есть разные типы. Например, Equal (Bool True) (Bool True) == Equal (Int 42) (Int 42)
проверка типов, но мы не можем тогда просто сравнивать Bool True == Int 42
, как мы могли бы попытаться сделать в данном Eq
случае.
Вот несколько альтернативных решений. Последний (обобщающий ==
eqExpr
) кажется мне самым простым, но и остальные тоже интересны.
Используйте одноэлементный и вычислительный типы
Мы начинаем с вашего оригинального типа
{-# LANGUAGE GADTs #-}
module Main where
data Expr a where
Num :: Int -> Expr Int
Bool :: Bool -> Expr Bool
Plus :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Equal :: Eq a => Expr a -> Expr a -> Expr Bool
и определите одноэлементный GADT для представления типов, которые у вас есть
data Ty a where
TyInt :: Ty Int
TyBool :: Ty Bool
Затем мы докажем , что ваши типы могут быть только Int
или Bool
, и как вычислить их из выражения.
tyExpr :: Expr a -> Ty a
tyExpr (Num _) = TyInt
tyExpr (Bool _) = TyBool
tyExpr (Plus _ _) = TyInt
tyExpr (If _ t _) = tyExpr t
tyExpr (Equal _ _) = TyBool
Теперь мы можем использовать это и определить Eq
экземпляр.
instance Eq (Expr a) where
Num x == Num y = x == y
Bool x == Bool y = x == y
Plus x y == Plus x' y' = x == x' amp;amp; y == y'
If c t e == If c' t' e' = c == c' amp;amp; t == t' amp;amp; e == e'
Equal x y == Equal x' y' = case (tyExpr x, tyExpr x') of
(TyInt, TyInt ) -> x == x' amp;amp; y == y'
(TyBool, TyBool) -> x == x' amp;amp; y == y'
_ -> False
_ == _ = False
Используйте Типируемый
Мы немного модифицируем оригинальный GADT:
import Data.Typeable
data Expr a where
Num :: Int -> Expr Int
Bool :: Bool -> Expr Bool
Plus :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Equal :: (Typeable a, Eq a) => Expr a -> Expr a -> Expr Bool
Затем мы можем попытаться привести значения к нужным типам: если приведение не удается, у нас было два Equal
s среди разных типов, поэтому мы можем вернуться False
.
instance Eq (Expr a) where
Num x == Num y = x == y
Bool x == Bool y = x == y
Plus x y == Plus x' y' = x == x' amp;amp; y == y'
If c t e == If c' t' e' = c == c' amp;amp; t == t' amp;amp; e == e'
Equal x y == Equal x' y' = case cast (x,y) of
Just (x2, y2) -> x2 == x' amp;amp; y2 == y'
Nothing -> False
_ == _ = False
Обобщить на гетерогенное равенство
Мы можем использовать оригинальный GADT:
data Expr a where
Num :: Int -> Expr Int
Bool :: Bool -> Expr Bool
Plus :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Equal :: Eq a => Expr a -> Expr a -> Expr Bool
и напишите гетерогенный тест на равенство, который может работать, даже если два выражения не имеют одного типа:
eqExpr :: Expr a -> Expr b -> Bool
eqExpr (Num x) (Num y) = x == y
eqExpr (Bool x) (Bool y) = x == y
eqExpr (Plus x y) (Plus x' y') = eqExpr x x' amp;amp; eqExpr y y'
eqExpr (If c t e) (If c' t' e') = eqExpr c c' amp;amp; eqExpr t t' amp;amp; eqExpr e e'
eqExpr (Equal x y) (Equal x' y') = eqExpr x x' amp;amp; eqExpr y y'
eqExpr _ _ = False
В Eq
таком случае данный экземпляр является особым случаем.
instance Eq (Expr a) where
(==) = eqExpr
Заключительная нота
Как отметил Джозеф Сибл в комментариях, во всех этих подходах нам не нужен Eq a
контекст в конкретных примерах. Мы можем просто удалить его:
instance {- Eq a => -} Eq (Expr a) where
...
Далее, в принципе нам даже не очень нужно Eq a
в определении Equal
, поэтому мы могли бы упростить наш GADT:
data Expr a where
Num :: Int -> Expr Int
Bool :: Bool -> Expr Bool
Plus :: Expr Int -> Expr Int -> Expr Int
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Equal :: Expr a -> Expr a -> Expr Bool
Однако, если мы это сделаем, определение eval :: Expr a -> a
становится более сложным в том Equal
случае, когда нам, вероятно, нужно использовать что-то вроде tyExpr
вывода типа, чтобы мы могли использовать ==
.
Комментарии:
1. Я собирался опубликовать ответ, в котором говорится, что вы можете реализовать
Equal x y == Equal x' y'
, выполнив анализ случаев x, y, x’ и y’, поскольку это дает вам представление о параметрах типа. Но реализация этого в качестве общей функции гетерогенного равенства, а не захоронение одного и того же кода в одном случае экземпляра Eq, намного лучше, чем моя идея.2. Этот ответ должен быть обязательным для чтения для всех, кто говорит, что шашки типа просто мешают им. Отличный прагматичный пример использования системы типов для подтверждения свойств ваших типов данных.
3. Вам это нужно
instance Eq a => Eq (Expr a) where
? Разве это не может быть просто такinstance Eq (Expr a) where
?4. @JosephSible-Восстанови права на монику! Нам действительно это не нужно. Мы также не нуждаемся
Eq a
в GADT.