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

时间:2013-12-16 21:30:48

标签: unit-testing haskell types functional-programming higher-rank-types

作为功能数据类型赋值的一部分,我们被要求在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) =>应始终位于类型本身之前。但也许那只是一个惯例?或者对于更高等级的类型,它们是不同的?无论如何,是否有可能以这种方式编写类型?

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

2 个答案:

答案 0 :(得分:7)

是的,您需要一个名为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

现在,您确保所收到的功能具有多态性,因此您可以将其应用于SimpleQueueFancyQueue

否则,Haskell将统一tSimpleQueue的第一个参数,然后当你尝试在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中,约束随之移动。您不能将约束一直放到左边,因为相关的类型变量甚至不在范围内。

答案 1 :(得分:3)

您正尝试在两种不同类型中使用函数参数(即t)。首先是SimpleQueue Int -> (Int, SimpleQueue Int)类型,然后是FancyQueue Int -> (Int, FancyQueue Int)。那就是你真的希望t的类型是多态的。

但是在Haskell中默认情况下,函数参数是单态的。它们可能只用于单一类型。该类型本身可能是类型变量,例如a,但在您选择了a之后的单个实例中,这就是它始终具有的类型。

解决方案是使用RankNTypes语言扩展名,并为makeTest提供排名-2类型:

{-# LANGUAGE RankNTypes #-}
module QueueTests where
...

makeTest :: (forall a q. (Num a, Queue q) => q a -> (a, q a)) -> Assertion
makeTest =  -- same as before