斯卡拉:回归有它的位置

vir*_*yes 13 scala type-inference return playframework-2.0

参考:
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)
}
Run Code Online (Sandbox Code Playgroud)

然后是pimp'd Either项目方法,放在你的应用程序的某个地方(在我的例子中,一个impbits特性,sbt root和子项目扩展了它们的基础包对象:

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)
Run Code Online (Sandbox Code Playgroud)

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

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)
  ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

无论如何,很明显,该功能的方法避免了需要嵌入式return语句,因为我试图用的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
        )}
    )}
)
Run Code Online (Sandbox Code Playgroud)

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

原始
在紧急情况下找到自己; 希望看到以下的替代方法(由于使用了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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

无论如何,Play框架示例提供la,la,la,la happy 1-shot-deal fold(错误,成功)条件,这在现实世界中并非总是如此;-)

那么上面代码块的惯用等价物(不使用return)是什么?我相信它会被嵌套的if/else,比赛,或折叠,从而变得有点难看,每一层嵌套条件缩进.

Dan*_*ton 27

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

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

processPayment :: Model -> OrderNum -> TransID
isEmpty :: TransID -> Bool
Run Code Online (Sandbox Code Playgroud)

我们暂停一下 在这一点上,我有点畏缩了一下,在boundFormHasErrorstransIDisEmpty.这些事情都意味着失败的可能性被注入 BoundFormTransID分别.那很糟.相反,应该将失败的可能性分开.请允许我提出这个替代方案:

bindFormRequest :: Request -> Form -> Either FormBindError BoundForm
processPayment :: Model -> OrderNum -> Either TransError TransID 
Run Code Online (Sandbox Code Playgroud)

感觉好一点,这些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
Run Code Online (Sandbox Code Playgroud)

好的,现在我要做一些有点不可思议的事情,让我告诉你为什么.Either monad就是这样的:只要你击中一个Left,你就会停下来.(我选择这个monad来模仿早期的回报是否有任何意外?)这一切都很好,但是我们总是想停下来Action,所以停止使用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
Run Code Online (Sandbox Code Playgroud)

在这一点上,在Haskell中,我谈论monad变换器,并在堆叠EitherT之上IO.但是,在Scala中,这不是一个问题,所以无论我们在哪里看到IO Foo,我们都可以假装它是一个Foo.

好吧,我们来写吧save.我们将使用do语法,后来将它翻译成Scalafor语法.回想一下for语法,你可以做三件事:

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

然后最后我们可以yield,这与returnHaskell中的相同.我们将自己限制在这些事情上,以确保从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
Run Code Online (Sandbox Code Playgroud)

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

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

现在,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) ) }
Run Code Online (Sandbox Code Playgroud)

请注意,左侧的变量<-=隐含地被认为是vals,因为它们位于for块内.你应该随意改变,onLeft以便Either为更漂亮的用法而加以限制.另外,请确保为Eithers 导入适当的"Monad实例" .

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

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


kir*_*uku 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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • @DanBurton:我认为理解方法中的方法并不复杂. (4认同)