【问题标题】:scala: improve readability and style of this piece of codescala:提高这段代码的可读性和风格
【发布时间】:2012-09-25 15:17:30
【问题描述】:

下面是一个很常见的play framework 2控制器:

def save(ideaId : Long) = CORSAction { request =>
  Idea.findById(ideaId).map { idea =>
    request.body.asJson.map { json =>
      json.asOpt[Comment].map { comment =>
        comment.copy(idea = idea).save.fold(
          errors => JsonBadRequest(errors),
          comment => Ok(toJson(comment).toString)
        )
      }.getOrElse     (JsonBadRequest("Invalid Comment entity"))
    }.getOrElse       (JsonBadRequest("Expecting JSON data"))
  }.getOrElse         (JsonBadRequest("Could not find idea with id '%s'".format(ideaId)))
}

我觉得所有那些嵌套的 .map 有点烦人,而且我也觉得每个错误处理都在底部有点乏味

您将如何改进它以使其更具可读性,同时保持功能性惯用的 scala 代码?

我在想可能是这样的(它是伪代码,仍然无法编译)

def save(ideaId : Long) = CORSAction { request =>

  val idea = Idea.findById(ideaId).getOrElse(
    return JsonBadRequest("Could not find idea with id '%s'".format(ideaId)))

  val json = request.body.asJson.getOrElse(
    return JsonBadRequest("Expecting JSON data"))

  val comment = json.asOpt[Comment].getOrElse(
    return JsonBadRequest("Invalid Comment entity"))

  comment.copy(idea = idea).save.fold(
    errors => JsonBadRequest(errors),
    comment => Ok(toJson(comment).toString)
  )

}

ps:我知道最好避免使用return语句...

【问题讨论】:

  • 恭喜,你刚刚发明了单子!
  • 删除returns 作为启动器。哦,等等,他们应该打破save 方法的流程吗?

标签: scala error-handling idioms


【解决方案1】:

首先要简化。假设我有三个方法接受 String 并返回 Option[String]

def foo(s: String): Option[String] = if (s.size >= 4) Some(s + "1") else None
def bar(s: String): Option[String] = if (s(0) != 'A') Some(s + "2") else None
def baz(s: String): Option[String] = if (s toSet ' ') Some(s + "3") else None

我想要一个通过这些管道传输字符串的方法,并在我收到None 时返回相应的错误消息。我可以这样写:

def all(s: String): Either[String, String] =
  foo(s).map { x =>
    bar(x).map { y =>
      baz(y).map { z =>
        Right(z)
      } getOrElse Left("Doesn't contain a space!")
    } getOrElse   Left("Starts with an A!")
  } getOrElse     Left("Too short!")

但是,这并不漂亮。我们可以使用for-comprehension 和Option 上的toRight 方法来编写更清晰的版本:

def all(s: String): Either[String, String] = for {
  x <- (foo(s) toRight "Too short!"              ).right
  y <- (bar(x) toRight "Starts with an A!"       ).right
  z <- (baz(y) toRight "Doesn't contain a space!").right
} yield z

Option 上调用toRight(msg),如果它是空的,我们会得到Left(msg),否则会得到Right(whatever)。然后我们必须对Either.right 进行正确的投影,因为Scala 的Either 没有右偏。

在你的情况下相当于这样:

def save(ideaId: Long) = CORSAction { request =>
  val saveResult = for {
    idea    <- (Idea.findById(ideaId) toRight "Could not find id"     ).right
    json    <- (request.body.asJson   toRight "Invalid Comment entity").right
    comment <- (json.asOpt[Comment]   toRight "Expecting JSON data"   ).right
    result  <- comment.copy(idea = idea).save().right
  } yield result

  saveResult.fold(
    error => JsonBadRequest(error),
    comment => Ok(toJson(comment).toString)
  )
}

不像您想要的语法那么简洁,但错误消息出现在更合乎逻辑的位置,我们已经摆脱了丑陋的嵌套。

【讨论】:

  • 我认为嵌套地图实际上是flatMaps,否则你最终会得到Either[Either[...],Either[...]],不是吗?
  • @pedrofurla:在嵌套版本中,每个级别的getOrElse 已经在进行扁平化。
  • @om-non-nom:感谢您的编辑!我发誓我从来没有在 Scala 中输入过那个词
猜你喜欢
  • 2013-10-01
  • 2010-10-07
  • 2020-07-20
  • 2020-11-18
  • 1970-01-01
  • 2015-05-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多