使用模板生成函数 Haskell

Generate a function using Template Haskell

是否可以使用模板 Haskell 定义函数?例如

convertStringToValue :: String -> Int
convertStringToValue "three" = 3
convertStringToValue "four" = 4

我也有一个Map [Char] Int.

fromList [("five",5),("six",6)]

如何添加功能

convertStringToValue "six" = 6
convertStringToValue "five" = 5

在编译时使用模板 Haskell 和 Map?为此目的使用模板 Haskell 似乎很愚蠢,但我还是想知道。

您可以使用两个文件执行此操作:

一个"maker"文件: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。本教程不是 "a complete introduction to...":还有其他技术可以做到这一点。

为了编写模板Haskell,您可以先尝试一些表达式,然后尝试泛化它们使用mapfold,等等

分析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_0VarE 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,因为该库对所有这些项目都有小写变体。您还可以尝试稍微“清理”代码(例如(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是makes/constructs函数的某种函数。

小心无限列表

请注意,编译器将计算美元括号 $() 之间的内容。例如,如果您要使用无限列表:

function = $(maker [(show i,i)|i<-[1..]]) -- Don't do this!

这会一直为抽象语法树分配内存,最终运行内存不足。编译器 不会 在 运行 时展开 AST。

import Language.Haskell.TH

generateDict :: String -> [(String, Int)] -> Q [Dec]
generateDict fname sns = do
    let clauses = map clause sns
    return $ [FunD (mkName fname) clauses]
        where clause (s,n) =
                Clause [LitP . IntegerL $ toInteger  n]
                       (NormalB . LitE $ StringL s )
                       []

然后

generateDict "myDict" $ zip (words "One Two Tree Four") [1..]

myDict 1 -- => "One"