斯卡拉:回归有它的位置

时间:2012-06-01 22:13:51

标签: scala playframework-2.0 return type-inference

参考文献:
Scala return keyword
handling errors in scala controllers

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 root&amp; child项目扩展了它们的基础包对象:

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
进化,已经从嵌入式的返回语句变成了这个密度较小的白矮星(感谢@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投影作为pimp添加到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
        )}
    )}
)

这肯定是一个密集的强大的代码块,很多发生在那里;但是,我认为相应的具有嵌入式返回的命令式代码不仅同样简洁,而且更容易理解(附加的好处是更少的尾随曲线和parens来跟踪)

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 happy 1-shot-deal fold(错误,成功)条件,这在现实世界中并非总是如此; - )

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

4 个答案:

答案 0 :(得分:26)

所以作为一个Haskeller,显然在我看来,一切的解决方案都是Monads。和我一起进入一个简化的世界(简化为我,就是这样),你的问题出在Haskell中,你有以下类型要处理(作为Haskeller,我对类型有这种迷恋):< / p>

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

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

我们暂停一下。在这一点上,我在boundFormHasErrorstransIDisEmpty有点畏缩。这两件事都暗示将失败的可能性分别注入 BoundFormTransID。那很糟。相反,应该将失败的可能性分开。请允许我提出这个替代方案:

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

感觉好一点,这些Eithers正在引领使用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的工作原理如下:只要你点击Left,你就会停下来。 (我选择这个monad来模仿早期的回报是否有任何意外?)这一切都很好,但我们总是希望Action停止,因此停止使用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变换器,并在EitherT之上堆叠IO。但是,在Scala中,这不是一个问题,因此无论我们在哪里看到IO Foo,我们都可以假装它是Foo

好的,我们来写save。我们将使用do语法,稍后会将其翻译为Scala的{​​{1}}语法。回想一下for语法,你可以做三件事:

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

然后最后我们可以guard,这与Haskell中的yield相同。我们将自己局限于这些事情,以确保从Haskell到Scala的翻译顺利。

return

注意什么?它看起来几乎与你用命令式风格写的代码完全相同

你可能想知道我为什么要经历所有这些努力来在Haskell中写出答案。好吧,这是因为我喜欢检查我的答案,而且我对Haskell中如何做到这一点非常熟悉。这是一个typechecks的文件,并且包含我刚刚指定的所有类型签名(无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 ):http://hpaste.org/69442

好的,现在让我们将其转换为Scala。首先是IO助手。

这里开始Scala

Either

现在,// 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) ) } <-左侧的变量被隐式地视为=,因为它们位于val块内。您可以随意更改for,以便将其设置为onLeft值,以获得更漂亮的使用效果。 另外,请确保为Either s导入适当的“Monad实例”。

总之,我只想指出monadic sugar的全部目的是为了压缩嵌套的功能代码。所以使用它!

[编辑:在Scala中,您必须“正确偏见”Either以使其与Either语法一起使用。这是通过将for添加到.right右侧的Either值来完成的。无需额外进口。这可以在<-内部完成,以获得更漂亮的代码。另见:https://stackoverflow.com/a/10866844/208257]

答案 1 :(得分: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
    }
  }
}

答案 2 :(得分:2)

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

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

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

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")
      }
  } 
}

(如果您希望能够嵌套returning,只需重新抛出Return,而不是在类型不匹配时抛出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功能,它使用了与此策略非常相似的功能。)

答案 3 :(得分:1)

恕我直言,这里的问题似乎是你在控制器中执行业务逻辑,并且Play签名没有 ahem 与返回值一起玩得很好,这是次要的。

我建议你把它封装起来 generateOrderNum, processPayment, createMember 在一个Facade后面调用,并且该返回值可以返回业务事务的适当状态,然后可以用它来返回正确的控制器状态。

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

编辑: 这非常草率,所以仔细检查语法,但我的答案的要点是将您的业务逻辑序列移动到外部类,该类将利用您已经使用的Either / Left / Right,但现在包括您对空事务的检查左侧回复中的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,但不确定将其折叠到匹配中的干净方法。

有意义吗?