Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  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.
like image 401
dave4420 Avatar asked Oct 01 '11 09:10

dave4420


1 Answers

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
like image 85
Jonas Duregård Avatar answered Sep 22 '22 14:09

Jonas Duregård