Haskell中的类型化抽象语法与DSL设计

Haskell中的类型化抽象语法与DSL设计,haskell,dsl,language-design,dependent-type,Haskell,Dsl,Language Design,Dependent Type,我正在用Haskell设计一个DSL,我想做一个赋值操作。类似这样的代码(下面的代码只是为了在有限的上下文中解释我的问题,我没有检查类型Stmt type): 在前面的代码中,我能够使用GADT对表达式强制执行类型约束。我的问题是如何强制赋值的左侧:1)定义,即在使用变量之前必须声明变量,2)右侧必须具有相同类型的左侧变量 我知道,在完全依赖类型的语言中,我可以定义由某种类型上下文索引的语句,即定义的变量及其类型的列表。我相信这会解决我的问题。但是,我想知道是否有一些方法可以在Haskell实现

我正在用Haskell设计一个DSL,我想做一个赋值操作。类似这样的代码(下面的代码只是为了在有限的上下文中解释我的问题,我没有检查类型Stmt type):

在前面的代码中,我能够使用GADT对表达式强制执行类型约束。我的问题是如何强制赋值的左侧:1)定义,即在使用变量之前必须声明变量,2)右侧必须具有相同类型的左侧变量

我知道,在完全依赖类型的语言中,我可以定义由某种类型上下文索引的语句,即定义的变量及其类型的列表。我相信这会解决我的问题。但是,我想知道是否有一些方法可以在Haskell实现这一点


非常感谢任何指向示例代码或文章的指针。

您应该知道您的目标非常崇高。我认为将变量完全视为字符串不会有太大进展。我会做一些更烦人的事,但更实用。为您的DSL定义monad,我称之为
M

newtype M a = ...

data Exp a where
    ... as before ...

data Var a  -- a typed variable

assign :: Var a -> Exp a -> M ()
declare :: String -> a -> M (Var a)
我不知道为什么你有
expa
作为作业,而只有
a
作为声明,但我在这里重复了这一点。
declare
中的
字符串
仅用于化妆品,如果您需要它来生成代码或报告错误或做其他事情,那么变量的标识实际上不应该与该名称绑定。所以它通常被用作

myFunc = do
    foobar <- declare "foobar" 42
对于
M
,我们需要唯一的名称和要输出的语句列表

newtype M a = M { runM :: WriterT [Stmt] (StateT Integer Identity a) }
    deriving (Functor, Applicative, Monad)
然后,这些操作通常相当琐碎

assign v a = M $ tell [Assign v a]

declare name a = M $ do
    ident <- lift get
    lift . put $! ident + 1
    let var = Var name ident
    tell [Declare var a]
    return var

foo = declare "foo" $ do
    -- actual function body

然后,您的
M
可以拥有从名称到变量的缓存作为其状态的一个组成部分,第一次使用具有特定名称的声明时,您将其呈现并放入变量中(这将需要比
[Stmt]
更复杂的monoid作为
编写器的目标)。以后您只需查找变量。不幸的是,它对名称的唯一性有着相当松散的依赖;一个显式的名称空间模型可以帮助解决这个问题,但永远不能完全消除它。

在看过@Cactus的所有代码和@luqui的Haskell建议后,我设法找到了一个接近我在Idris中想要的解决方案。完整代码的要点如下:

()

在上一个解决方案中,我需要解决一些小问题:

  • 我还不知道IDRI是否支持整数文本重载,什么对构建我的DSL非常有用
  • 我曾尝试在DSL语法中为程序变量定义一个前缀运算符,但它没有按我喜欢的方式工作。我有一个解决方案(在前面的要点中),它使用关键字--use--进行变量访问

  • 我会和Idris#freenode频道的人一起检查这两点,看看这两点是否可行

    鉴于我的工作重点是在类型级别编码的范围和类型安全相关问题,我在谷歌搜索时偶然发现了这个古老的问题,并认为我应该尝试一下

    我认为,这篇文章提供了一个非常接近原始规范的答案。一旦你有了正确的设置,整个过程就出人意料地短了

    首先,我将从一个示例程序开始,让您了解最终结果:

    program :: Program
    program = Program
      $  Declare (Var :: Name "foo") (Of :: Type Int)
      :> Assign  (The (Var :: Name "foo")) (EInt 1)
      :> Declare (Var :: Name "bar") (Of :: Type Bool)
      :> increment (The (Var :: Name "foo"))
      :> Assign  (The (Var :: Name "bar")) (ENot $ EBool True)
      :> Done
    
    范围界定 为了确保我们只能给以前声明过的变量赋值,我们需要一个范围的概念

    GHC.TypeLits
    为我们提供了类型级别的字符串(称为
    Symbol
    ),因此如果需要,我们可以很好地使用字符串作为变量名。而且,因为我们希望确保类型安全,所以每个变量声明都附带一个类型注释,我们将把它与变量名一起存储。因此,我们的作用域类型是:
    [(符号,*)]

    我们可以使用类型族来测试给定的
    符号
    是否在范围内,如果是这种情况,则返回其关联的类型:

    type family HasSymbol (g :: [(Symbol,*)]) (s :: Symbol) :: Maybe * where
      HasSymbol '[]            s = 'Nothing
      HasSymbol ('(s, a) ': g) s = 'Just a
      HasSymbol ('(t, a) ': g) s = HasSymbol g s
    
    根据这个定义,我们可以定义变量的概念:范围
    g
    a
    类型的变量是一个符号
    s
    ,因此
    HasSymbol g s
    返回的
    '只是一个
    。这就是
    ScopedSymbol
    数据类型通过使用存在量化来存储
    s
    来表示的内容

    data ScopedSymbol (g :: [(Symbol,*)]) (a :: *) = forall s.
      (HasSymbol g s ~ 'Just a) => The (Name s)
    
    data Name (s :: Symbol) = Var
    
    在这里,我故意到处滥用符号:
    the
    是类型
    ScopedSymbol
    的构造函数,
    Name
    是一个具有更好名称和构造函数的类型。这使我们能够写出以下细节:

    example :: ScopedSymbol ('("foo", Int) ': '("bar", Bool) ': '[]) Bool
    example = The (Var :: Name "bar")
    
    声明 既然我们有了范围的概念以及该范围内类型良好的变量,我们就可以开始考虑
    语句
    s应该具有的效果了。考虑到新变量可以在
    语句中声明,我们需要找到一种在范围中传播此信息的方法。关键的后见之明是有两个指标:输入范围和输出范围

    若要
    声明
    新变量及其类型,将使用变量名和相应类型对扩展当前范围

    data Statement (g :: [(Symbol, *)]) (h :: [(Symbol,*)]) where
      Declare :: Name s -> Type a -> Statement g ('(s, a) ': g)
      Assign  :: ScopedSymbol g a -> Exp g a -> Statement g g
    
    data Type (a :: *) = Of
    
    分配
    另一方面,不修改范围。它们仅将
    范围符号
    与相应类型的表达式相关联

    data Statement (g :: [(Symbol, *)]) (h :: [(Symbol,*)]) where
      Declare :: Name s -> Type a -> Statement g ('(s, a) ': g)
      Assign  :: ScopedSymbol g a -> Exp g a -> Statement g g
    
    data Type (a :: *) = Of
    
    我们再次引入了一种代理类型,以具有更好的用户级语法

    example' :: Statement '[] ('("foo", Int) ': '[])
    example' = Declare (Var :: Name "foo") (Of :: Type Int)
    
    example'' :: Statement ('("foo", Int) ': '[]) ('("foo", Int) ': '[])
    example'' = Assign (The (Var :: Name "foo")) (EInt 1)
    
    语句
    s可以通过定义以下类型对齐序列的GADT以范围保留的方式链接:

    infixr 5 :>
    data Statements (g :: [(Symbol, *)]) (h :: [(Symbol,*)]) where
      Done :: Statements g g
      (:>) :: Statement g h -> Statements h i -> Statements g i
    
    表达 表达式与原始定义基本相同,只是它们现在的作用域已确定,并且新的构造函数
    EVar
    允许我们取消引用以前声明的变量(使用
    ScopedSymbol
    ),从而提供适当类型的表达式

    data Exp (g :: [(Symbol,*)]) (t :: *) where
      EVar    :: ScopedSymbol g a -> Exp g a
      EBool   :: Bool -> Exp g Bool
      EInt    :: Int  -> Exp g Int
      EAdd    :: Exp g Int -> Exp g Int -> Exp g Int
      ENot    :: Exp g Bool -> Exp g Bool
    
    程序 一个
    程序
    就是一系列语句
    infixr 5 :>
    data Statements (g :: [(Symbol, *)]) (h :: [(Symbol,*)]) where
      Done :: Statements g g
      (:>) :: Statement g h -> Statements h i -> Statements g i
    
    data Exp (g :: [(Symbol,*)]) (t :: *) where
      EVar    :: ScopedSymbol g a -> Exp g a
      EBool   :: Bool -> Exp g Bool
      EInt    :: Int  -> Exp g Int
      EAdd    :: Exp g Int -> Exp g Int -> Exp g Int
      ENot    :: Exp g Bool -> Exp g Bool
    
    data Program = forall h. Program (Statements '[] h)
    
    increment :: ScopedSymbol g Int -> Statement g g
    increment v = Assign v (EAdd (EVar v) (EInt 1))