13

参考: Scala在 Scala 控制器中
返回关键字处理错误

EDIT3
这是“最终”解决方案,再次感谢 Dan Burton。

def save = Action { implicit request =>
  val(orderNum, ip) = (generateOrderNum, request.remoteAddress)
  val result = for {
    model   <- bindForm(form).right // error condition already json'd
    transID <- payment.process(model, orderNum) project json
    userID  <- dao.create(model, ip, orderNum, transID) project json
  } yield (userID, transID)
}

然后是 pimp'd Either 项目方法,放置在您的应用程序中的某个位置(在我的情况下,一个隐式特征,即 sbt 根和子项目从以下位置扩展它们的基本包对象:

class EitherProvidesProjection[L1, R](e: Either[L1, R]) {
  def project[L1, L2](f: L1 => L2) = e match {
    case Left(l:L1) => Left(f(l)).right
    case Right(r)   => Right(r).right
  }
}
@inline implicit final def either2Projection[L,R](e: Either[L,R]) = new EitherProvidesProjection(e)

EDIT2
Evolution,已经从嵌入式返回语句变成了这个密度的小白矮星(感谢@DanBurton,Haskell 流氓;-))

def save = Action { implicit request =>
  val(orderNum, ip) = (generateOrderNum, request.remoteAddress)
  val result = for {
    model   <- form.bindFromRequest fold(Left(_), Right(_)) project( (f:Form) => Conflict(f.errorsAsJson) )
    transID <- payment.process(model, orderNum) project(Conflict(_:String))
    userID  <- dao.create(model, ip, orderNum, transID) project(Conflict(_:String))
  } yield (userID, transID)
  ...
}

我已将 Dan 的 onLeft Either 投影作为皮条客添加到 Either,使用上述“项目”方法,该方法允许右偏eitherResult project(left-outcome). 基本上,您将失败优先错误作为左派,将成功作为右派,这在将选项结果提供给理解时不起作用(您只会得到一些/无结果)。

我唯一不满意的是必须指定 ; 的类型project(Conflict(param))。我认为编译器能够从传递给它的 Either 中推断出左条件类型:显然不是。

无论如何,很明显,函数式方法不需要嵌入返回语句,就像我尝试使用 if/else 命令式方法一样。

编辑
功能等价物是:

val bound = form.bindFromRequest
bound fold(
  error=> withForm(error),
  model=> {
    val orderNum = generateOrderNum()
    payment.process(model, orderNum) fold (
      whyfail=> withForm( bound.withGlobalError(whyfail) ),
      transID=> {
        val ip = request.headers.get("X-Forwarded-For")
        dao.createMember(model, ip, orderNum, transID) fold (
          errcode=> 
            Ok(withForm( bound.withGlobalError(i18n(errcode)) )),
          userID=> 
            // generate pdf, email, redirect with flash success
        )}
    )}
)

这当然是一个功能强大的代码块,在那里发生了很多事情;但是,我认为相应的带有嵌入式返回的命令式代码不仅同样简洁,而且更容易理解(具有更少的尾随花括号和括号来跟踪的额外好处)

ORIGINAL
发现自己处于紧急情况;希望看到以下方法的替代方法(由于使用 return 关键字和方法上缺少显式类型而不起作用):

def save = Action { implicit request =>
  val bound = form.bindFromRequest
  if(bound.hasErrors) return Ok(withForm(bound))

  val model = bound.get
  val orderNum = generateOrderNum()
  val transID  = processPayment(model, orderNum)
  if(transID.isEmpty) return Ok(withForm( bound.withGlobalError(...) ))

  val ip = request.headers.get("X-Forwarded-For")
  val result = dao.createMember(model, ip, orderNum, transID)
  result match {
    case Left(_) => 
      Ok(withForm( bound.withGlobalError(...) ))
    case Right((foo, bar, baz)) =>
      // all good: generate pdf, email, redirect with success msg
    }
  }
}

在这种情况下,我喜欢使用 return 来避免嵌套多个 if/else 块、折叠、匹配或填充空白的非命令式方法。当然,问题在于它不起作用,必须指定显式返回类型,这有其自身的问题,因为我还没有弄清楚如何指定一个满足 Play 魔法工作的类型 - 不,def save: Result, 不起作用,因为编译器然后抱怨implicit result现在没有显式类型;-(

无论如何,Play 框架示例提供了 la, la, la, la 快乐的 1-shot-deal fold(error, success) 条件,这在现实世界中并非总是如此™ ;-)

那么上述代码块的惯用等价物(不使用返回)是什么?我假设它会嵌套 if/else、match 或 fold,这有点难看,每个嵌套条件都会缩进。

4

4 回答 4

27

因此,作为一个 Haskeller,显然在我看来,所有问题的解决方案都是 Monads。和我一起进入一个简化的世界(对我来说很简单),你的问题在 Haskell 中,你有以下类型需要处理(作为一个 Haskeller,我有点迷恋类型):

bindFormRequest :: Request -> Form -> BoundForm
hasErrors :: BoundForm -> Bool

processPayment :: Model -> OrderNum -> TransID
isEmpty :: TransID -> Bool

让我们在这里暂停一下。在这一点上,我有点畏缩boundFormHasErrorsand transIDisEmpty。这两件事都意味着失败的可能性分别被注入 BoundFormTransID。那很糟。相反,应该单独维护失败的可能性。请允许我提出这个替代方案:

bindFormRequest :: Request -> Form -> Either FormBindError BoundForm
processPayment :: Model -> OrderNum -> Either TransError TransID 

这感觉好多了,这些 Either 导致了 Either monad 的使用。让我们写一些更多的类型。我将忽略OK它,因为它几乎涵盖了所有内容。我有点捏造,但概念仍然会翻译成一样的。相信我; 最后,我将把它带回 Scala。

save :: Request -> IO Action

form :: Form
withForm :: BoundForm -> Action

getModel :: BoundForm -> Model
generateOrderNum :: IO OrderNum
withGlobalError :: ... -> BoundForm -> BoundForm

getHeader :: String -> Request -> String
dao :: DAO
createMember :: Model -> String -> OrderNum -> TransID
             -> DAO -> IO (Either DAOErr (Foo, Bar, Baz))

allGood :: Foo -> Bar -> Baz -> IO Action

好的,现在我要做一些有点奇怪的事情,让我告诉你为什么。Either monad 的工作原理是这样的:只要你点击 a Left,你就会停下来。(我选择这个 monad 来模拟早期返回有什么意外吗?)这一切都很好,但是我们希望总是Action以 a 停止,因此以 a 停止并FormBindError不会削减它。因此,让我们定义两个函数,让我们以这样一种方式处理 Eithers,如果我们发现一个Left.

-- if we have an `Either a a', then we can always get an `a' out of it!
unEither :: Either a a -> a
unEither (Left a) = a
unEither (Right a) = a

onLeft :: Either l r -> (l -> l') -> Either l' r
(Left l)  `onLeft` f = Left (f l)
(Right r) `onLeft` _ = Right r

此时,在 Haskell 中,我讨论 monad 转换器,以及EitherTIO. 然而,在 Scala 中,这不是问题,所以无论我们看到什么IO Foo,我们都可以假装它是一个Foo.

好吧,我们来写吧save。我们将使用do语法,稍后将其转换为Scala'sfor语法。回想一下for语法,你可以做三件事:

  • 使用从生成器分配<-(这与 Haskell 的 相当<-
  • =使用(这类似于 Haskell 的let)为计算结果分配一个名称
  • 使用带有关键字的过滤器if(这类似于 Haskell 的guard函数,但我们不会使用它,因为它不能让我们控制产生的“异常”值)

然后最后我们可以yield,这与 Haskell 中的相同return。我们将把自己限制在这些事情上,以确保从 Haskell 到 Scala 的转换是顺利的。

save :: Request -> Action
save request = unEither $ do
  bound <- bindFormRequest request form
           `onLeft` (\err -> withForm (getSomeForm err))

  let model = getModel bound
  let orderNum = generateOrderNum
  transID <- processPayment model orderNum
             `onLeft` (\err -> withForm (withGlobalError ... bound))

  let ip = getHeader "X-Forwarded-For" request
  (foo, bar, baz) <- createMember model ip orderNum transID dao
                     `onLeft` (\err -> withForm (withGlobalError ... bound))

  return $ allGood foo bar baz

注意到什么了吗?它看起来几乎您以命令式风格编写的代码相同!

您可能想知道为什么我要花这么多精力在 Haskell 中写出答案。嗯,这是因为我喜欢对我的答案进行类型检查,而且我对如何在 Haskell 中执行此操作相当熟悉。这是一个类型检查的文件,并且具有我刚刚指定的所有类型签名(sans IO):http ://hpaste.org/69442

好的,现在让我们将其转换为 Scala。第一,Either帮手。

从这里开始 Scala

// be careful how you use this.
// Scala's subtyping can really screw with you if you don't know what you're doing
def unEither[A](e: Either[A, A]): A = e match {
  case Left(a)  => a
  case Right(a) => a
}

def onLeft[L1, L2, R](e: Either[L1, R], f: L1 => L2) = e match {
  case Left(l) = Left(f(l))
  case Right(r) = Right(r)
}

现在,save方法

def save = Action { implicit request => unEither( for {
  bound <- onLeft(form.bindFormRequest,
                  err => Ok(withForm(err.getSomeForm))).right

  model = bound.get
  orderNum = generateOrderNum()
  transID <- onLeft(processPayment(model, orderNum),
                    err => Ok(withForm(bound.withGlobalError(...))).right

  ip = request.headers.get("X-Forwarded-For")
  (foo, bar, baz) <- onLeft(dao.createMember(model, ip, orderNum, transID),
                            err => Ok(withForm(bound.withGlobalError(...))).right
} yield allGood(foo, bar, baz) ) }

<-请注意, or左侧的变量=被隐式视为vals,因为它们位于for块内。您应该随意更改onLeft,以便将其添加到Either更漂亮使用的值上。此外,请确保为Eithers 导入适当的“Monad 实例”。

总之,我只想指出一元糖的全部目的是扁平化嵌套的函数式代码。所以使用它!

[编辑:在 Scala 中,您必须使用“正确Either的偏见”才能使它们与for语法一起工作。这是通过添加.rightEither右侧的值来完成的<-。不需要额外的进口。这可以在内部完成,onLeft以获得更漂亮的代码。另请参阅:https ://stackoverflow.com/a/10866844/208257 ]

于 2012-06-02T22:21:09.203 回答
5

一些嵌套的defs呢?

def save = Action { implicit request =>
  def transID = {
    val model = bound.get
    val orderNum = generateOrderNum()
    processPayment(model, orderNum)
  }
  def result = {
    val ip = request.headers.get("X-Forwarded-For")
    dao.createMember(model, ip, orderNum, transID)
  }
  val bound = form.bindFromRequest

  if(bound.hasErrors) Ok(withForm(bound))
  else if(transID.isEmpty) Ok(withForm( bound.withGlobalError(...) ))
  else result match {
    case Left(_) => 
      Ok(withForm( bound.withGlobalError(...) ))
    case Right((foo, bar, baz)) =>
      // all good: generate pdf, email, redirect with success msg
    }
  }
}
于 2012-06-01T22:35:50.793 回答
2

Scala 内部使用 throw/catch 机制来处理返回在语法上可以但实际上必须跳出几个方法的地方。所以你可以让它这样做:

def save = Action { implicit request =>
  def result(): Foo = {
    /* All your logic goes in here, including returns */
  }
  result()
}

或者,如果您愿意,可以使用自己的数据传递可抛出类(没有堆栈跟踪):

import scala.util.control.ControlThrowable
case class Return[A](val value: A) extends ControlThrowable {}

def save = Action { implicit request =>
  try {
    /* Logic */
    if (exitEarly) throw Return(Ok(blahBlah))
    /* More logic */
  }
  catch {
    case Return(x: Foo) => x
  }
}

或者你可以更花哨一些,添加你自己的异常处理:

case class Return[A](val value: A) extends ControlThrowable {}
class ReturnFactory[A]{ def apply(a: A) = throw new Return(a) }
def returning[A: ClassManifest](f: ReturnFactory[A] => A) = {
  try { f(new ReturnFactory[A]) } catch {
    case r: Return[_] =>
      if (implicitly[ClassManifest[A]].erasure.isAssignableFrom(r.value.getClass)) {
        r.value.asInstanceOf[A]
      } else {
        throw new IllegalArgumentException("Wrong Return type")
      }
  } 
}

(如果您希望能够嵌套returnings,只需在类型不匹配时重新抛出Return而不是抛出 an 。)您可以像这样使用它:IllegalArgumentException

def bar(i: Int) = returning[String] { ret =>
  if (i<0) ret("fish")
  val j = i*4
  if (j>=20) ret("dish")
  "wish"*j
}

bar(-3)   // "fish"
bar(2)    // "wishwishwishwishwishwishwishwish"
bar(5)    // "dish"

或在您的特定情况下

def save = Action{ implicit request => returning[Foo] { ret =>
  /* Logic goes here, using ret(foo) as needed */
}}

它不是内置的,但向人们解释如何使用它应该不难,即使理解该功能的构建方式并不容易。(注意:Scala 确实具有内置break功能,scala.util.control.Breaks其中使用非常类似于此策略的东西。)

于 2012-06-02T04:08:56.940 回答
1

恕我直言,这里的问题似乎是您正在控制器中执行业务逻辑,并且 Play 签名与返回值不匹配,这样的返回值是次要的。

我建议您将 generateOrderNum、processPayment、createMember 调用封装在外观后面,并且该返回值可以返回业务事务的适当状态,然后可以用于返回适当的控制器状态。

稍后将用一个例子更新这个答案。

编辑:这很草率,所以仔细检查语法,但我的回答的要点是将您的业务逻辑序列移动到一个外部类中,该类将利用您已经使用的 Either/Left/Right,但现在包括您的检查Left 响应中的空事务 ID。

def save = Action {implicit request =>
  val bound = form.bindFromRequest
  if (!bound.hasErrors) {
    val model = bound.get
    val ip = request.headers.get("X-Forwarded-For")

    val result = paymentService.processPayment(model, ip)

    result match {
      case Left(_) => Ok(withForm(bound.withGlobalError(...)))
      case Right((foo, bar, baz)) => // all good: generate pdf, email, redirect with success msg
    }
  }
  else Ok(withForm(bound))
}

class PaymentService {
  def processPayment(model, ip): Either[Blah, Blah] = {
    val orderNum = generateOrderNum()
    val transID = processPayment(model, orderNum)
    if (transID.isEmpty) Left(yadda)
    else Right(dao.createMember(model, ip, orderNum, transID))
  }
}

这里唯一有点做作的是bound.hasErrors的if / else,但不确定将其折叠到匹配中的干净方法。

说得通?

于 2012-06-02T00:05:36.787 回答