如何剔除我的样板代码

3
我正在使用syntactic库来创建AST。要将AST评估为(Haskell)值,我所有的节点都需要成为syntactic类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

但是在EvalEnv实例中无法访问对sig的约束条件,这使得下面的(例如重叠的)实例变得不可能:

instance EvalEnv sym env where
  compileSym = compileSymDefault

我所有的用户定义的AST节点都是GADTs,通常有多个构造函数,其中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的约束。

有没有什么办法可以避免为我创建的每种节点类型列出每个构造器?


不相关的是,我不确定您是否应该将代理传递给compileSym。这并非必要,并且会使定义复杂化。 - Carl
@Carl 如果不清楚的话,compileSymDefault 是由语法库提供的。可能有一些原因在那里。 - crockeea
Additive在哪里?我在任何syntax模块中都找不到它,Hayoo也是如此。http://hayoo.fh-wedel.de/?query=syntactic+Additive - Cirdec
@Cirdec 那是使用numeric-prelude。我替换了约束条件。 - crockeea
2个回答

2
您不能完全取消您的样板文件,但可以稍微减少它。无论是废弃您的模板文件还是较新的GHC通用代码,都不能为像您的那样的GADTs派生实例。可以使用模板Haskell生成EvalEnv实例,但我不会讨论这个。
我们可以稍微减少我们编写的模板文件数量。我们遇到麻烦的想法是对于任何ADDITIVE a,都有一个Signature a实例,对于所有的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构造函数捕获的约束条件,我们必须对其进行模式匹配。然后,我们可以使用signatureDictcompileSymDefault来编写compileSym
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及其实例,捕捉到这样一个想法:对于任何ADDITIVE a都存在一个Signature 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 实例稍微容易些。

我一直在考虑泛型是否可能,但既然您说不可能,那我就相信您的话了。 - crockeea
你只需要添加一行代码 deriving instance Generic1 ADDITIVE 就可以看到 GHC.Generics 无法为 ADDITIVE 派生一个 Generic1 实例。即使你没有使用 {-# LANGUAGE StandaloneDeriving #-}{-# LANGUAGE DeriveGeneric #-}import GHC.Generics, GHC 也会给出错误信息。 - Cirdec

2
如果我理解问题正确,那么模板代码的产生是因为需要对每个构造函数进行模式匹配,以便将所需上下文引入作用域。除了构造函数名称外,所有case分支都是相同的。
以下代码使用一个removeBoilerplate rank-2函数,可用于将上下文引入作用域。首先定义两个示例函数,使用模板代码,然后转换为使用helper 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接