开发者

How to avoid extra indentation in Template Haskell declaration quotations?

开发者 https://www.devze.com 2023-04-09 13:58 出处:网络
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:

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:

  1. The Template Haskell in my Real Program is more complex than id开发者_开发问答 — it 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?
  2. 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.

0

精彩评论

暂无评论...
验证码 换一张
取 消