我一直在尝试掌握IO monad一段时间,这很有意义。如果我没记错的话,目标是将副作用的描述与实际执行分开。如下例所示,Scala有一种方法来获取不是参照透明的环境变量。提出了两个问题。

问题1:此参照透明吗

问题2:如何正确(基于单位/属性)进行测试?无法检查是否相等,因为它将检查内存引用,并且无法检查内部函数,因为如果我没记错的话,就无法进行函数比较。但是,我不想在单元测试中运行实际的副作用。另外,这是设计错误还是IO monad的滥用?

case class EnvironmentVariableNotFoundException(message: String) extends Exception(message)

object Env {
  def get(envKey: String): IO[Try[String]] = IO.unit.flatMap((_) => IO.pure(tryGetEnv(envKey)))

  private[this] def tryGetEnv(envKey: String): Try[String] =
    Try(System.getenv(envKey))
      .flatMap(
        (x) =>
          if (x == null) Failure(EnvironmentVariableNotFoundException(s"$envKey environment variable does not exist"))
          else Success(x)
      )
}

最佳答案

最好使用IO来包装程序中来自不纯来源的值,就像示例中的System调用一样。这将给您返回IO[A],其内容为“我能够通过不正当手段获得A”。之后,您可以使用通过Amap等作用于该flatMap的纯/参照透明函数。

这导致两个答案。我想问一下,您要测试什么性质?

查看该代码,我注意到flatMap中的tryGetEnv可能很复杂,需要进行测试。您可以通过将此逻辑提取到纯函数中来实现。您可能(例如)重写它,以便有一个函数返回一个IO[String],然后编写一个(经过测试的)函数,将其转换为所需的类型。

IO完全按照您说的去做,但这显然不包括使代码参照透明!如果要在此处测试实际的副作用,则可以考虑将System作为参数传递并对其进行模拟,以进行测试,就像在不使用IO的程序中一样。

因此,总而言之,我将考虑创建一个最小函数,该函数对System进行调用以创建IO[A](在本例中为IO[Try[String]])。您可能选择通过模拟测试此最小功能,但前提是您认为自己正在通过这样做来增加价值。围绕此,您可以编写采用A的函数,并通过将纯值传递给这些函数来对其进行测试。请记住,mapIO的签名如下,这里的f是纯(可测试)函数!

sealed abstract class IO[+A] {

  def map[B](f: A => B): IO[B]
             ^ f is a pure function!
               test it by passing A values and verifying the Bs


极端地讲,这种模式鼓励您仅在程序的边缘(例如IO函数)创建main值。然后,可以从纯函数创建程序的其余部分,这些纯函数将在程序运行时根据IO类型的值起作用。

09-05 21:37
查看更多