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