Как избежать лишних отступов в шаблонных цитатах объявлений Haskell? - PullRequest
4 голосов
/ 01 октября 2011

У меня есть игрушечная программа:

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

Давайте добавим к нему несколько шаблонов 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 export?

Примечания:

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

Ответы [ 2 ]

4 голосов
/ 01 октября 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
3 голосов
/ 01 октября 2011

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

Да, есть!Используя пакет 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 , который в значительной степени выполняет именно это, но также поддерживает 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, который полезен, если вы хотите объединить тесты из нескольких файлов.

...