如何废弃我的样板

时间:2014-09-29 22:32:42

标签: haskell boilerplate

我使用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约束。

有什么办法可以避免为每个节点类型列出每个构造函数吗?

2 个答案:

答案 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构造函数捕获的约束,我们必须对其进行模式匹配。然后,我们可以使用compileSymsignatureDict来撰写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