您可以使用两个文件来做到这一点:
“制造商”文件:Maker.hs:
module Maker where
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
maker items = do
x <- newName "x"
lamE [varP x] (caseE (varE x) (map (\(a,b) -> match (litP $ stringL a) (normalB $ litE $ integerL b) []) items))
和主文件:Main.hs:
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
import Maker
function = $(maker [("five",5),("six",6)])
在这种情况下,function 将是 [Char] -> Int 类型,并将编译为:
\x -> case x of
"five" -> 5
"six" -> 6
就好像你会这样写:
function = \x -> case x of
"five" -> 5
"six" -> 6
你自己。显然,这不会为两三个案例带来回报,但正如您自己在问题中所写的那样,当您想要使用数千个案例或列表理解生成的项目列表时,这开始得到回报。
自己制作模板 Haskell
本节旨在简要描述如何自己编写模板 Haskell。本教程不是“对...的完整介绍”:还有其他技术可以做到这一点。
为了编写Haskell模板,你可以先尝试几个表达式,然后尝试使用map、fold等概括它们。
分析 AST 树
首先你最好看看 Haskell 是如何解析某个表达式本身的。您可以使用runQ 和括号[| ... |] 来执行此操作,... 是您要分析的表达式。比如:
$ ghci -XTemplateHaskell
GHCi, version 7.6.3: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :m Language.Haskell.TH
Prelude Language.Haskell.TH> runQ [| \x -> case x of "five" -> 5; "six" -> 6 |]
Loading package array-0.4.0.1 ... linking ... done.
Loading package deepseq-1.3.0.1 ... linking ... done.
Loading package containers-0.5.0.0 ... linking ... done.
Loading package pretty-1.1.1.0 ... linking ... done.
Loading package template-haskell ... linking ... done.
LamE [VarP x_0] (CaseE (VarE x_0) [Match (LitP (StringL "five")) (NormalB (LitE (IntegerL 5))) [],Match (LitP (StringL "six")) (NormalB (LitE (IntegerL 6))) []])
因此,AST 是:
LamE [VarP x_0] (CaseE (VarE x_0) [Match (LitP (StringL "five")) (NormalB (LitE (IntegerL 5))) [],Match (LitP (StringL "six")) (NormalB (LitE (IntegerL 6))) []])
所以现在我们从该表达式派生 抽象语法树(AST)。一个提示是使表达式足够通用。例如,在 case 块中使用多个案例,因为使用单个案例并不能告诉您应该如何在表达式中添加第二个案例。现在我们希望自己创建这样的抽象语法树。
创建变量名
第一个方面是变量,例如VarP x_0 和VarE x_0。您不能简单地复制粘贴它们。这里x_0 是一个名字。为了确保您不使用已经存在的名称,您可以使用newName。现在您可以构造以下表达式来完全复制它:
maker = do
x <- newName "x"
return $ LamE [VarP x] (CaseE (VarE x) [Match (LitP (StringL "five")) (NormalB (LitE (IntegerL 5))) [],Match (LitP (StringL "six")) (NormalB (LitE (IntegerL 6))) []])
泛化函数
显然我们对构建一个固定的抽象语法树不感兴趣,否则我们可以自己编写它。现在的重点是你引入一个或多个变量,并对这些变量进行推理。对于每个元组("five",5) 等,我们引入一个Match 语句:
Match (LitP (StringL "five")) (NormalB (LitE (IntegerL 5))) []
现在我们可以使用\(a,b) 轻松概括这一点:
\(a,b) -> Match (LitP (StringL a)) (NormalB (LitE (IntegerL b))) []
然后使用map 遍历所有项目:
map (\(a,b) -> Match (LitP (StringL a)) (NormalB (LitE (IntegerL b))) []) items
items 是我们希望为其生成案例的元组列表。现在我们完成了:
maker items = do
x <- newName "x"
return $ LamE [VarP x] (CaseE (VarE x) (map (\(a,b) -> Match (LitP (StringL a)) (NormalB (LitE (IntegerL b))) []) items))
现在您可以简单地省略 return,因为库中所有这些项目都有小写变体。您还可以尝试稍微“cleanup”代码(例如 (NormalB (LitE (IntegerL b))) 到 (NormalB $ LitE $ IntegerL b) 等);例如使用hlint。
maker items = do
x <- newName "x"
lamE [varP x] (caseE (varE x) (map (\(a,b) -> match (litP $ stringL a) (normalB $ litE $ integerL b) []) items))
这里的 maker 是某种创建/构造函数的函数。
小心无限列表
请注意,编译器将评估美元括号 $() 之间的内容。例如,如果您将使用无限列表:
function = $(maker [(show i,i)|i<-[1..]]) -- Don't do this!
这将继续为抽象语法树分配内存并最终耗尽内存。编译器不在运行时扩展 AST。