How to avoid extra indentation in Template Haskell declaration quotations?
I have a toy program:
$ cat a.hs
main = putStrLn "Toy example"
$ runghc a.hs
Toy example
Let's add some Template Haskell to it:
$ cat b.hs
{-# LANGUAGE TemplateHaskell #-}
id [d|
main = putStrLn "Toy example"
|]
$ runghc b.hs
b.hs:3:0: parse error (possibly incorrect indentation)
Right then, let's fix the indentation:
$ cat c.hs
{-# LANGUAGE TemplateHaskell #-}
id [d|
main = putStrLn "Toy example"
|]
$ runghc c.hs
Toy example
A single space is enough, but I do have to indent both trailing lines.
Can I avoid having to indent most of my module? (My Real Modules have much more than a single line of code.) (And without using { ; ; }
notation?)
I do want all of the module declarations to be captured in the quotation — in normal code I can replace (...)
with $ ...
, is there some equivalent of [d|...|]
that would let me avoid the close brackets and also the indenting?
Or is there some way module A can say that the top-level declarations of any module B that A is imported into are automatically processed by a function A exports?
Notes:
- The Template Haskell in my Real Program is more complex than
id开发者_开发问答
— it scans the declarations for variable names that startprop_
, and builds a test suite containing them. Is there some other pure Haskell way I could do this instead, without directly munging source files? - I'm using GHC v6.12.1. When I use GHC v7.0.3, the error for b.hs is reported for a different location —
b.hs:3:1
— but the behaviour is otherwise identical.
If the test suite is for QuickCheck, i advise you to use the new All
module instead:
http://hackage.haskell.org/packages/archive/QuickCheck/2.4.1.1/doc/html/Test-QuickCheck-All.html
It does the same thing except it fetches the names of properties by accessing the file system and parsing the file that the splice resides in (if you are using some other test framework, you can still use the same approach).
If you really want to quote the entire file, you could use a quasi-quoter instead (which does not require indentation). You can easily build your quoter on haskell-src-meta, but i advice against this approach because it will not support some Haskell features and it will probably give poor error messages.
Aggregating test suits is a difficult problem, one could probably extend the name gathering routine to somehow follow imports but it's a lot of work. Here's a workaround:
You can use this modified version of 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)) |]
You also need the function runQuickCheckAll
which is not exported from All:
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
In each test module you now define
propsN = $allProperties
where N
is some number or other unique identifier (or you could use the same name and use qualified names in the step below).
In your main test suite you define
props :: [(String,Property)]
props = concat [props1, props2 ... propsN]
If you really want to avoid adding a list member for each module, you could make a TH script that generates this list.
To run all your tests you simply say
runTests = runQuickCheckAll quickCheckResult props
[my program] scans the declarations for variable names that start prop_, and builds a test suite containing them. Is there some other pure Haskell way I could do this instead, without directly munging source files?
Yes, there is! Using 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
Running this, we get:
prop_foo
+++ OK, passed 100 tests.
prop_bar
+++ OK, passed 100 tests.
However, before you go reinventing the wheel I would also recommend you take a look at the test-framework-th
package, which does pretty much exactly this, but also supports HUnit and has a nice test runner (with colors!).
{-# 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)
Output:
Main:
bar: [OK, passed 100 tests]
foo: [OK]
Properties Test Cases Total
Passed 1 1 2
Failed 0 0 0
Total 1 1 2
There's also a testGroupGenerator
which is useful if you want to combine tests from multiple files.
精彩评论