为什么Finatra使用flatMap而不仅仅是map



这可能是一个非常愚蠢的问题,但我正在尝试理解在Finatra的HttpClient定义中使用#flatMap而不仅仅是#映射的方法定义背后的逻辑:

def executeJson[T: Manifest](request: Request, expectedStatus: Status = Status.Ok): Future[T] = {
execute(request) flatMap { httpResponse =>
if (httpResponse.status != expectedStatus) {
Future.exception(new HttpClientException(httpResponse.status, httpResponse.contentString))
} else {
Future(parseMessageBody[T](httpResponse, mapper.reader[T]))
.transformException { e =>
new HttpClientException(httpResponse.status, s"${e.getClass.getName} - ${e.getMessage}")
}
}
}
}

当我可以使用#map并使用类似的东西时,为什么要创建一个新的Future

execute(request) map { httpResponse =>
if (httpResponse.status != expectedStatus) {
throw new HttpClientException(httpResponse.status, httpResponse.contentString)
} else {
try {
FinatraObjectMapper.parseResponseBody[T](httpResponse, mapper.reader[T])
} catch {
case e => throw new HttpClientException(httpResponse.status, s"${e.getClass.getName} - ${e.getMessage}")
}
}
}

这纯粹是风格上的差异吗?在这种情况下,使用Future.exception只是更好的风格,而投掷看起来几乎是一种副作用(事实上并不是,因为它不会退出Future的上下文(,或者它背后还有更多的东西,比如执行顺序等等?

Tl;博士:抛出Future和返回Future.exception有什么区别?

从理论的角度来看,如果我们去掉异常部分(无论如何都无法使用范畴理论来推理它们(,那么只要你选择的结构(在你的例子中是TwitterFuture(形成有效的monad,这两个操作就完全相同。

我不想详细介绍这些概念,所以我只想直接介绍这些定律(使用ScalaFuture(:

import scala.concurrent.ExecutionContext.Implicits.global
// Functor identity law
Future(42).map(x => x) == Future(42)
// Monad left-identity law
val f = (x: Int) => Future(x)
Future(42).flatMap(f) == f(42) 
// combining those two, since every Monad is also a Functor, we get:
Future(42).map(x => x) == Future(42).flatMap(x => Future(x))
// and if we now generalise identity into any function:
Future(42).map(x => x + 20) == Future(42).flatMap(x => Future(x + 20))

所以,是的,正如你已经暗示的,这两种方法是相同的。

然而,考虑到我们将例外情况纳入其中,我对此有三点意见:

  1. 小心-当涉及到抛出异常时,ScalaFuture(可能也是Twitter(故意违反了左身份法,以换取一些额外的安全

示例:

import scala.concurrent.ExecutionContext.Implicits.global
def sneakyFuture = {
throw new Exception("boom!")
Future(42)
}
val f1 = Future(42).flatMap(_ => sneakyFuture)
// Future(Failure(java.lang.Exception: boom!))
val f2 = sneakyFuture
// Exception in thread "main" java.lang.Exception: boom!
  1. 正如@randbw所提到的,抛出异常不是FP的惯用用法,它违反了函数的纯粹性和值的引用透明性等原则

Scala和TwitterFuture使您可以轻松地抛出异常——只要它发生在Future上下文中,异常就不会出现,而是导致Future失败。然而,这并不意味着应该允许在代码中随意使用它们,因为这会破坏程序的结构(类似于GOTO语句的操作方式,或循环中的中断语句等(

首选的做法是始终将每个代码路径评估为一个值,而不是到处扔炸弹,这就是为什么将map平面化为(失败的(Future比映射到一些扔炸弹的代码要好。

  1. 请记住引用透明度

如果使用map而不是flatMap,并且有人从映射中提取代码并将其提取到函数中,那么如果此函数返回Future,则会更安全,否则有人可能会在Future上下文之外运行它。

示例:

import scala.concurrent.ExecutionContext.Implicits.global
Future(42).map(x => {
// this should be done inside a Future
x + 1
})

这很好。但是,在完全有效的重构(利用引用透明性规则(之后,您的密码变成了:

def f(x: Int) =  {
// this should be done inside a Future
x + 1
}
Future(42).map(x => f(x))

如果有人直接打电话给f,你会遇到问题。将代码封装到Future中并在其上进行flatMap会安全得多

当然,你可能会争辩说,即使在使用flatMap时,也有人可以从.flatMap(x => Future(f(x))中删除f,但可能性不大。另一方面,简单地将响应处理逻辑提取到一个单独的函数中,完全符合函数编程将小函数组合成大函数的想法,而且这种情况很可能发生。

根据我对FP的理解,不会抛出异常。正如你所说,这将是一个副作用。相反,异常是在程序执行的某个时刻处理的值。

Cats(我相信其他库也是(也采用了这种技术(https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/ApplicativeError.scala)。

因此,flatMap调用允许异常包含在满足的Future中,并在程序执行的稍后点进行处理,在该点也可能发生其他异常值处理。

相关内容

最新更新