在Haskell中,如何根据GADT将无类型的AST解析为有类型的?
In Haskell, how to parse an untyped AST to a typed one based on a GADT?
我正在用 Haskell 编写一种特定于领域的语言,并已确定
具有两个 AST 的设计:一个初始的无类型的表示语法和
代表一切的最终类型。我正在写最后一个作为
一个 GADT 以获得更好的类型检查。
我认为它 几乎 可以工作,但是我在编写一个函数时遇到了问题
转换 initial -> final (检查类型,加上一些其他未显示的东西,
就像所有的引用都对应一个变量)。
这是一个简化的例子:
{-# LANGUAGE GADTs, StandaloneDeriving #-}
module Main where
-- untyped initial AST
data Untyped
= UNum Int
| UStr String
| UAdd Untyped Untyped
deriving (Show, Eq)
-- typed final AST
data Typed a where
TNum :: Int -> Typed Int
TStr :: String -> Typed String
TAdd :: Typed Int -> Typed Int -> Typed Int
deriving instance Eq (Typed a)
deriving instance Show (Typed a)
-- wrapper that allows working with a `Typed a` for any `a`
data TypedExpr where
TypedExpr :: Typed a -> TypedExpr
这是我对 check
功能的尝试。基本情况很简单:
check :: Untyped -> Either String TypedExpr
check (UNum n) = Right $ TypedExpr $ TNum n
check (UStr s) = Right $ TypedExpr $ TStr s
-- check (Uadd e1 e2) = ???
但是如何添加 Add
?它可以递归地评估子表达式以
Either String (TypedExpr (Typed a))
类型的值,但我还没有成功
打开它们,检查类型是否对齐(a
s 应该是 Int
s),然后
之后重新包装。我计划用大模式匹配来完成这一切,但是 GHC
不赞成:
My brain just exploded
I can't handle pattern bindings for existential or GADT data constructors.
Instead, use a case-expression, or do-notation, to unpack the constructor.
解释得很好 here,但我没看懂解释。它似乎
我不要模式匹配。
更新:我一定是在没有注意到的情况下搞砸了其他事情。模式匹配有效,如 Nikita 所示。
所以我摆弄着试图强迫事情进入
正确的形状,但还没有得到任何实质性的东西。如果这些是
只是 Either String SomeValue
我想使用应用程序,对吗?是吗
可以向它们添加另一级别的展开+类型检查吗?我怀疑 is close to what I want since the question is very similar, but again I don't understand it. This 也可能相关。
更新:毕竟是我想要的。但是直到 chi 在没有额外的 Type
类型的情况下编写了下面的中间版本之前,我才明白如何。这是一个可行的解决方案。诀窍是用一个新类型标记 TypedExpr
s,该类型只表示 Typed a
的 return 类型 (a
):
data Returns a where
RNum :: Returns Int
RStr :: Returns String
-- extend TypedExpr to include the return type
data TypedExpr2 where
TypedExpr2 :: Returns a -> Typed a -> TypedExpr2
这样 check
就不必知道每个子表达式是否是原始的
TNum
或 return 是 TNum
:
的函数(如 Add
)
check :: Untyped -> Either String TypedExpr2
check (UNum n) = Right $ TypedExpr2 RNum (TNum n)
check (UStr s) = Right $ TypedExpr2 RStr (TStr s)
check (UAdd u1 u2) = do
-- typecheck subexpressions, then unwrap by pattern matching
TypedExpr2 r1 t1 <- check u1
TypedExpr2 r2 t2 <- check u2
-- check the tags to find out their return types
case (r1, r2) of
-- if correct, create an overall expression tagged with its return type
(RNum, RNum) -> return $ TypedExpr2 RNum $ TAdd t1 t2
_ -> Left "type error"
GHC 足够聪明,知道任何 TypedExpr2
中的两个 a
必须匹配,
因此,如果您尝试使用错误的总体 return 类型
结尾。精彩!
您的确切问题可以通过以下解决方案轻松回答:
check (UAdd (UNum a) (UNum b)) = Right $ TypedExpr $ TAdd (TNum a) (TNum b)
然而,那里有很多设计迷宫的迹象。
您是否意识到,一旦将某些内容放入 TypedExpr
中,您就会丢失有关 a
类型的所有信息?这使您的 check
函数毫无意义。
我理解你这样做是因为只有这样才能统一你的GADT类型,否则你无法实现check
功能。但实际上它只是证明你建模错误,GADT 可能不适合你的用例。
我不明白为什么 UAdd
构造函数超过 Untyped
值,而不是 Int
,我不明白是什么让你选择了这个多阶段AST策略。
我可以继续,但我会在这里中断并建议您重新设计模型。
我的建议是使用 "plain" 类型域的表示(带有解释类型函数),然后将存在类型的 Sing
leton 具体化存储在 TypedExpr
,即
{-# LANGUAGE DataKinds, KindSignatures, TypeFamilies #-}
data Type = TInt | TString
type family InterpT (a :: Type) where
InterpT TInt = Int
InterpT TString = String
-- plus the usual singletons stuff
-- ...
-- and finally
data Typed (a :: Type) where ...
data TypedExpr where
TypedExpr :: Sing a -> Typed a -> TypedExpr
通过这种方式,您可以执行类似
的操作
check (UAdd e1 e2) = do
TypedExpr t1 e1' <- check e1
TypedExpr t2 e2' <- check e2
case testEquality t1 t2 of
Just Refl -> ... use e1' and e2' here, you know they have the same Type
Nothing -> Left ...
找到一个完整的例子。
从技术上讲,它是可行的,但它非常不方便:您必须"dig"直到找到 GADT 构造函数。
check :: Untyped -> Either String TypedExpr
check (UNum n) = return $ TypedExpr $ TNum n
check (UStr s) = return $ TypedExpr $ TStr s
check (UAdd t1 t2) = do
t1 <- check t1
t2 <- check t2
case (t1, t2) of
(TypedExpr (TNum x) , TypedExpr (TNum y))
-> return $ TypedExpr $ TAdd (TNum x ) (TNum y)
(TypedExpr (TAdd x1 x2) , TypedExpr (TNum y))
-> return $ TypedExpr $ TAdd (TAdd x1 x2) (TNum y)
(TypedExpr (TNum x) , TypedExpr (TAdd y1 y2))
-> return $ TypedExpr $ TAdd (TNum x ) (TAdd y1 y2)
(TypedExpr (TAdd x1 x2) , TypedExpr (TAdd y1 y2))
-> return $ TypedExpr $ TAdd (TAdd x1 x2) (TAdd y1 y2)
_ -> Left "type error"
我会寻找替代方案。当构造函数的数量很大时,上述方法会出现组合爆炸。
我正在用 Haskell 编写一种特定于领域的语言,并已确定 具有两个 AST 的设计:一个初始的无类型的表示语法和 代表一切的最终类型。我正在写最后一个作为 一个 GADT 以获得更好的类型检查。
我认为它 几乎 可以工作,但是我在编写一个函数时遇到了问题 转换 initial -> final (检查类型,加上一些其他未显示的东西, 就像所有的引用都对应一个变量)。
这是一个简化的例子:
{-# LANGUAGE GADTs, StandaloneDeriving #-}
module Main where
-- untyped initial AST
data Untyped
= UNum Int
| UStr String
| UAdd Untyped Untyped
deriving (Show, Eq)
-- typed final AST
data Typed a where
TNum :: Int -> Typed Int
TStr :: String -> Typed String
TAdd :: Typed Int -> Typed Int -> Typed Int
deriving instance Eq (Typed a)
deriving instance Show (Typed a)
-- wrapper that allows working with a `Typed a` for any `a`
data TypedExpr where
TypedExpr :: Typed a -> TypedExpr
这是我对 check
功能的尝试。基本情况很简单:
check :: Untyped -> Either String TypedExpr
check (UNum n) = Right $ TypedExpr $ TNum n
check (UStr s) = Right $ TypedExpr $ TStr s
-- check (Uadd e1 e2) = ???
但是如何添加 Add
?它可以递归地评估子表达式以
Either String (TypedExpr (Typed a))
类型的值,但我还没有成功
打开它们,检查类型是否对齐(a
s 应该是 Int
s),然后
之后重新包装。我计划用大模式匹配来完成这一切,但是 GHC
不赞成:
My brain just exploded
I can't handle pattern bindings for existential or GADT data constructors.
Instead, use a case-expression, or do-notation, to unpack the constructor.
解释得很好 here,但我没看懂解释。它似乎 我不要模式匹配。
更新:我一定是在没有注意到的情况下搞砸了其他事情。模式匹配有效,如 Nikita 所示。
所以我摆弄着试图强迫事情进入
正确的形状,但还没有得到任何实质性的东西。如果这些是
只是 Either String SomeValue
我想使用应用程序,对吗?是吗
可以向它们添加另一级别的展开+类型检查吗?我怀疑
更新:Type
类型的情况下编写了下面的中间版本之前,我才明白如何。这是一个可行的解决方案。诀窍是用一个新类型标记 TypedExpr
s,该类型只表示 Typed a
的 return 类型 (a
):
data Returns a where
RNum :: Returns Int
RStr :: Returns String
-- extend TypedExpr to include the return type
data TypedExpr2 where
TypedExpr2 :: Returns a -> Typed a -> TypedExpr2
这样 check
就不必知道每个子表达式是否是原始的
TNum
或 return 是 TNum
:
Add
)
check :: Untyped -> Either String TypedExpr2
check (UNum n) = Right $ TypedExpr2 RNum (TNum n)
check (UStr s) = Right $ TypedExpr2 RStr (TStr s)
check (UAdd u1 u2) = do
-- typecheck subexpressions, then unwrap by pattern matching
TypedExpr2 r1 t1 <- check u1
TypedExpr2 r2 t2 <- check u2
-- check the tags to find out their return types
case (r1, r2) of
-- if correct, create an overall expression tagged with its return type
(RNum, RNum) -> return $ TypedExpr2 RNum $ TAdd t1 t2
_ -> Left "type error"
GHC 足够聪明,知道任何 TypedExpr2
中的两个 a
必须匹配,
因此,如果您尝试使用错误的总体 return 类型
结尾。精彩!
您的确切问题可以通过以下解决方案轻松回答:
check (UAdd (UNum a) (UNum b)) = Right $ TypedExpr $ TAdd (TNum a) (TNum b)
然而,那里有很多设计迷宫的迹象。
您是否意识到,一旦将某些内容放入 TypedExpr
中,您就会丢失有关 a
类型的所有信息?这使您的 check
函数毫无意义。
我理解你这样做是因为只有这样才能统一你的GADT类型,否则你无法实现check
功能。但实际上它只是证明你建模错误,GADT 可能不适合你的用例。
我不明白为什么 UAdd
构造函数超过 Untyped
值,而不是 Int
,我不明白是什么让你选择了这个多阶段AST策略。
我可以继续,但我会在这里中断并建议您重新设计模型。
我的建议是使用 "plain" 类型域的表示(带有解释类型函数),然后将存在类型的 Sing
leton 具体化存储在 TypedExpr
,即
{-# LANGUAGE DataKinds, KindSignatures, TypeFamilies #-}
data Type = TInt | TString
type family InterpT (a :: Type) where
InterpT TInt = Int
InterpT TString = String
-- plus the usual singletons stuff
-- ...
-- and finally
data Typed (a :: Type) where ...
data TypedExpr where
TypedExpr :: Sing a -> Typed a -> TypedExpr
通过这种方式,您可以执行类似
的操作check (UAdd e1 e2) = do
TypedExpr t1 e1' <- check e1
TypedExpr t2 e2' <- check e2
case testEquality t1 t2 of
Just Refl -> ... use e1' and e2' here, you know they have the same Type
Nothing -> Left ...
找到一个完整的例子
从技术上讲,它是可行的,但它非常不方便:您必须"dig"直到找到 GADT 构造函数。
check :: Untyped -> Either String TypedExpr
check (UNum n) = return $ TypedExpr $ TNum n
check (UStr s) = return $ TypedExpr $ TStr s
check (UAdd t1 t2) = do
t1 <- check t1
t2 <- check t2
case (t1, t2) of
(TypedExpr (TNum x) , TypedExpr (TNum y))
-> return $ TypedExpr $ TAdd (TNum x ) (TNum y)
(TypedExpr (TAdd x1 x2) , TypedExpr (TNum y))
-> return $ TypedExpr $ TAdd (TAdd x1 x2) (TNum y)
(TypedExpr (TNum x) , TypedExpr (TAdd y1 y2))
-> return $ TypedExpr $ TAdd (TNum x ) (TAdd y1 y2)
(TypedExpr (TAdd x1 x2) , TypedExpr (TAdd y1 y2))
-> return $ TypedExpr $ TAdd (TAdd x1 x2) (TAdd y1 y2)
_ -> Left "type error"
我会寻找替代方案。当构造函数的数量很大时,上述方法会出现组合爆炸。