如何避免模板Haskell声明引用中的额外缩进?

时间:2011-10-01 09:09:31

标签: haskell template-haskell

我有一个玩具程序:

$ cat a.hs
main = putStrLn "Toy example"
$ runghc a.hs
Toy example

让我们添加一些Template Haskell:

$ cat b.hs
{-# LANGUAGE TemplateHaskell #-}
id [d|
main = putStrLn "Toy example"
|]
$ runghc b.hs

b.hs:3:0: parse error (possibly incorrect indentation)

然后,让我们修复缩进:

$ cat c.hs
{-# LANGUAGE TemplateHaskell #-}
id [d|
 main = putStrLn "Toy example"
 |]
$ runghc c.hs
Toy example

单个空格就足够了,但我必须缩进两个尾随行。

我可以避免缩进大部分模块吗? (我的真实模块只有一行代码。)(并且没有使用{ ; ; }符号?)

我确实希望所有模块声明都在引号中捕获 - 在正常代码中我可以用(...)替换$ ...,是否有一些等同于{{ 1}}这会让我避开括号和缩进吗?

或者模块 A 是否有某种方式可以说导入 A 的任何模块 B 的顶级声明都是自动的由函数 A 导出处理?

注意:

  1. 我的真实程序中的模板Haskell比[d|...|]更复杂 - 它会扫描启动id的变量名称的声明,并构建包含它们的测试套件。有没有其他纯Haskell方式我可以这样做而不直接修改源文件?
  2. 我正在使用GHC v6.12.1。当我使用GHC v7.0.3时,b.hs的错误报告在不同的位置 - prop_ - 但行为在其他方面是相同的。

2 个答案:

答案 0 :(得分:4)

如果测试套件适用于QuickCheck,我建议您使用新的All模块: http://hackage.haskell.org/packages/archive/QuickCheck/2.4.1.1/doc/html/Test-QuickCheck-All.html

它做了同样的事情,除了它通过访问文件系统并解析splice所在的文件来获取属性的名称(如果你使用的是其他测试框架,你仍然可以使用相同的方法)。

如果你真的想引用整个文件,你可以使用准引号(不需要缩进)。你可以在haskell-src-meta上轻松构建你的引用,但我建议不要使用这种方法,因为它不支持一些Haskell功能,它可能会给出错误的错误消息。


聚合测试套装是一个难题,人们可能会扩展名称收集例程以某种方式跟随导入,但这是很多工作。这是一个解决方法:

您可以使用forAllProperties的此修改版本:

import Test.QuickCheck
import Test.QuickCheck.All
import Language.Haskell.TH
import Data.Char
import Data.List
import Control.Monad

allProperties :: Q Exp -- :: [(String,Property)]
allProperties = do
  Loc { loc_filename = filename } <- location
  when (filename == "<interactive>") $ error "don't run this interactively"
  ls <- runIO (fmap lines (readFile filename))
  let prefixes = map (takeWhile (\c -> isAlphaNum c || c == '_') . dropWhile (\c -> isSpace c || c == '>')) ls
      idents = nubBy (\x y -> snd x == snd y) (filter (("prop_" `isPrefixOf`) . snd) (zip [1..] prefixes))
      quickCheckOne :: (Int, String) -> Q [Exp]
      quickCheckOne (l, x) = do
        exists <- return False `recover` (reify (mkName x) >> return True)
        if exists then sequence [ [| ($(stringE $ x ++ " on " ++ filename ++ ":" ++ show l),
                                     property $(mono (mkName x))) |] ]
         else return []
  [|$(fmap (ListE . concat) (mapM quickCheckOne idents)) |]

您还需要不从All导出的函数runQuickCheckAll

runQuickCheckAll :: [(String, Property)] -> (Property -> IO Result) -> IO Bool
runQuickCheckAll ps qc =
  fmap and . forM ps $ \(xs, p) -> do
    putStrLn $ "=== " ++ xs ++ " ==="
    r <- qc p
    return $ case r of
      Success { } -> True
      Failure { } -> False
      NoExpectedFailure { } -> False

在每个测试模块中,您现在定义

propsN = $allProperties

其中N是某个数字或其他唯一标识符(或者您可以使用相同的名称并在下面的步骤中使用限定名称)。

在主要测试套件中定义

props :: [(String,Property)]
props = concat [props1, props2 ... propsN]

如果你真的想避免为每个模块添加一个列表成员,你可以制作一个生成这个列表的TH脚本。

要运行所有测试,只需说

即可
runTests = runQuickCheckAll quickCheckResult props

答案 1 :(得分:3)

  

[我的程序]扫描启动prop_的变量名称的声明,并构建包含它们的测试套件。有没有其他纯Haskell方式我可以这样做,而不直接修改源文件?

是的,有!使用the language-haskell-extract package

{-# LANGUAGE TemplateHaskell #-}

import Language.Haskell.Extract
import Test.QuickCheck

prop_foo xs = reverse (reverse xs) == (xs :: [Int])
prop_bar = 2 + 2 == 4

properties = $(functionExtractorMap "^prop_"
    [|\name prop -> putStrLn name >> quickCheck prop|])

main = sequence_ properties

运行这个,我们得到:

prop_foo
+++ OK, passed 100 tests.
prop_bar
+++ OK, passed 100 tests.

然而,在你重新发明轮子之前,我还建议你看看the test-framework-th package,这几乎可以做到这一点,但也支持HUnit并且有一个很好的测试跑步者(带颜色!)。< / p>

{-# LANGUAGE TemplateHaskell #-}

import Test.Framework.Providers.HUnit
import Test.Framework.Providers.QuickCheck2
import Test.Framework.TH
import Test.HUnit
import Test.QuickCheck

prop_bar = 1+1 == 2
case_foo = 2+2 @?= 4

main = $(defaultMainGenerator)

输出:

Main:
  bar: [OK, passed 100 tests]
  foo: [OK]

         Properties  Test Cases  Total      
 Passed  1           1           2          
 Failed  0           0           0          
 Total   1           1           2   

如果你想组合来自多个文件的测试,还有一个testGroupGenerator非常有用。