Вопрос по playframework-2.0 – Скала: возвращение имеет свое место
Рекомендации:
Scala возвращаемое ключевое слово
обработка ошибок в контроллерах scala
EDIT3
Это «окончательный» Решение, снова благодаря Дэн Бертон.
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 'a Either, размещенный где-то в вашем приложении (в моем случае это признак, который указывает на то, что sbt root & amp; дочерний проект (ы) расширяет свой объект базового пакета из:
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, негодяй Хаскелла ;-))
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 с вышеупомянутым "project" quot; метод, который учитываетeitherResult project(left-outcome)
, По сути, вы получаете ошибку «первый сбой» как «Левый», а успех как «Правый», что не сработает при подаче результатов Варианта для понимания (вы получаете только результат «Некоторые / Нет»).
Единственное, что меня не радует, - это указание типа дляproject(Conflict(param))
; Я думал, что компилятор сможет вывести левый тип условия из того, что передается ему: очевидно, нет.
Во всяком случае, ясно, что функциональный подход устраняет необходимость во встроенных операторах возврата, как я пытался сделать с императивным подходом if / else.
EDIT
Функциональный эквивалент:
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
теперь не имеет явного типа ;-(
В любом случае, примеры игровых фреймворков предоставляют условие «la, la, la, la, la happy 1-shot-сделки» (ошибка, успех), что не всегда имеет место в реальном мире & # x2122; ;-)
Так что же такое идиоматический эквивалент (без использования возврата) вышеуказанному блоку кода? Я предполагаю, что он будет вложенным, если / else, совпадет или сложится, что немного уродливо, если отступать для каждого вложенного условия.
generateOrderNum
, dao.createMember
и т. д.) зависит от того, выполняются ли определенные условия.
Dan Burton
Left
значения для вычисления короткого замыкания вместоreturn
. Я думаю, что это получилось довольно хорошо, хотя я не достаточно удобен со Scala, чтобы быть в состоянии объявить его 100% -ым звуковым решением. (Я вполне уверен в части Haskell.) Я даже не уверен, где (или если вообще) в стандартных библиотеках Scala подходящий экземпляр монады дляEither
может быть найден
Dan Burton
как Хаскеллер, очевидно, на мой взгляд, решение всего - Монады. Шагните со мной на мгновение в упрощенный мир (то есть упрощенный для меня), где ваша проблема в Haskell, и у вас есть следующие типы для решения (как у Haskeller, у меня вроде есть этот фетиш для типов):
bindFormRequest :: Request -> Form -> BoundForm
hasErrors :: BoundForm -> Bool
processPayment :: Model -> OrderNum -> TransID
isEmpty :: TransID -> Bool
Давай остановимся здесь. На данный момент, я как бы немного цепляюсь заboundFormHasErrors
а такжеtransIDisEmpty
. Обе эти вещи подразумевают, что возможность отказа вводитс BoundForm
а такжеTransID
соответственно. Плохо. Вместо этого возможность отказа должна поддерживаться отдельно. Позвольте мне предложить эту альтернативу:
bindFormRequest :: Request -> Form -> Either FormBindError BoundForm
processPayment :: Model -> OrderNum -> Either TransError TransID
Это чувствует себя немного лучше, и эти Итеры ведут к использованию монады Итер. Давайте напишем еще несколько типов, хотя. Я собираюсь игнорироватьOK
потому что это оборачивается практически всем; Я немного выдумал, но концепции все равно будут переводиться. Доверьтесь мне; В конце я возвращаю это Скале.
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
Ладно, теперь я собираюсь сделать что-то немного странное, и позвольте мне рассказать вам, почему. Любая монада работает так: как только вы нажметеLeft
, ты прекрати. (Удивительно ли, что я выбрал эту монаду для имитации ранних возвратов?) Это все хорошо, но мы хотим всегда останавливаться на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
В этот момент в Хаскеле ябыло б говорить о монадных трансформаторах и укладкеEitherT
на вершинеIO
. Однако в Scala это не проблема, поэтому везде, где мы видимIO Foo
, мы можем просто притвориться, что этоFoo
.
Хорошо, давай напишемsave
. Мы будем использоватьdo
синтаксис, а позже переведет его наScala
sfor
синтаксис. Напомним, вfor
синтаксис вы можете делать три вещи:
<-
(это сравнимо с @ Хаскел<-
) присвоить имя результату вычисления, используя=
(это сравнимо с @ Хаскелlet
) использовать фильтр с ключевым словомif
(это сравнимо с @ Хаскелguard
function, но мы не будем использовать это, потому что это не дает нам контроль над «исключительным» значением) А потом в конце мы сможемyield
, что совпадает с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. Вот файл, который проверяет типы и содержит все сигнатуры типов, которые я только что указал (sansIO
):http: //hpaste.org/6944
Ладно, теперь давайте переведем это на 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
значения для более красивого использования. Также убедитесь, что вы импортировали соответствующий «экземпляр Monad» дляEither
S.
В заключение я просто хотел бы отметить, что цель монадного сахара в том, чтобы сгладить вложенный функциональный код. используйте его!
[править: в Scala вам нужно «смещаться вправо»Either
s, чтобы заставить их работать сfor
синтаксис. Это делается путем добавления.right
наEither
значения в правой части<-
. Никакого дополнительного импорта не требуется. Это можно сделать внутриonLeft
для более красивого кода. Смотрите также:https: //stackoverflow.com/a/10866844/20825 ]
Как насчет вложенного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
}
}
}
def
работа. На несвязанной ноте я бы предположил, что «все хорошо» код может быть еще одним определением, если это необходимо.
Dan Burton
transID
блокировать дважды? Это было бы плохо, потому что это было быgenerateOrderNum
а такжеprocessPayment
дважды. Возможно, это должен бытьlazy val
вместо этого?
Dan Burton
def
, val
, var
а такжеlazy val
, это правда..
kiritsuku
тов в тех местах, где синтаксически все нормально, но на самом деле приходится выпрыгивать из нескольких методов. Так что вы можете позволить этому сделать это:
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")
}
}
}
(Если вы хотите иметь возможность вкладыватьreturning
s, просто отбросить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
который использует нечто очень похожее на эту стратегию.)
/* All your logic goes in here, including returns */
... Я только что попробовал, и кажется, что явноreturns
внутри именованных локальных функций просто возвращаются из этой функции, а не из вмещающей функции, в отличие от лямбд. Я что-то пропустил? Что-то изменилось?
michiakig
похоже, проблема в том, что вы выполняете бизнес-логику в контроллере, а подписи Play не делают Гм играть хорошо с возвращаемыми значениями, как это вторично.
Я бы порекомендовал вам инкапсулировать вызовы generateOrderNum, processPayment, createMember за фасадом, и это возвращаемое значение может возвращать соответствующее состояние бизнес-транзакции, которое затем можно использовать для возврата надлежащего состояния контроллера.
Скоро обновлю этот ответ примеро
Редактировать: это довольно небрежно, поэтому перепроверьте синтаксис, но суть моего ответа состоит в том, чтобы переместить вашу последовательность бизнес-логики во внешний класс, который будет использовать те или иные / влево / вправо, которые вы уже используете, но теперь включает вашу проверку для пустого идентификатора транзакции в левом ответе.
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))
}
}
Единственное, что немного обманывает - это if / else для bound.hasErrors, но он не уверен в правильном способе сложить это в матч.
Имеет смысл?
Either.cond(transID.isEmpty, dao.createMember(model, ip, orderNum, transID), yadda)
, который выглядит немного лучше, ИМХО.
Landei