Haskell是一种纯函数式语言,这意味着Haskell函数没有副作用。 I / O使用代表I / O计算块的monad实现。
是否可以测试Haskell I / O函数的返回值?
假设我们有一个简单的'hello world'计划:
main :: IO ()
main = putStr "Hello world!"
我是否可以创建一个可以运行main
的测试工具并检查I / O monad是否返回正确的'值'?或者monad被认为是不透明的计算块会阻止我这样做吗?
注意,我不是要比较I / O操作的返回值。 我想比较I / O函数的返回值 - I / O monad本身。
因为在Haskell中返回而不是执行I / O,所以我希望检查I / O函数返回的I / O计算块,看看它是否正确。我认为这可以允许I / O函数以I / O是副作用的命令式语言中的单元测试方式进行单元测试。
答案 0 :(得分:8)
我这样做的方法是创建我自己的IO monad,其中包含我想要建模的动作。我会在我的monad中运行我想要比较的monadic计算并比较它们的效果。
我们举一个例子。假设我想模拟打印的东西。然后我可以这样建模我的IO monad:
data IO a where
Return :: a -> IO a
Bind :: IO a -> (a -> IO b) -> IO b
PutChar :: Char -> IO ()
instance Monad IO where
return a = Return a
Return a >>= f = f a
Bind m k >>= f = Bind m (k >=> f)
PutChar c >>= f = Bind (PutChar c) f
putChar c = PutChar c
runIO :: IO a -> (a,String)
runIO (Return a) = (a,"")
runIO (Bind m f) = (b,s1++s2)
where (a,s1) = runIO m
(b,s2) = runIO (f a)
runIO (PutChar c) = ((),[c])
以下是我如何比较效果:
compareIO :: IO a -> IO b -> Bool
compareIO ioA ioB = outA == outB
where ioA = runIO ioA ioB
有些东西是这种模型无法处理的。例如,输入很棘手。但我希望它适合你的用例。我还要提一下,以这种方式建模效果有更聪明有效的方法。我选择了这种特殊方式,因为我认为这是最容易理解的方式。
有关更多信息,我可以推荐论文“野兽之美:尴尬小队的功能语义”,可以在this page上找到一些其他相关论文。
答案 1 :(得分:4)
在IO monad中,您可以测试IO功能的返回值。要测试IO monad之外的返回值是不安全的:这意味着它可以完成,但只会有破坏程序的风险。仅供专家使用。
值得注意的是,在您显示的示例中,main
的值具有类型IO ()
,这意味着“我是一个IO动作,执行时会执行一些I / O然后返回()
类型的值。“类型()
发音为“unit”,并且此类型只有两个值:空元组(也写为()
和发音为“unit”)和“bottom”,这是Haskell的名称不终止或出错的计算。
值得指出的是,测试 IO monad中的IO函数的返回值是非常简单和正常的,并且通过使用do
表示法来实现它的惯用方法
答案 2 :(得分:1)
您可以使用QuickCheck 2测试某些 monadic代码。自从我阅读这篇论文以来已经很长时间了,所以我不记得它是否适用于IO动作或它可以应用于哪种monadic计算。此外,您可能会发现很难将单元测试表示为QuickCheck属性。尽管如此,作为一个非常满意的QuickCheck用户,我会说它比很多更好,而不是无所事事,或者只是用unsafePerformIO
进行攻击。
答案 3 :(得分:0)
我很遗憾地告诉你,你不能这样做。
unsafePerformIO
基本上让你完成这个。但我强烈希望你不使用它。
Foreign.unsafePerformIO :: IO a -> a
:/
答案 4 :(得分:0)
我喜欢this answer关于SO的类似问题及其评论。基本上,IO通常会产生一些可能会被外界注意到的变化;您的测试需要与更改是否正确有关。 (例如,生成了正确的目录结构等)。
基本上,这意味着“行为测试”,在复杂的情况下可能会非常痛苦。这就是为什么你应该将代码的IO特定部分保持在最低限度并将尽可能多的逻辑移动到纯粹(因此超级易测试)函数的部分原因。
然后,您可以使用断言函数:
actual_assert :: String -> Bool -> IO ()
actual_assert _ True = return ()
actual_assert msg False = error $ "failed assertion: " ++ msg
faux_assert :: String -> Bool -> IO ()
faux_assert _ _ = return ()
assert = if debug_on then actual_assert else faux_assert
(您可能希望在构建脚本构建之前构建的单独模块中定义debug_on
。此外,这很可能是由Hackage上的包以更精细的形式提供的,如果不是标准库...如果有人知道这样的工具,请编辑这篇文章/评论,以便我可以编辑。)
我认为 GHC将足够聪明,可以完全跳过它发现的任何虚假断言,而实际的断言肯定会在失败时使程序崩溃。
这是IMO,不太可能 - 你仍然需要在复杂场景中进行行为测试 - 但我想这可以帮助检查代码所做的基本假设是否正确。