GADT的优点的典型示例代表DSL的语法; 说这里的维基或在PLDI 2005年纸.
我可以看到,如果你通过构造得到一个类型正确的AST,那么编写一个eval函数很容易.
如何将GADT处理构建到REPL中?或者更具体地说是Read-Parse-Typecheck-Eval-Print-Loop?我看到你只是将eval步骤的复杂性推到了前面的步骤中.
GHCi是否在内部使用GADT来表示它正在评估的表达式?(表达式比典型的DSL要厚很多.)
首先,您不能derive Show使用GADT,因此对于Print步骤,您可以手动滚动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)
Run Code Online (Sandbox Code Playgroud)
(在我看来,Show构造函数中纠缠的那些约束已经无法将问题分开.)
我更倾向于为进入DSL表达式的人提供用户体验,而不是程序员方便的eval功能.或者:
Read实例真的很辛苦.也许吧在最后一个子弹,我似乎回到了'聪明的构造者',GADT应该改进(?)我的工作在某个地方翻了一倍.
我没有"更好的方法"来接近它.我想知道如何在实践中处理DSL应用程序.(对于上下文:我正在考虑一个数据库查询环境,其中类型推断必须查看数据库中字段的类型以验证它们上的操作.)
Addit:在完成@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工作正常.
请注意,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
Run Code Online (Sandbox Code Playgroud)漂亮的打印UExp和Exp是手写的,但至少使用相同的代码(它通过PrettyExp类来实现)。
该评估代码本身是美丽的,但我怀疑我需要向你推销上。:)
据我了解,GADT 非常适合 EDSL(嵌入式 DSL),因为它们只是大型 Haskell 程序中的代码部分。是的,类型错误可能很复杂(并且会直接来自 GHC),但这是您为能够在代码中维护类型级不变量而付出的代价。例如,考虑hooplCFG 中基本块的表示:
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
Run Code Online (Sandbox Code Playgroud)
当然,您会遇到令人讨厌的类型错误,但您也有能力在类型级别跟踪失败信息。这使得考虑数据流问题变得更加容易。
我想说明的一点是:如果您的 GADT 是从String(或自定义 REPL)构建的,那么您将很难执行翻译。这是不可避免的,因为您所做的实际上是重新实现一个简单的类型检查器。最好的办法是直面这个问题(也是glambda如此),并将解析与类型检查区分开来。
但是,如果您有能力保持在 Haskell 代码的范围内,您可以将解析和类型检查交给 GHC。恕我直言,EDSL 比非嵌入式 DSL 更酷、更实用。
| 归档时间: |
|
| 查看次数: |
160 次 |
| 最近记录: |