【问题标题】:Is it possible to test the return value of Haskell I/O functions?是否可以测试 Haskell I/O 函数的返回值?
【发布时间】:2009-12-20 21:23:30
【问题描述】:

Haskell 是一种纯函数式语言,这意味着 Haskell 函数没有副作用。 I/O 是使用代表 I/O 计算块的 monad 实现的。

是否可以测试 Haskell I/O 函数的返回值?

假设我们有一个简单的“hello world”程序:

main :: IO ()
main = putStr "Hello world!"

我是否可以创建一个可以运行main 的测试工具并检查它是否返回正确的“值”?或者 monad 应该是不透明的计算块这一事实是否会阻止我这样做?

注意,我并不是要比较 I/O 操作的返回值。 我想比较 I/O 函数的返回值 - I/O monad 本身。

由于在 Haskell 中 I/O 是返回而不是执行,我希望检查 I/O 函数返回的 I/O 计算块,看看它是否正确。我认为这可以允许 I/O 函数以一种在 I/O 是副作用的命令式语言中无法进行的单元测试。

【问题讨论】:

  • Monad 不一定是“不透明”的计算块。例如,List 和 Maybe monad 具有应用程序可见的行为 - IO monad 是唯一(据我所知)专门设计用于将程序逻辑与其某些行为隔离开来的。
  • Control.Monad.ST 也是相当不透明的。
  • 我不太擅长计算理论的东西。但这不等同于停机问题吗?我的意思是,您将拥有一个返回程序(IO 操作)的函数,并希望编写另一个程序来静态分析它的正确性。这是描述问题的有效方式吗?这是可判定的吗?

标签: unit-testing haskell functional-programming


【解决方案1】:

我这样做的方式是创建我自己的 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 上找到,以及其他一些相关论文。

【讨论】:

  • 我相信这是解决 OP 意图的答案。在这种情况下,“返回值”短语有点令人困惑,但我认为 ctford 的意思是返回值,意思是像 putStr 本身这样的函数的返回值。也就是说,比较 IO 操作本身,而不是结果。
  • @Dan 是的,这就是我的意思。很难清楚地谈论纯函数式 IO :)
【解决方案2】:

在 IO monad 中,您可以测试 IO 函数的返回值。在 IO monad 之外测试返回值是不安全的:这意味着可以这样做,但只有冒着破坏程序的风险。仅供专家使用。

值得注意的是,在您展示的示例中,main 的值具有IO () 类型,这意味着“我是一个 IO 操作,执行时会执行一些 I/O,然后返回值输入()。” () 读作“unit”,这种类型只有两个值:空元组(也写成(),读作“unit”)和“bottom”,这是 Haskell 对不计算的计算的名称终止或以其他方式出错。

值得指出的是,在 IO monad 内测试 IO 函数的返回值是非常简单和正常的,并且惯用的方法是使用 do 表示法。

【讨论】:

  • 我希望测试 IO 操作本身是否正确,而不是操作返回的内容(正如您正确指出的那样,在这种情况下什么都不是)。我希望区分写“Hello world!”的动作。到标准输出和一个将“Foo”写入标准输出的。我意识到我想做的事情有些不寻常和理论上。
  • 好吧,我不明白。您最好的选择可能是单子快速检查(请参阅单独的答案)。如果你尝试一下,请告诉我们你是怎么做出来的。您也可以尝试看看是否可以从 dons 那里获得一些建议,他们使用 xmonad 进行了很多有趣的测试。
【解决方案3】:

您可以使用QuickCheck 2 测试一些 monadic 代码。我已经很久没有读过这篇论文了,所以我不记得它是否适用于 IO 动作或它可以应用于哪些类型的单子计算。此外,您可能发现很难将单元测试表示为 QuickCheck 属性。尽管如此,作为 QuickCheck 的一个非常满意的用户,我会说它比什么都不做或比 unsafePerformIO 乱搞要好很多。

【讨论】:

    【解决方案4】:

    很抱歉告诉你,你不能这样做。

    unsafePerformIO 基本上让你完成这个。但我强烈希望你不要使用它。

    Foreign.unsafePerformIO :: IO a -> a
    

    :/

    【讨论】:

    • 为什么不:test = main >>= \d -> return $ _test_value_ d??
    • 所以没有办法在我的测试代码中使用 unsafePerformIO 而不是在我的生产代码中以编程方式测试 IO?
    • @ctford:我真正建议的是让您的测试代码在 IO monad 中工作。
    【解决方案5】:

    我喜欢this answer 对 SO 和 cmets 的类似问题。基本上,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,这不太可能足够——您仍然需要在复杂场景中进行行为测试——但我想它可以帮助检查代码所做的基本假设是否正确。

    【讨论】:

      猜你喜欢
      • 2019-03-24
      • 1970-01-01
      • 2016-09-04
      • 1970-01-01
      • 1970-01-01
      • 2021-06-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多