猫效应和 IO monad
Cats-effect and the IO monad
一段时间以来,我一直在尝试掌握 IO monad,这很有意义。如果我没记错的话,目标是将副作用的描述和实际执行分开。如下例所示,Scala 有一种获取非引用透明环境变量的方法。出现了两个问题。
问题一:这个是引用透明的吗
问题 2:如何正确 (unit/property-based) 测试这个?不可能检查相等性,因为它会检查内存引用,并且不可能检查内部函数,因为如果我没记错的话,函数比较是不可能的。但是,我不想 运行 单元测试中的实际副作用。另外,这是设计错误还是对 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
来包装程序中来自不纯来源的值,就像示例中的系统调用一样。这将返回一个 IO[A]
,其内容为“我能够通过不纯的方式获得一个 A
”。此后,您可以使用通过 map
、flatMap
等作用于 A
的纯/引用透明函数
这导致两个答案。我想问一下,您要测试的 属性 是什么?
查看该代码,我注意到 tryGetEnv
中的 flatMap
可能很复杂,需要进行测试。您可以通过将此逻辑提取到一个纯函数中来做到这一点。您可能(例如)重写它,所以有一个 returns 和 IO[String]
的函数,然后编写一个(测试的)函数将其转换为您想要的类型。
IO 完全按照你说的做,但这显然不包括使代码引用透明!如果您想在这里测试实际的副作用,您可以考虑将 System 作为参数传递并模拟它进行测试,就像在不使用 IO
.
的程序中一样
所以总而言之,我会考虑创建一个 minimal 函数来调用 System
来创建一个 IO[A]
(在这种情况下,一个 IO[Try[String]]
)。您可以选择通过模拟来测试这个最小功能,但前提是您觉得这样做会增加价值。围绕这一点,您可以编写接受 A
的函数,并通过将纯值传递给这些函数来测试这些函数。请记住,IO
上 map
的签名如下,这里的 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 类型的值。
一段时间以来,我一直在尝试掌握 IO monad,这很有意义。如果我没记错的话,目标是将副作用的描述和实际执行分开。如下例所示,Scala 有一种获取非引用透明环境变量的方法。出现了两个问题。
问题一:这个是引用透明的吗
问题 2:如何正确 (unit/property-based) 测试这个?不可能检查相等性,因为它会检查内存引用,并且不可能检查内部函数,因为如果我没记错的话,函数比较是不可能的。但是,我不想 运行 单元测试中的实际副作用。另外,这是设计错误还是对 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
来包装程序中来自不纯来源的值,就像示例中的系统调用一样。这将返回一个 IO[A]
,其内容为“我能够通过不纯的方式获得一个 A
”。此后,您可以使用通过 map
、flatMap
等作用于 A
的纯/引用透明函数
这导致两个答案。我想问一下,您要测试的 属性 是什么?
查看该代码,我注意到 tryGetEnv
中的 flatMap
可能很复杂,需要进行测试。您可以通过将此逻辑提取到一个纯函数中来做到这一点。您可能(例如)重写它,所以有一个 returns 和 IO[String]
的函数,然后编写一个(测试的)函数将其转换为您想要的类型。
IO 完全按照你说的做,但这显然不包括使代码引用透明!如果您想在这里测试实际的副作用,您可以考虑将 System 作为参数传递并模拟它进行测试,就像在不使用 IO
.
所以总而言之,我会考虑创建一个 minimal 函数来调用 System
来创建一个 IO[A]
(在这种情况下,一个 IO[Try[String]]
)。您可以选择通过模拟来测试这个最小功能,但前提是您觉得这样做会增加价值。围绕这一点,您可以编写接受 A
的函数,并通过将纯值传递给这些函数来测试这些函数。请记住,IO
上 map
的签名如下,这里的 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 类型的值。