模板Haskell:生成记录

时间:2017-12-26 16:57:38

标签: haskell template-haskell

使用Template Haskell我想生成记录,例如:

data MyRecordA = MyRecordA
  {fooA :: String, barA :: Bool} 

MyRecordA 中的大写 A fooA barA 以及类型 Bool 应该是可变的,并由TH函数的调用者指定。

我尝试了几种变体:

{-# LANGUAGE TemplateHaskell #-}
module THRecord where
import Language.Haskell.TH

mkRecord :: Name -> Name -> Q [Dec] 
mkRecord name cls = [d|
  data $typeName :: $constName 
    {$fieldFoo, $fieldBar}
  |]
  where
    typeName = conT  $ "MyRecord" <> name
    constrName = RecC $ "MyRecord" <> name
    fieldFoo = sigP name ($clsString)
    fieldBar = sigP name cls
    clsString = conT "String" 

不幸的是,我得到了像

这样的解析错误
  

src / THRecord.hs:8:9:错误:输入'$ fieldFoo'上的解析错误

1 个答案:

答案 0 :(得分:2)

这里有几个问题;让我们逐一看看它们。你有拼接:

[d|
  data $typeName :: $constName 
    {$fieldFoo, $fieldBar}
  |]

根本无效;您只能拼接整个表达式,类型或声明,而不能拼接其中的一部分。您也可能意味着data $typeName = $constName,但当然同样的限制也适用于此,因此它仍然无法工作。

定义

fieldFoo = sigP name ($clsString)

不起作用,因为如果没有介入报价,您可能没有局部变量的拼接。这被称为“阶段限制”。

fieldFoo = sigP name ($clsString)
fieldBar = sigP name cls

sigP是错误的,因为它构造了一个模式;你不需要建立任何模式(不确定你的意思)。

typeName = conT  $ "MyRecord" <> name
constrName = RecC $ "MyRecord" <> name
clsString = conT "String" 

所有这些都试图将Name视为String。如果不清楚为什么这没有意义,也许你应该熟悉Haskell的基础知识。

现在的解决方案:

import Data.Monoid
import Language.Haskell.TH
import Language.Haskell.TH.Syntax

defBang = Bang NoSourceUnpackedness NoSourceStrictness
stringType = ConT ''String

mkRecord :: Name -> Name -> Q [Dec] 
mkRecord name cls = (pure.pure)$
  DataD [] typeName [] Nothing [constr] []
  where
    typeName = mkName $ "MyRecord" <> nameBase name
    constr = RecC typeName [(mkName $ "foo" <> nameBase name, defBang, stringType)
                           ,(mkName $ "bar" <> nameBase name, defBang, ConT cls)]

请注意,您甚至不能在这里使用Q monad;不要生成名称,也不要重新确定有关名称的信息。因此,您实际上可以编写函数Name -> Name -> Dec,然后将pure.pure应用于结果会生成一个可以拼接的类型。

上述内容适用于GHC 8.0.1;模板Haskell的AST在主要版本之间存在很大差异,因此它可能无法像其他版本那样完全编译。

然后,例如

$(mkRecord (mkName "XYZ") ''Bool)
$(mkRecord (mkName "A") ''Int)

生成

data MyRecordXYZ = MyRecordXYZ {fooXYZ :: String, barXYZ :: Bool}
data MyRecordA = MyRecordA {fooA :: String, barA :: Int}

最后,这是一个不需要TH的解决方案。您希望生成的类型系列可以用第一类方式表示:

import GHC.TypeLits

data MyRecord (nm :: Symbol) t = MyRecord { foo :: String, bar :: t }

type MyRecordA = MyRecord "A" Bool
type MyRecordXYZ = MyRecord "XYZ" Int