我使用syntactic库制作AST。要将AST评估为(Haskell)值,我的所有节点都需要是语法类EvalEnv
的实例:
class EvalEnv sym env where
compileSym :: proxy env -> sym sig -> DenotationM (Reader env) sig
Syntactic还提供"默认"实施:
compileSymDefault :: (Eval sym, Signature sig)
=> proxy env -> sym sig -> DenotationM (Reader env) sig
但在sig
的实例中无法访问EvalEnv
上的约束,使得以下(例如,重叠)实例无法实现:
instance EvalEnv sym env where
compileSym = compileSymDefault
我所有用户定义的AST节点都是GADT,通常有多个构造函数,a
参数总是满足compileSymDefault
的约束:
data ADDITIVE a where
Add :: (Num a) => ADDITIVE (a :-> a :-> Full a)
Sub :: (Num a) => ADDITIVE (a :-> a :-> Full a)
因此,我发现EvalEnv
的所有实例看起来像:
instance EvalEnv ADDITIVE env where
compileSym p Add = compileSymDefault p Add
compileSym p Sub = compileSymDefault p Sub
这个样板实例对于所有AST节点都是相同的,并且每个GADT构造函数都需要单独列出,因为GADT构造函数签名意味着compileSymDefault
约束。
有什么办法可以避免为每个节点类型列出每个构造函数吗?
答案 0 :(得分:2)
您不能废弃样板,但可以稍微减少它。 scrap your boilerplate和较新的GHC Generics代码都不能为您的GADT派生实例。可以使用template haskell生成EvalEnv
个实例,但我不会讨论它。
我们可以减少我们写的样板量。我们在捕获时遇到问题的想法是forall a
任何Signature a
都有ADDITIVE a
个实例。让我们做出真实的事情。
class Signature1 f where
signatureDict :: f a -> Dict (Signature a)
Dict
是捕获约束的GADT。定义它需要{-# LANGUAGE ConstraintKinds #-}
。或者,您可以从constraints包中的Data.Constraint
导入。
data Dict c where
Dict :: c => Dict c
要使用Dict
构造函数捕获的约束,我们必须对其进行模式匹配。然后,我们可以使用compileSym
和signatureDict
来撰写compileSymDefault
。
compileSymSignature1 :: (Eval sym, Signature1 sym) =>
proxy env -> sym sig -> DenotationM (Reader env) sig
compileSymSignature1 p s =
case signatureDict s of
Dict -> compileSymDefault p s
现在我们可以写出ADDITIVE
及其实例,捕捉到任何Signature a
始终存在ADDITIVE a
个实例的想法。
data ADDITIVE a where
Add :: (Num a) => ADDITIVE (a :-> a :-> Full a)
Sub :: (Num a) => ADDITIVE (a :-> a :-> Full a)
instance Eval ADDITIVE where
evalSym Add = (+)
evalSym Sub = (-)
instance Signature1 ADDITIVE where
signatureDict Add = Dict
signatureDict Sub = Dict
instance EvalEnv ADDITIVE env where
compileSym = compileSymSignature1
写出Signature1
实例并不比写出EvalEnv
实例有多大好处。我们获得的唯一好处是我们已经捕获了一个可能在其他地方有用的想法,并且Signature1
实例的编写稍微简单。
答案 1 :(得分:2)
如果我正确理解了这个问题,那么样板文件就需要对每个构造函数使用模式匹配来在范围内引入所需的上下文。除了构造函数名称之外,所有案例分支都是相同的。
下面的代码使用removeBoilerplate
rank-2函数,该函数可用于将上下文带入范围。首先使用样板代码定义两个示例函数,然后转换为使用帮助器removeBoilerplate
函数。
如果您有许多GADT,则每个GADT都需要自定义removeBoilerplate
。因此,如果您需要为每种类型多次删除样板,这种方法很有用。
我不熟悉句法,100%确定这会有效,但它看起来很有可能。您可能需要稍微调整removeBoilerplate
函数的类型。
{-# LANGUAGE GADTs , ExplicitForAll , ScopedTypeVariables ,
FlexibleContexts , RankNTypes #-}
class Class a where
-- Random function requiring the class
requiresClass1 :: Class a => a -> String
requiresClass1 _ = "One!"
-- Another one
requiresClass2 :: Class a => a -> String
requiresClass2 _ = "Two!"
-- Our GADT, in which each constructor puts Class in scope
data GADT a where
Cons1 :: Class (GADT a) => GADT a
Cons2 :: Class (GADT a) => GADT a
Cons3 :: Class (GADT a) => GADT a
-- Boring boilerplate
boilerplateExample1 :: GADT a -> String
boilerplateExample1 x@Cons1 = requiresClass1 x
boilerplateExample1 x@Cons2 = requiresClass1 x
boilerplateExample1 x@Cons3 = requiresClass1 x
-- More boilerplate
boilerplateExample2 :: GADT a -> String
boilerplateExample2 x@Cons1 = requiresClass2 x
boilerplateExample2 x@Cons2 = requiresClass2 x
boilerplateExample2 x@Cons3 = requiresClass2 x
-- Scrapping Boilerplate: let's list the constructors only here, once for all
removeBoilerplate :: GADT a -> (forall b. Class b => b -> c) -> c
removeBoilerplate x@Cons1 f = f x
removeBoilerplate x@Cons2 f = f x
removeBoilerplate x@Cons3 f = f x
-- No more boilerplate!
niceBoilerplateExample1 :: GADT a -> String
niceBoilerplateExample1 x = removeBoilerplate x requiresClass1
niceBoilerplateExample2 :: GADT a -> String
niceBoilerplateExample2 x = removeBoilerplate x requiresClass2