有关GADT好处的典型示例表示DSL的语法;说here on the wiki或the PLDI 2005 paper。
我可以看到,如果您的AST构造正确,则编写eval
函数很容易。
如何将GADT处理构建为REPL?或更具体地说,是进入Read-Parse-Typecheck-Eval-Print-Loop?我看到您只是将eval
步骤的复杂性推到了先前的步骤。
GHCi是否在内部使用GADT表示其正在评估的表达式? (这些表达式比典型的DSL更加笨拙。)
一方面,您无法derive Show
使用GADT,所以对于“打印”步骤,您可以手动滚动Show
实例或类似的东西:
{-# LANGUAGE GADTs, StandaloneDeriving #-}
data Term a where
Lit :: Int -> Term Int
Inc :: Term Int -> Term Int
IsZ :: Term Int -> Term Bool
If :: Term Bool -> Term a -> Term a -> Term a
Pair :: (Show a, Show b) => Term a -> Term b -> Term (a,b)
Fst :: (Show b) => Term (a,b) -> Term a
Snd :: (Show a) => Term (a,b) -> Term b
deriving instance (Show a) => Show (Term a)
(在我看来,那些在构造函数中缠结的Show
约束已经无法分离问题了。)
我更多地考虑的是有人输入DSL表达式的用户体验,而不是程序员对eval
函数的便利。要么:
Read
实例是一项艰巨的工作。所以也许在最后一个项目符号上,我似乎还支持“智能构造函数”,即GADT应该改进(?)。此外,我还在某处加倍了工作。
我没有“更好的方法”来解决它。我想知道如何在实践中处理DSL应用程序。 (对于上下文:我正在考虑一个数据库查询环境,在该环境中,类型推断必须查看数据库中字段的类型以验证对其执行的操作。)
添加:完成@Alec的回答
我看到glambda
中用于漂亮打印的代码涉及几层类和实例。与GADT所声称的AST优势相比,这里有些不对劲。 (类型正确的)AST的想法是,您同样可以轻松地进行评估:或漂亮地打印它;或对其进行优化;或从中生成代码;等
glambda
似乎是针对评估的(考虑到练习的目的,这很公平)。我想知道...
为什么需要在一种数据类型中表达(E)DSL的整个语法? (以Wikibook为例,开始做草data Expr = ...
的稻草人,并迅速遇到类型麻烦。当然,它确实做到了;这永远行不通;几乎任何事情都比这更好;我感到上当受骗。)
如果最终还是要编写类和实例,为什么不使每个语法产生都成为单独的数据类型:data Lit = Lit Int
... data If b a1 a2 = If b a1 a2
...然后是class IsTerm a c | a -> c where ...
(例如FunDep
或类型家族,其实例告诉我们术语的结果类型。)
现在,EDSL使用相同的构造函数(用户不在乎它们来自不同的数据类型);并且他们应用了“草率”的类型检查。漂亮的打印/错误报告也不需要严格的类型检查。 Eval会这样做,并坚持将IsTerm
实例全部排成一行。
我以前没有建议过这种方法,因为它似乎涉及太多繁琐的代码。但这实际上并不比glambda
差,也就是说,当您考虑整个功能时,不仅要考虑评估步骤。
在我看来,仅表达一次语法是一个很大的优势。此外,它似乎更具可扩展性:为每个语法生成添加新的数据类型,而不是打开现有的数据类型。哦,因为它们是H98数据类型(没有存在),所以deriving
可以正常工作。
答案 0 :(得分:4)
请注意,GHCi不使用GADT表示表达式。甚至GHC的内部核心表达式类型Expr
都不是GADT。
为使您的Term
类型的实例更加充实,请考虑使用glambda
。其Exp
类型甚至可以在类型级别上跟踪变量。
还有第二种UExp
数据类型,正如您所观察到的那样,它实际上是从REPL中解析出来的。然后,将这种类型进行类型检入到Exp
中,并传递给以下内容的延续:
check :: (MonadError Doc m, MonadReader Globals m)
=> UExp -> (forall t. STy t -> Exp '[] t -> m r)
-> m r
UExp
和Exp
的漂亮印刷是手写的,但至少是uses the same code(通过PrettyExp
类实现)。
evaluation code itself很漂亮,但我怀疑我需要以此为卖点。 :)
据我了解,GADT对于EDSL(嵌入式DSL)非常出色,因为它们只是大型Haskell程序中代码的一部分。是的,类型错误可能很复杂(并且将直接来自GHC),但这是您能够在代码中维护类型级不变式的代价。例如,考虑一下hoopl
在CFG中基本块的表示形式:
data Block n e x where
BlockCO :: n C O -> Block n O O -> Block n C O
BlockCC :: n C O -> Block n O O -> n O C -> Block n C C
BlockOC :: Block n O O -> n O C -> Block n O C
BNil :: Block n O O
BMiddle :: n O O -> Block n O O
BCat :: Block n O O -> Block n O O -> Block n O O
BSnoc :: Block n O O -> n O O -> Block n O O
BCons :: n O O -> Block n O O -> Block n O O
当然,您可以打开讨厌的类型错误,但是您还可以在类型级别跟踪失败提示信息。这使得考虑数据流问题变得更加容易。
我要提出的观点是:如果您的GADT是由String
(或自定义的REPL)构建的,则执行翻译的时间会很艰难。这是不可避免的,因为您要做的实际上是重新实现一个简单的类型检查器。最好的选择是像glambda
那样直面这一点,并从类型检查中区分出解析。
但是,如果您有能力保持在Haskell代码的范围内,则可以只对GHC进行手工解析和类型检查。恕我直言,EDSL比非嵌入式DSL更酷,更实用。