单元测试 – 单元测试功能数据结构的多个实现,无需代码重复

作为功​​能数据类型分配的一部分,我们被要求在Haskell中给出不同的队列实现,其中两个在下面给出.

来自OO世界,第一反应是让他们实现一个共同的界面,使他们可以例如分享测试代码.从我们在Haskell上读到的内容来看,这转化为两种数据类型,它们是常见类型类的实例.这部分相当简单:

data SimpleQueue a = SimpleQueue [a]
data FancyQueue a = FancyQueue ([a], [a])

class Queue q where
  empty :: q a
  enqueue :: a -> q a -> q a
  dequeue :: q a -> (a, q a)

instance Queue SimpleQueue where
  empty = SimpleQueue []
  enqueue e (SimpleQueue xs) = SimpleQueue $xs ++ [e]
  dequeue (SimpleQueue (x:xs)) = (x, SimpleQueue xs)

instance Queue FancyQueue where
  empty = FancyQueue ([], [])
  enqueue e (FancyQueue (h, t)) =
    if length h > length t
    then FancyQueue (h, e:t)
    else FancyQueue (h ++ reverse (e:t), [])
  dequeue (FancyQueue ((e:h), t)) =
    if length h > length t
    then (e, FancyQueue (h, t))
    else (e, FancyQueue (h ++ reverse t, []))

经过大量的摆弄,我们得到了以下工作方式来编写测试用例(使用HUnit),使用相同的函数f测试两个实现:

f :: (Queue q, Num a) => q a -> (a, q a)
f = dequeue . enqueue 4

makeTest = let (a, _) = f (empty :: SimpleQueue Int)
               (b, _) = f (empty :: FancyQueue Int)
           in assertEqual "enqueue, then dequeue" a b

test1 = makeTest

main = runTestTT (TestCase test1)

正如代码所示,我们非常感兴趣的是让函数makeTest将测试函数作为参数,这样我们就可以使用它来生成几个测试用例,而不必复制应用整个函数的代码:

makeTest t = let (a, _) = t (empty :: SimpleQueue Int)
                 (b, _) = t (empty :: FancyQueue Int)
             in assertEqual "enqueue, then dequeue" a b

test1 = makeTest f

main = runTestTT (TestCase test1)

但是,这无法编译错误

queue.hs:52:30:
    Couldn't match expected type `FancyQueue Int'
                with actual type `SimpleQueue Int'
    In the first argument of `t', namely `(empty :: SimpleQueue Int)'
    In the expression: t (empty :: SimpleQueue Int)
    In a pattern binding: (a, _) = t (empty :: SimpleQueue Int)

我们的问题是,是否有某种方法可以使这项工作:是否可以编写一个函数来生成我们的单元测试;一个接受一个函数并将它应用于这两个实现的方式,以避免重复应用该函数的代码?此外,对上述错误的解释将非常受欢迎.

编辑

根据以下答案,以下是我们最终得到的结果:

{-# LANGUAGE RankNTypes #-}

import Test.HUnit

import Queue
import SimpleQueue
import FancyQueue

makeTest :: String -> (forall q a. (Num a, Queue q) => q a -> (a, q a)) -> Assertion
makeTest msg t = let (a, _) = t (empty :: SimpleQueue Int)
                     (b, _) = t (empty :: FancyQueue Int)
                 in assertEqual msg a b

test1 = makeTest "enqueue, then dequeue" $dequeue . enqueue 4
test2 = makeTest "enqueue twice, then dequeue" $dequeue . enqueue 9 . enqueue 4
test3 = makeTest "enqueue twice, then dequeue twice" $dequeue . snd . dequeue . enqueue 9 . enqueue 4

tests = TestList $map (\ test -> TestCase test) [test1, test2, test3]

main = runTestTT tests

我想知道makeTest上的类型注释是否是正确的写入方式?我试着摆弄它,但这是我唯一可以开始工作的东西.只是我认为那部分(Num a,Queue q)=>应始终在类型本身之前.但也许那只是一个惯例?或者对于更高等级的类型,它们是不同的?无论如何,是否有可能以这种方式编写类型?

此外,这不重要,但出于好奇;是否使用此扩展影响性能(显着)?

最佳答案
是的,您需要一个名为Rank2Types的语言扩展.它允许这样的功能

makeTest :: (forall q a. (Num a, Queue q) => q a -> (a, q a)) -> Assertion
makeTest t = let (a, _) = t (empty :: SimpleQueue Int)
                 (b, _) = t (empty :: FancyQueue Int)
             in assertEqual "enqueue, then dequeue" a b

现在你要确保你收到的函数是多态的,这样你就可以将它应用于SimpleQueue和FancyQueue.

否则,Haskell将使用SimpleQueue统一t的第一个参数,然后当你尝试在FancyQueue上使用它时会生气.换句话说,默认情况下,Haskell使函数参数变为单态.为了使它们具有多态性,尽管你必须使用显式签名,但Haskell不会推断它.

要使用此扩展程序,您需要启用它

{-# LANGUAGE RankNTypes #-}

在您的文件的顶部.有关此扩展程序的功能及其工作方式的详细说明,请参阅here.

对编辑的回应

这就是应该正确键入的方式. Haskell含蓄地转向

foo :: Show a => a -> b -> c

foo :: forall a b c. Show a => a -> b -> c

随着更高级别的类型,你将forall移动到lambda中,约束随之移动.您不能将约束一直放到左边,因为相关的类型变量甚至不在范围内.

转载注明原文:单元测试 – 单元测试功能数据结构的多个实现,无需代码重复 - 代码日志