【问题标题】:How can Haskell quasiquotation be used for replacing tokens on the Haskell level?Haskell quasiquotation 如何用于替换 Haskell 级别的令牌?
【发布时间】:2012-08-18 00:26:51
【问题描述】:

haskellwiki 中描述的准引用主要被显示为在 Haskell 中嵌入其他语言而不会乱用字符串引用的有用工具。

问题是:对于 Haskell 本身而言,将现有的 Haskell 代码放入一个 quasiquoter 以仅替换标记并将结果传递给 ghc 有多容易?也许模板 Haskell 是这里的关键?

我查找了代码示例,但没有找到。一些 EDSL 可以通过减小其组合运算符的大小来受益于这种能力(例如,将 'a .|. b .>>. c' 转换为 '[myedsl|a | b >> c]')。

【问题讨论】:

  • 您可能想查看haskell-src-exts,它会解析 Haskell 并为您提供 AST,然后您可以使用它。
  • Template Haskell 本质上只不过是 Haskell 的准引用器。

标签: haskell metaprogramming dsl


【解决方案1】:

您可以构建操作 Haskell 代码的准引用器,例如,使用 haskell-src-meta 包。它将有效的 Haskell 代码解析为 AST,然后您可以对其进行修改。

在这种情况下,修改 AST 的最简单方法是使用 Data.Generics 对整个 AST 应用通用转换,将运算符替换为其他运算符。

我们将从为通用 Haskell 表达式构建转换函数开始。表示表达式的数据类型是template-haskell 包中的Exp

例如,要将运算符 >> 转换为 .>>.,我们会使用类似的函数

import Language.Haskell.TH (Exp(..), mkName)

replaceOp :: Exp -> Exp
replaceOp (VarE n) | n == mkName ">>" = VarE (mkName ".>>.")
replaceOp e = e

这会更改变量表达式 (VarE),但不能对任何其他类型的表达式执行任何操作。

现在,要遍历整个 AST 并替换所有出现的 >>,我们将使用来自 Data.Generic 的函数 everywheremkT

import Data.Generics (everywhere, mkT)

replaceEveryOp :: Exp -> Exp
replaceEveryOp = everywhere (mkT replaceOp) 

为了进行多次替换,我们可以改变函数,让它接受任何操作符的关联列表来替换。

type Replacements = [(String, String)]

replaceOps :: Replacements -> Exp -> Exp
replaceOps reps = everywhere (mkT f) where
    f e@(VarE n) = case rep of
        Just n' -> VarE (mkName n')
        _ -> e
        where rep = lookup (show n) reps
    f e = e

顺便说一句,这是一个很好的函数示例,使用 view patterns 语言扩展编写起来会更好。

{-# LANGUAGE ViewPatterns #-}

replaceOps :: Replacements -> Exp -> Exp
replaceOps reps = everywhere (mkT f) where
    f (VarE (replace -> Just n')) = VarE (mkName n')
    f e = e

    replace n = lookup (show n) reps

现在我们要做的就是构建“myedsl”准报价器。

{-# LANGUAGE ViewPatterns #-}

import Data.Generics (everywhere, mkT)
import Language.Haskell.Meta.Parse (parseExp)
import Language.Haskell.TH (Exp(..), mkName, ExpQ)
import Language.Haskell.TH.Quote (QuasiQuoter(..))

type Replacements = [(String, String)]

replacements :: Replacements
replacements =
    [ ("||", ".|.")
    , (">>", ".>>.")
    ]

myedls = QuasiQuoter
    { quoteExp  = replaceOpsQ
    , quotePat  = undefined
    , quoteType = undefined
    , quoteDec  = undefined
    }

replaceOpsQ :: String -> ExpQ
replaceOpsQ s = case parseExp s of
    Right e -> return $ replaceOps replacements e
    Left err -> fail err

replaceOps :: Replacements -> Exp -> Exp
replaceOps reps = everywhere (mkT f) where
    f (VarE (replace -> Just n')) = VarE (mkName n')
    f e = e

    replace n = lookup (show n) reps

如果您将上述内容保存到其自己的模块中(例如MyEDSL.hs),则可以将其导入并使用准引号。

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}

import MyEDSL

foo = [myedsl| a || b >> c |]

请注意,我使用 || 而不是 |,因为后者在 Haskell 中不是有效的运算符(因为它是用于模式保护的句法元素)。

【讨论】:

  • 那里everywhere的使用真的很巧妙,我删除了我的答案,因为我没有使用那个功能。
  • 但是,如果转换后的函数是合格的(例如foo = [myedsl| a MyEDSL.|| b MyEDSL.>> c |]),您的代码将会失败,并且即使您通过后缀替换运算符名称,如果存在运算符MyEDSL.|.|.||,事情也会变得模棱两可将替换为MyEDSL.|.|..||.。对于通用解决方案,应使用nameBasenameModule 而不是show。有关示例,请参阅 my attempt
  • 不错!顺便说一句,如果替换运算符的中缀值与原始运算符的中缀值不同怎么办? parseExp 的结果是否已经考虑了中缀?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-21
相关资源
最近更新 更多