我一直试图掌握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)
)
}
答案 0 :(得分:2)
最好使用IO
来包含程序中来自不纯来源的值,就像示例中的System调用一样。这将返回IO[A]
,其内容为" 我可以通过不纯的方式获取A
"。此后,您可以使用纯{/ 1}},A
,map
等行为的纯/引用透明函数。
这导致两个答案。我问,你试图测试的是什么属性?
看一下这段代码,我注意到flatMap
中的flatMap
,因为它可能很复杂,值得进行测试。您可以通过将此逻辑提取到纯函数中来实现。你可以(例如)重写这个,这样就有一个返回tryGetEnv
的函数,然后写一个(测试过的)函数,将它转换成你想要的类型。
IO正是你所说的,但这明确地不包括使代码引用透明!如果你想在这里测试实际的副作用,你可以考虑将System作为参数传递并嘲笑它进行测试,就像你在不使用IO[String]
的程序中一样。
总而言之,我考虑创建一个最小函数来调用IO
来创建System
(在本例中为IO[A]
1}})。您可以选择通过模拟来测试这个最小功能,但前提是您觉得通过这样做来增加价值。在此基础上,您可以编写带IO[Try[String]]
的函数,并通过将纯值传递给这些函数来测试它们。请注意,A
上map
的签名如下,IO
这里是纯(可测试)函数!
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
函数)。然后可以从纯函数创建程序的其余部分,这些函数在程序运行时对来自IO类型的值起作用。