DSL的GADT:摆动和回旋处?

时间:2019-02-28 06:19:10

标签: haskell gadt

有关GADT好处的典型示例表示DSL的语法;说here on the wikithe 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函数的便利。要么:

  • 用户直接使用GADT构造函数输入表达式。容易在语法上犯错但类型错误(例如,错误放置的括号)。然后,GHCi给出了非常不友好的拒绝消息。或者
  • REPL将输入作为文本进行解析。但是对于这样的GADT,获得一个Read实例是一项艰巨的工作。所以也许
  • 该应用程序具有两种数据结构:一种是允许类型错误的数据结构;另一种是允许类型错误的数据结构。另一个是GADT;如果可以安全地进行类型验证,则validate步骤将构造GADT AST。

在最后一个项目符号上,我似乎还支持“智能构造函数”,即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可以正常工作。

1 个答案:

答案 0 :(得分:4)

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

DSLs

为使您的Term类型的实例更加充实,请考虑使用glambda。其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类实现)。

  • p>
  • 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更酷,更实用。