Haskell quickcheck для создания и тестирования розовых деревьев?

Я пробую простой код розового дерева.

data RoseT a = Leaf a | Node a [RoseT a] deriving (Show)

instance Eq (RoseT a) where
 (==) (Leaf a) (Leaf b) = a == b
 (==) (Node a rs1) (Node b rs2) = and ((a==b): (zipWith (==) rs1 rs2))
 (==) _ _ = False 

Могу ли я использовать quickcheck для проверки реализации экземпляра Eq? если да, то как? Если нет, то какая альтернатива лучше?

У меня также есть функция, которая делает следующее:

appendPath :: (RoseT a) -> (RoseT (a,[a]))
appendPath rst = appendPath' [] rst 

appendPath :: [a] -> (RoseT a) -> (RoseT (a,[a]))
appendPath' p (Leaf a) = Leaf (a,p)
appendPath' p (Node a rs) = Node (a,p) (map (appendPath' (a:p)) rs)

Функция appendPath принимает дерево роз в качестве входных данных и возвращает дерево роз с путем от узла к корню в каждом узле дерева. Эту информацию я использую в другой функции.

Как с помощью quickcheck проверить эту реализацию на розах большого размера?

РЕДАКТИРОВАТЬ: Как было предложено mhwombat, кажется невозможным написать генератор, который принимает параметр типа в качестве аргумента. Итак, вот мой параметр типа. Я хочу построить RoseT строк, представляющих действительные случайные арифметические выражения, причем сами арифметические выражения имеют следующую структуру:

data Expr = Var String | AddExpr [Expr] | MulExpr [Expr] deriving Show

Итак, есть ли способ сгенерировать случайный RoseT Expr, когда сами Expr генерируются случайным образом только с использованием быстрой проверки?

Еще раз спасибо, mhwombat, за то, что терпел мои детские шаги.


person Tem Pora    schedule 12.07.2013    source источник
comment
Что бы вы хотели протестировать? Конечно, можно использовать quickcheck для проверки свойств вашего дерева, но что вы имели в виду?   -  person Daniel Gratzer    schedule 12.07.2013
comment
ваше RoseT определение кажется неверным. Розовые деревья не должны иметь Leaf шаблон в ADT, поскольку с этим определением неоднозначно, должны ли листовые узлы записываться как Leaf 42 или Node 42 []. Это неоднозначно, потому что эти два выражения должны быть равны, но это не так.   -  person Daniel Dinnyes    schedule 18.03.2019


Ответы (1)


Если я чего-то не упускаю, ваша Eq реализация RoseT такая же, как производная реализация по умолчанию. Так что ты можешь просто сказать

data RoseT a = Leaf a | Node a [RoseT a] deriving (Show, Eq)

и забудьте про instance Eq (RoseT a) where вещи.

Следующий вопрос: удовлетворит ли это ваши потребности в тестировании. Если вы тестируете параметр типа с плавающей запятой, например RoseT Double, вам необходимо учитывать различия из-за округления. В этом случае вам нужна функция, которая сравнивает два дерева и проверяет, являются ли значения «достаточно близкими».

Однако я подозреваю, что ваша реализация RoseT никоим образом не будет зависеть от параметра типа. В этом случае вы можете просто протестировать его с помощью простого простого типа, такого как Char или Int, и использовать == для любых необходимых вам сравнений.

У вас есть две подписи типа для appendPath. Думаю, второй должен был быть appendPath':

appendPath' :: [a] -> (RoseT a) -> (RoseT (a,[a]))

Теперь о том, как это проверить. Было бы лучше, если бы вы / QuickCheck имели некоторый контроль над сложностью тестируемых деревьев. Это поможет вам, потому что в первую очередь будут проверены простейшие деревья, поэтому вы обнаружите ошибки «на раннем этапе» (то есть в более простых тестовых примерах, которые легче отлаживать). Вы можете сделать это, реализовав для своего класса генератор «определенного размера». Вот один из способов сделать это. Чем выше значение параметра «размер», тем больше уровней может иметь дерево.

type TestRoseT = RoseT Char

sizedArbTestRoseT :: Int -> Gen TestRoseT
sizedArbTestRoseT 0 = do
  c <- arbitrary
  return $ Leaf c
sizedArbTestRoseT n = do
  c <- arbitrary
  subtreeCount <- choose (0,n-1)
  subtrees <- vectorOf subtreeCount (sizedArbTestRoseT (n-1))
  return $ Node c subtrees

instance Arbitrary TestRoseT where
  arbitrary = sized sizedArbTestRoseT

prop_appendPath_does_something :: TestRoseT -> Property
prop_appendPath_does_something t = undefined -- stub

Мы можем выбрать тестовые данные, которые сгенерированы следующим образом:

λ> sample (sizedArbTestRoseT 2)
Node '\a' [Node '\RS' []]
Node '?' []
Node '\158' []
Node 'o' [Node 'E' []]
Node '\196' []
Node '4' [Node 'G' []]
Node ';' [Node 'f' []]
Node 'A' [Node '\CAN' []]
Node '!' []
Node 'q' [Node '\t' []]
Node '\'' [Node '\212' []]

Редактировать:

Для вашего типа Expr мы можем написать такой генератор:

sizedArbExpr :: Int -> Gen Expr
sizedArbExpr 0 = do
  s <- arbitrary
  return $ Var s
sizedArbExpr n = do
  es <- vectorOf 2 (sizedArbExpr (n-1))
  elements [AddExpr es, MulExpr es]

instance Arbitrary Expr where
  arbitrary = sized sizedArbExpr

Нам нужно будет изменить TestRoseT и его генератор так, чтобы сложность дерева согласовывалась с параметром size:

type TestRoseT = RoseT Expr

sizedArbTestRoseT :: Int -> Gen TestRoseT
sizedArbTestRoseT 0 = do
  c <- sizedArbExpr 0 -- changed this to keep complexity in bounds
  return $ Leaf c
sizedArbTestRoseT n = do
  c <- sizedArbExpr (n-1) -- changed this to keep complexity in bounds
  subtreeCount <- choose (0,n-1)
  subtrees <- vectorOf subtreeCount (sizedArbTestRoseT (n-1))
  return $ Node c subtrees

Тестирование этих модификаций дает что-то вроде:

λ> sample (sizedArbTestRoseT 3)
Node (MulExpr [MulExpr [Var "",Var ""],AddExpr [Var "",Var ""]]) [Node (MulExpr [Var "",Var ""]) [Node (Var "") []]]
Node (MulExpr [AddExpr [Var "",Var ""],AddExpr [Var "",Var ""]]) [Node (AddExpr [Var "",Var ""]) []]
Node (MulExpr [AddExpr [Var "\164D",Var "\151\246\FS"],MulExpr [Var ":\149j\EM",Var "h\253\255"]]) [Node (MulExpr [Var "\CAN\a\ACK",Var "\184"]) [Node (Var "t\154]\\") []],Node (MulExpr [Var "\135",Var "\f\DEL\\"]) [Node (Var "\SOH\DEL") []]]
Node (AddExpr [AddExpr [Var "",Var ""],MulExpr [Var "Kj\STXV",Var "D\141<s\187"]]) []
Node (AddExpr [MulExpr [Var "\252",Var "`"],MulExpr [Var "\167`t\196",Var ":\135\NULdr\237\167"]]) []
Node (AddExpr [MulExpr [Var "]\173\&28D\SOCom",Var "^\196\ETB2\216\&2\GS\ENQ\ENQ"],AddExpr [Var "$bB\212\SOH\146\234",Var "\DC3\213\&3\SUB\\}^\246(\200"]]) [Node (MulExpr [Var "l;\133\EM\147#\SUBN\\\t",Var "\235\151U\129m3|"]) [Node (Var "\NULb\133") []],Node (AddExpr [Var "\187\EOT\165S\207\r\"\RS",Var "4"]) []]
Node (MulExpr [MulExpr [Var "%0eK",Var "`N**k\FS6\NAK"],MulExpr [Var "'lUL\NAKRPc\ENQR",Var "j\232+`\FS@n"]]) [Node (AddExpr [Var "H\DC1C%\DC48<\t\ETX.L",Var "\235+\v\STXX,\NAK\SUBQc="]) [Node (Var "f\254oX?w\224\195~/") []]]
Node (AddExpr [AddExpr [Var "P",Var "\148G\STX\DEL*\136(\161\159\&7"],AddExpr [Var "\218\136-",Var "9?\128\159\b\b%3t}\131qe"]]) [Node (MulExpr [Var "\198\249\&4\176\193/}\DC28",Var ")Gl0ym\GS"]) [Node (Var ")\204\226qA\175") []]]
Node (MulExpr [MulExpr [Var "\t\186r.",Var "j\ENQ\183\NUL\NAK\129:rg[\170o\157g\238"],AddExpr [Var "\218F\226\248\156\225\&1",Var "vu\SOH\138+CKW\EM\167\&1n"]]) [Node (MulExpr [Var ",\241\158={o\182\"5\t\STX\ETX\DC2\218\162",Var "\197\&1"]) [Node (Var "u?a};\238") []]]
Node (MulExpr [MulExpr [Var "*",Var "R"],AddExpr [Var "\CAN8C",Var "\232V.\172ILy\162a"]]) []
Node (MulExpr [MulExpr [Var "\SI\240NF\249-\v$",Var "K`\STX\231w{"],MulExpr [Var "\DC1\255\209",Var "/\227\146\236\STX\185T3r\f"]]) [Node (MulExpr [Var "\229,\DLE\NAKwf[7P\160\DEL",Var "\134#\RS\SI0KCg\195\NAK\"\191\&6\243\193\SI"]) [Node (Var "\226\&7b8\f\EOTgF\171\GS}\189c\SUBc\ETX") []]]

Кстати, вы сказали: «Кажется, невозможно написать генератор, который принимает параметр типа в качестве аргумента». На самом деле это возможно, но я не думаю, что это то, что вам действительно нужно.

Между прочим, кажется немного необычным, что вы хотите создать дерево (RoseT), где листья содержат двоичные деревья (Expr). Другими словами, вы создаете деревья из деревьев. Конечно, я не знаю вашего приложения.

person mhwombat    schedule 12.07.2013
comment
Спасибо. да, мой RoseT impl не сильно зависит от параметра типа. Но последующее использование RoseT делает. Что-то вроде фантомных типов. Например, я хочу использовать его для обработки строк с определенными свойствами, например строк, которые являются допустимыми арифметическими выражениями. Я отредактировал вопрос, чтобы добавить эту информацию. - person Tem Pora; 13.07.2013
comment
Я столкнулся с аналогичной проблемой. Параметр типа (в дереве a) не важен, но функции, которые я хочу протестировать с помощью QuickCheck, имеют параметр типа в своей сигнатуре, и я не могу изменить их, чтобы они ссылались на какой-то тип TestRoseT. Можно ли изменить генератор так, чтобы деревья символов можно было использовать для тестирования моих функций? - person Rich Ashworth; 16.06.2015
comment
@RichAshworth В общем, когда у вас есть тип с параметром типа (например, Tree a), вам, вероятно, нужно протестировать его только с одним типом. Например, после тестирования Tree Char вы знаете, что он будет работать для Tree Int. Как и в примере выше, вы можете определить type TestTree = Tree Char, написать генератор для TestTrees и записать свойства для тестирования. Исходные функции по-прежнему будут иметь подписи типа с Tree a. TestTree будет использоваться только в вашем тестовом коде. Если это объяснение непонятно, вы можете открыть новый вопрос и указать ссылку на него. - person mhwombat; 16.06.2015
comment
Спасибо @mhwombat, думаю, это имеет смысл. Не могли бы вы привести пример реализации функции prop_appendPath_does_something? Свойства, которые я определил, относятся только к типу Property ... - person Rich Ashworth; 16.06.2015
comment
@RichAshworth Предположим, у вас есть функции addNode и containsNode, которые работают с вашими деревьями. Затем вы можете определить prop_addNode_works n t = property $ (addNode t n) 'containsNode' n, который проверяет, что после добавления узла он действительно находится в дереве. - person mhwombat; 16.06.2015