DSL 的 GADT:摇摆和迂回?

GADTs for a DSL: swings and roundabouts?

GADT 优势的典型示例是表示 DSL 的语法;说 here on the wiki or the PLDI 2005 paper.

我可以看出,如果你有一个通过构造类型正确的 AST,编写一个 eval 函数很容易。

如何将 GADT 处理构建到 REPL 中?或者更具体地说,进入 Read-Parse-Typecheck-Eval-Print-Loop?我发现您只是将 eval 步骤的复杂性推到了前面的步骤中。

GHCi 是否在内部使用 GADT 来表示它正在计算的表达式? (这些表达式比典型的 DSL 大得多。)

我更多地考虑输入 DSL 表达式的用户体验,而不是 eval 函数对程序员的便利性。或者:

在最后一个项目符号中,我似乎回到了 'smart constructors',GADT 应该改进(?)更重要的是我在某处加倍了工作。

我没有 'better way' 来接近它。我想知道如何在实践中处理 DSL 应用程序。 (对于上下文:我正在考虑一个数据库查询环境,其中类型推断必须查看数据库中字段的类型以验证对它们的操作。)

添加: 在完成@Alec

的回答后

我看到 glambda 中的漂亮打印代码涉及多个层 类 和实例。与 GADT 对 AST 的声称优势相反,这里感觉有些不对劲。 (类型良好的)AST 的想法是您同样可以轻松地:评估它;或漂亮地打印出来;或优化它;或从中生成代码;等等

glambda 似乎旨在评估(考虑到练习的目的,这很公平)。我想知道...

我以前没有建议过这种方法,因为它似乎涉及太多笨拙的代码。但实际上它并不比 glambda 差——也就是说,当您考虑整个功能时,而不仅仅是评估步骤。

在我看来,只表达一次语法是一个很大的优势。此外,它似乎更具可扩展性:为每个语法生成添加一个新的数据类型,而不是打破现有的数据类型。哦,因为它们是 H98 数据类型(没有存在性),deriving 工作正常。

请注意,GHCi 不使用 GADT 来表示表达式。甚至 GHC 的内部核心表达式类型 Expr 也不是 GADT。

DSL

为了获得更大更充实的 Term 类型示例,请考虑 glambda. Its Exp 类型甚至在类型级别跟踪变量。

  • 还有第二种 UExp 数据类型,正如您自己观察到的那样,它实际上是从 REPL 中解析出来的。这个类型然后被类型检查到 Exp 并传递给一个延续:

    check :: (MonadError Doc m, MonadReader Globals m)
          => UExp -> (forall t. STy t -> Exp '[] t -> m r)
          -> m r
    
  • UExpExp 的漂亮打印是手写的,但至少 uses the same code(它通过 PrettyExp class).

  • evaluation code itself 很漂亮,但我怀疑我是否需要就此向您推销。 :)

EDSL

据我所知,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 更酷、更实用。