为什么 Finatra 使用 flatMap 而不仅仅是地图?
Why does Finatra use flatMap and not just map?
这可能是一个非常愚蠢的问题,但我试图理解使用 #flatMap 而不仅仅是 #map 背后的逻辑Finatra 的 HttpClient 中的此方法定义 definition:
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 而不是像这样的东西时,为什么要创建一个新的未来:
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 只是更好的风格,而 throw 几乎看起来像是副作用(实际上不是,因为它不会退出上下文Future 的)还是它背后有更多的东西,比如执行顺序等等?
Tl;博士:
在 Future 中抛出与返回 Future.exception 之间有什么区别?
以我对FP的理解,不会抛出异常。正如您所说,这将是一种副作用。异常是在程序执行的某个时刻处理的值。
Cats(我相信其他图书馆也一样)也采用了这种技术 (https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/ApplicativeError.scala)。
因此,flatMap
调用允许将异常包含在此处满足的 Future 中,并在程序执行的稍后时间处理,此时也可能发生其他异常值处理。
从理论的角度来看,如果我们去掉例外部分(无论如何都不能用范畴论来推理它们),那么这两个操作是完全相同的,只要你选择的构造(在你的情况下是 Twitter Future
) 形成一个有效的 monad。
我不想详细介绍这些概念,所以我将直接介绍这些定律(使用 Scala Future
):
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))
所以是的,正如您已经暗示的那样,这两种方法是相同的。
但是,考虑到我们将例外情况纳入其中,我对此有三点意见:
- 小心 - 在抛出异常时,Scala
Future
(也可能是 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!
- 正如@randbw 提到的,抛出异常不是 FP 的惯用做法,它违反了诸如函数纯度和值的引用透明性等原则。
Scala 和 Twitter Future
让您可以轻松地抛出异常 - 只要它发生在 Future
上下文中,异常就不会冒泡,而是导致 Future
失败。然而,这并不意味着应该允许在代码中随意使用它们,因为它会破坏程序的结构(类似于 GOTO 语句的执行方式,或循环中的 break 语句等)。
首选的做法是始终将每个代码路径评估为一个值,而不是四处乱扔炸弹,这就是为什么将 flatMap 转换为(失败的)Future
比映射到一些会抛出炸弹的代码更好.
- 记住引用透明度。
如果你使用 map
而不是 flatMap
并且有人从映射中提取代码并将其提取到一个函数中,那么如果这个函数 returns a 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
,但这种可能性不大。另一方面,简单地将响应处理逻辑提取到一个单独的函数中,完全符合函数式编程将小函数组合成大函数的思想,并且很可能会发生。
这可能是一个非常愚蠢的问题,但我试图理解使用 #flatMap 而不仅仅是 #map 背后的逻辑Finatra 的 HttpClient 中的此方法定义 definition:
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 而不是像这样的东西时,为什么要创建一个新的未来:
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 只是更好的风格,而 throw 几乎看起来像是副作用(实际上不是,因为它不会退出上下文Future 的)还是它背后有更多的东西,比如执行顺序等等?
Tl;博士: 在 Future 中抛出与返回 Future.exception 之间有什么区别?
以我对FP的理解,不会抛出异常。正如您所说,这将是一种副作用。异常是在程序执行的某个时刻处理的值。
Cats(我相信其他图书馆也一样)也采用了这种技术 (https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/ApplicativeError.scala)。
因此,flatMap
调用允许将异常包含在此处满足的 Future 中,并在程序执行的稍后时间处理,此时也可能发生其他异常值处理。
从理论的角度来看,如果我们去掉例外部分(无论如何都不能用范畴论来推理它们),那么这两个操作是完全相同的,只要你选择的构造(在你的情况下是 Twitter Future
) 形成一个有效的 monad。
我不想详细介绍这些概念,所以我将直接介绍这些定律(使用 Scala Future
):
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))
所以是的,正如您已经暗示的那样,这两种方法是相同的。
但是,考虑到我们将例外情况纳入其中,我对此有三点意见:
- 小心 - 在抛出异常时,Scala
Future
(也可能是 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!
- 正如@randbw 提到的,抛出异常不是 FP 的惯用做法,它违反了诸如函数纯度和值的引用透明性等原则。
Scala 和 Twitter Future
让您可以轻松地抛出异常 - 只要它发生在 Future
上下文中,异常就不会冒泡,而是导致 Future
失败。然而,这并不意味着应该允许在代码中随意使用它们,因为它会破坏程序的结构(类似于 GOTO 语句的执行方式,或循环中的 break 语句等)。
首选的做法是始终将每个代码路径评估为一个值,而不是四处乱扔炸弹,这就是为什么将 flatMap 转换为(失败的)Future
比映射到一些会抛出炸弹的代码更好.
- 记住引用透明度。
如果你使用 map
而不是 flatMap
并且有人从映射中提取代码并将其提取到一个函数中,那么如果这个函数 returns a 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
,但这种可能性不大。另一方面,简单地将响应处理逻辑提取到一个单独的函数中,完全符合函数式编程将小函数组合成大函数的思想,并且很可能会发生。