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

标签 scala functional-programming flatmap finatra twitter-util

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

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 只是更好的风格,而 throw 几乎看起来像是一种副作用(实际上它不是,因为它不会退出 Future 的上下文)或者它背后还有更多的东西,比如执行顺序等等?

我;博士: 在 Future 中抛出异常与返回 Future.exception 之间有什么区别?

最佳答案

从理论的角度来看,如果我们去掉异常部分(无论如何都不能用范畴论来推理它们),那么只要您选择的构造(在您的情况下是 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))

是的,正如您已经暗示的那样,这两种方法是相同的。

但是,考虑到我们将异常(exception)情况纳入其中,我对此有三点意见:

  1. 小心 - 当涉及到抛出异常时,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!
  1. 正如@randbw 提到的,抛出异常不是 FP 的惯用做法,它违反了诸如函数纯度和值的引用透明性等原则。

Scala 和 Twitter Future 让您可以很容易地抛出异常 - 只要它发生在 Future 上下文中,异常就不会冒泡,而是导致 Future 失败。但是,这并不意味着应该允许在代码中随意使用它们,因为它会破坏程序的结构(类似于 GOTO 语句的执行方式,或循环中的 break 语句等)。

首选的做法是始终将每个代码路径评估为一个值,而不是四处 throw 炸弹,这就是为什么将 flatMap 放入(失败的)Future 比映射到一些 throw 炸弹的代码更好。

  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 时,也有人可以从 f 中删除 .flatMap(x => Future(f(x)),但这种可能性不大。另一方面,简单地将响应处理逻辑提取到一个单独的函数中,完全符合函数式编程将小函数组合成大函数的思想,而且很可能会发生这种情况。

关于scala - 为什么 Finatra 使用 flatMap 而不仅仅是 map ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65000181/

相关文章:

haskell - 如何在 State monad 中包装链式有状态计算?

function - 这个函数是如何工作的 : const const (negate 1) (negate 2) 3

Java - 将带有列表变量的对象转换为对象列表

在 Flowable 中使用方法引用时,Kotlin 无法推断类型

java - Main(args) 包含多个数组

scala - 如何在 Scala 中堆叠应用仿函数

scala - 在 Scala 中输入 Lambda : why is the extra parentheses needed in a declaration?

scala - 流式源的查询必须使用 writeStream.start() 执行;

javascript - Javascript 中的函数式循环与非函数式循环

python - 相当于Python中的pySpark flatMap