Scala:回归有其地位 [英] Scala: return has its place
问题描述
参考文献:
Scala返回关键字
处理Scala控制器中的错误
References:
Scala return keyword
handling errors in scala controllers
EDIT3
再次感谢丹·伯顿,这是最终"解决方案.
EDIT3
This is the "final" solution, again thanks to 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根和子项目的隐式特征从以下位置扩展其基本包对象:
Then the pimp'd Either project method, placed somewhere in your application (in my case, an implicits trait that sbt root & child project(s) extends their base package object from:
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
进化,从嵌入的return语句变成了密度的白色小矮人(对@ DanBurton,Haskell rascal ;-)表示敬意)
EDIT2
Evolution, have gone from embedded return statements to this little white dwarf of density (kudos to @DanBurton, the 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)
...
}
我通过上述"project"方法将Dan的onLeft Either投影作为皮条客添加到Either,该方法允许右偏eitherResult project(left-outcome)
.基本上,您将失败优先错误作为左方,将成功作为右方,这在将Option结果提供给理解时是行不通的(您只会得到Some/None结果).
I have added Dan's onLeft Either projection as a pimp to Either, with the above "project" method, which allows for right-biased eitherResult project(left-outcome)
. Basically you get fail-first error as a Left and success as a Right, something that would not work when feeding Option outcomes to for comprehension (you only get Some/None outcome).
我唯一不感到兴奋的是必须指定project(Conflict(param))
的类型.我认为编译器将能够从传递给它的Either中推断出左条件类型:显然不是.
The only thing I'm not thrilled with is having to specify the type for the project(Conflict(param))
; I thought the compiler would be able to infer the left condition type from the Either that is being passed to it: apparently not.
无论如何,很明显,正如我尝试使用if/else命令式方法一样,函数方法避免了对嵌入式return语句的需求.
At any rate, it's clear that the functional approach obviates the need for embedded return statements as I was trying to do with if/else imperative approach.
编辑
功能上等效的是:
EDIT
The functional equivalent is:
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
)}
)}
)
这肯定是一个功能强大的代码块,在那里发生了很多事情;但是,我认为具有嵌入式收益的相应命令式代码不仅简洁明了,而且更易于理解(具有减少尾随的curly和parens以便跟踪的附加好处)
which is certainly a densely power packed block of code, a lot happening there; however, I would argue that corresponding imperative code with embedded returns is not only similarly concise, but also easier to grok (with added benefit of fewer trailing curlies & parens to keep track of)
原始
发现自己处于当务之急;希望看到以下替代方法(由于使用return关键字且方法上缺少显式类型而无法使用):
ORIGINAL
Finding myself in an imperative situation; would like to see an alternative approach to the following (which does not work due to the use of return keyword and lack of explicit type on method):
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
现在没有显式类型;-(
In this case I like the use of return as you avoid nesting several if/else blocks, or folds, or matches, or fill-in-the-blank non-imperative approach. The problem of course, is that it doesn't work, an explicit return type has to specified, which has its own issues as I have yet to figure out how to specify a type that satisfies whatever Play magic is at work -- no, def save: Result
, does not work as the compiler then complains about implicit result
now not having an explicit type ;-(
无论如何,Play框架示例提供了la,la,la,la快乐的一杆交易折叠(错误,成功)条件,这在现实世界和贸易中并非总是如此; ;-)
At any rate, Play framework examples provide la, la, la, la happy 1-shot-deal fold(error, success) condition which is not always the case in the real world™ ;-)
那么上述代码块的惯用等效词是什么(不使用return)?我认为它会嵌套在if/else,match或fold上,这会有点丑陋,并且会缩进每个嵌套条件.
So what is the idiomatic equivalent (without use of return) to above code block? I assume it would be nested if/else, match, or fold, which gets a bit ugly, indenting with each nested condition.
推荐答案
因此,作为Haskeller,显然在我看来,解决所有问题的方法就是Monads.跟我走一会儿,进入一个简化的世界(对我来说是简化的),那里的问题出在Haskell上,您需要处理以下类型(作为Haskeller,我有点喜欢这种类型的恋物癖):
So as a Haskeller, obviously in my mind, the solution to everything is Monads. Step with me for a moment into a simplified world (simplified for me, that is) where your problem is in Haskell, and you have the following types to deal with (as a Haskeller, I sort of have this fetish for types):
bindFormRequest :: Request -> Form -> BoundForm
hasErrors :: BoundForm -> Bool
processPayment :: Model -> OrderNum -> TransID
isEmpty :: TransID -> Bool
让我们在这里暂停.在这一点上,我对boundFormHasErrors
和transIDisEmpty
有点勉强.这两件事都暗示将失败的可能性分别注入到 BoundForm
和TransID
.那很糟.取而代之的是,应该将发生故障的可能性分开保持.请允许我提出这个替代方案:
Let's pause here. At this point, I'm sort of cringing a bit at boundFormHasErrors
and transIDisEmpty
. Both of these things imply that the possibility of failure is injected into BoundForm
and TransID
respectively. That's bad. Instead, the possibility of failure should be maintained separate. Allow me to propose this alternative:
bindFormRequest :: Request -> Form -> Either FormBindError BoundForm
processPayment :: Model -> OrderNum -> Either TransError TransID
感觉好多了,这些Eithers都在使用Either monad.让我们再写一些类型.我将忽略OK
,因为它几乎包裹了所有内容;我有点困惑,但是这些概念仍然会翻译成相同的意思.相信我;最后,我将其带回Scala.
That feels a bit better, and these Eithers are leading into the use of the Either monad. Let's write up some more types though. I'm going to ignore OK
because that is wrapped around pretty much everything; I'm fudging a little bit but the concepts will still translate just the same. Trust me; I'm bringing this back around to Scala in the end.
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
,则可以安装更多处理".
OK, now I'm going to do something a bit wonky, and let me tell you why. The Either monad works like this: as soon as you hit a Left
, you stop. (Is it any surprise I chose this monad to emulate early returns?) This is all well and good, but we want to always stop with an Action
, and so stopping with a FormBindError
isn't going to cut it. So let's define two functions that will let us deal with Eithers in such a way that we can install a little more "handling" if we discover a 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中讨论单子变压器,并将EitherT
堆叠在IO
上.但是,在Scala中,这不是问题,因此无论我们在哪里看到IO Foo
,我们都可以假装它是Foo
.
At this point, in Haskell, I would talk about monad transformers, and stacking EitherT
on top of IO
. However, in Scala, this is not a concern, so wherever we see IO Foo
, we can just pretend it is a Foo
.
好的,让我们写save
.我们将使用do
语法,稍后将其转换为Scala
的for
语法.回忆起for
语法,您可以做三件事:
Alright, let's write save
. We will use do
syntax, and later will translate it to Scala
's for
syntax. Recall in for
syntax you are allowed to do three things:
- 使用
<-
从生成器分配(与Haskell的<-
类似) - 使用
=
为计算结果分配名称(与Haskell的let
类似) - 使用带有关键字
if
的过滤器(这与Haskell的guard
函数相当,但是我们不会使用它,因为它无法控制产生的异常"值)
- assign from a generator using
<-
(this is comparable to Haskell's<-
) - assign a name to the result of a computation using
=
(this is comparable to Haskell'slet
) - use a filter with the keyword
if
(this is comparable to Haskell'sguard
function, but we won't use this because it doesn't give us control of the "exceptional" value produced)
然后最后我们可以yield
,与Haskell中的return
相同.我们将限制自己在这些事情上,以确保从Haskell到Scala的翻译是顺畅的.
And then at the end we can yield
, which is the same as return
in Haskell. We will restrict ourselves to these things to make sure that the translation from Haskell to Scala is smooth.
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
有事吗?与您以命令式风格编写的代码几乎几乎完全相同!
Notice something? It looks almost identical to the code you wrote in imperative style!
您可能想知道为什么我要花这么多精力在Haskell上写一个答案.好吧,这是因为我喜欢对答案进行类型检查,并且我对在Haskell中执行此操作非常熟悉.这是一个进行类型检查的文件,并且具有我刚刚指定的所有类型签名(无cc14): http://hpaste .org/69442
You may be wondering why I went through all this effort to write up an answer in Haskell. Well, it's because I like to typecheck my answers, and I'm rather familiar with how to do this in Haskell. Here's a file that typechecks, and has all of the type signatures I just specified (sans IO
): http://hpaste.org/69442
好的,现在让我们将其翻译为Scala.首先是Either
助手.
OK, so now let's translate that to Scala. First, the Either
helpers.
此处开始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) ) }
请注意,由于<-
或=
左侧的变量位于for
块内部,因此它们被隐式认为是val
.您应该随意更改onLeft
,以便将其设置在Either
值上以更美观. 此外,请确保为 Either
s导入适当的"Monad实例".
Note that variables on the left hand side of <-
or =
are implicitly considered to be val
s since they are inside of a for
block. You should feel free to change onLeft
so that it is pimped onto Either
values for prettier usage. Also, make sure you import an appropriate "Monad instance" for Either
s.
最后,我只想指出,单子糖的全部目的是使嵌套的功能代码平坦.所以使用它!
In conclusion, I just wanted to point out that the whole purpose of monadic sugar is to flatten out nested functional code. So use it!
[edit:在Scala中,您必须正确偏向" Either
以使其使用for
语法工作.这是通过将.right
添加到<-
右侧的Either
值来完成的.无需额外进口.对于看起来更漂亮的代码,这可以在onLeft
内部完成.另请参阅: https://stackoverflow.com/a/10866844/208257 ]
[edit: in Scala, you have to "right bias" Either
s to make them work with for
syntax. This is done by adding .right
to the Either
values on the right-hand side of the <-
. No extra imports necessary. This could be done inside of onLeft
for prettier-looking code. See also: https://stackoverflow.com/a/10866844/208257 ]
这篇关于Scala:回归有其地位的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!