Как избежать лишних отступов в цитатах декларации 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

Достаточно одного пробела, но мне нужно сделать отступ для обеих конечных строк.

Могу ли я избежать отступа большей части моего модуля? (Мои настоящие модули содержат гораздо больше, чем одну строку кода.) (И без использования нотации { ; ; }?)

Я хочу, чтобы все объявления модуля были захвачены в цитату — в обычном коде я могу заменить (...) на $ ..., есть ли эквивалент [d|...|], который позволил бы мне избежать закрывающих скобок, а также отступов ?

Или есть способ, которым модуль A может сказать, что объявления верхнего уровня любого модуля B, в который импортируется A, автоматически обрабатываются функция A экспортирует?

Примечания:

  1. Шаблон Haskell в моей реальной программе более сложен, чем id — он сканирует объявления на наличие имен переменных, начинающихся с prop_, и создает набор тестов, содержащий их. Есть ли какой-то другой чистый способ Haskell, который я мог бы сделать вместо этого, без прямого изменения исходных файлов?
  2. Я использую GHC v6.12.1. Когда я использую GHC v7.0.3, ошибка для b.hs сообщается для другого местоположения — b.hs:3:1 — но в остальном поведение идентично.

person dave4420    schedule 01.10.2011    source источник
comment
Я не знаю, почему GHC всегда настаивает на дополнительных отступах, но почему бы не обойти проблему и не использовать файловый кавычка? Возможно, напишите такой, который принимает полный файл модуля с заголовком module ... where и выдает свои объявления верхнего уровня.   -  person n. 1.8e9-where's-my-share m.    schedule 01.10.2011
comment
@н.м. Что такое файловый котировщик? Кроме того, помните, что вы не можете генерировать декларации импорта, декларации фиксации и некоторые другие вещи с помощью TH.   -  person Jonas Duregård    schedule 02.10.2011
comment
@Jonas Duregård: извините, я только что придумал термин. Это то, что вы получаете с quoteFile и квазикавычками. Однако у меня сложилось впечатление, что есть квази-кавычки, предоставляемые библиотекой, которые работают так же, как [d|...|], но теперь похоже, что это не так.   -  person n. 1.8e9-where's-my-share m.    schedule 02.10.2011


Ответы (2)


[моя программа] просматривает объявления имен переменных, которые начинаются с prop_, и создает набор тестов, содержащий их. Есть ли какой-то другой чистый способ Haskell, который я мог бы сделать вместо этого, без прямого изменения исходных файлов?

Да, есть! Использование language-haskell-extract пакет.

{-# 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.

Однако, прежде чем изобретать велосипед, я также рекомендую вам взглянуть на пакет test-framework-th< /a>, который делает именно это, но также поддерживает HUnit и имеет хороший инструмент для запуска тестов (с цветами!).

{-# 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, который полезен, если вы хотите объединить тесты из нескольких файлов.

person hammar    schedule 01.10.2011
comment
testGroupGenerator это именно то, что я пытался написать. Спасибо. - person dave4420; 02.10.2011

Если набор тестов предназначен для QuickCheck, я советую вам вместо этого использовать новый модуль All: http://hackage.haskell.org/packages/archive/QuickCheck/2.4.1.1/doc/html/Test-QuickCheck-All.html

Он делает то же самое, за исключением того, что извлекает имена свойств, обращаясь к файловой системе и анализируя файл, в котором находится соединение (если вы используете какую-то другую тестовую среду, вы все равно можете использовать тот же подход).

Если вы действительно хотите процитировать весь файл, вы можете вместо этого использовать квазицитирование (которое не требует отступа). Вы можете легко построить свой котировщик на 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)) |]

Вам также нужна функция runQuickCheckAll, которая не экспортируется из 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

В каждом тестовом модуле вы теперь определяете

propsN = $allProperties

где N — это число или другой уникальный идентификатор (или вы можете использовать то же имя и использовать полные имена на шаге ниже).

В своем основном наборе тестов вы определяете

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

Если вы действительно хотите избежать добавления члена списка для каждого модуля, вы можете создать скрипт TH, который генерирует этот список.

Чтобы запустить все ваши тесты, вы просто говорите

runTests = runQuickCheckAll quickCheckResult props
person Jonas Duregård    schedule 01.10.2011
comment
Я использую QuickCheck, и это почти то, что мне нужно, но я также хочу объединить наборы тестов из разных модулей в основной набор тестов. Test.QuickCheck.All не позволит мне этого сделать. (Прежде я не знал о Test.QuickCheck.All, так что спасибо, что рассказали мне об этом.) - person dave4420; 01.10.2011