模板 Haskell:生成记录

Template Haskell: Generate Records

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

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

A中的大写MyRecordA,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: error: parse error on input ‘$fieldFoo’

这里有几个问题;让我们一一看看。您拥有的接头:

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

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


定义

fieldFoo = sigP name ($clsString)

不起作用,因为如果没有中间引号,您可能无法拼接局部变量。这被称为 'stage restriction'.


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 的解决方案。您希望生成的类型族可以用第一种 class 方式表示:

import GHC.TypeLits

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

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