Scala: возвращение имеет свое место
Литература:
Scala ключевое слово return
обработка ошибок в контроллерах 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'd Любой метод проекта, помещенный где-то в ваше приложение (в моем случае, свойство implicits, что 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)
EDIT2
Evolution, перешли от встроенных возвратных операторов к этому маленькому белому карлику плотности (kudos to @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)
...
}
Я добавил Dan onLeft. Или проецирование в качестве сутенера, либо с помощью вышеописанного метода "project", который допускает правое смещение eitherResult project(left-outcome)
. В основном вы получаете ошибку с ошибкой как левый и успех как правую, то, что не сработает при подаче результатов Варианта для понимания (вы получаете только результат Some/None).
Единственное, с чем я не в восторге, - это указать тип для project(Conflict(param))
; Я думал, что компилятор сможет вывести тип левого состояния из Лица, который передается ему: видимо, нет.
Во всяком случае, ясно, что функциональный подход устраняет необходимость в встроенных операторах 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
)}
)}
)
который, безусловно, представляет собой плотно упакованный блок кода, много чего там происходит; Тем не менее, я бы сказал, что соответствующий императивный код со встроенными результатами не только аналогичен кратким, но и более легким для поиска (с дополнительным преимуществом меньшего количества конических завитушек и парсеров для отслеживания)
ОРИГИНАЛ
Нахождение себя в императивной ситуации; хотел бы увидеть альтернативный подход к следующему (который не работает из-за использования ключевого слова 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, или сгибов, или совпадений, или незаполненного неинвазивного подхода. Проблема, конечно, в том, что она не работает, должен указываться явный тип возвращаемого значения, который имеет свои собственные проблемы, поскольку мне еще предстоит выяснить, как указать тип, который удовлетворяет любой магии игры, - нет, def save: Result
, не работает, поскольку компилятор тогда жалуется на implicit result
, теперь не имеющий явного типа; - (
Во всяком случае, примеры Framework для игры обеспечивают la, la, la, la happy 1-shot-deal fold (ошибка, успех), которое не всегда имеет место в реальном мире и торговле;; -)
Итак, каков идиоматический эквивалент (без использования возврата) на предыдущий блок кода? Я предполагаю, что он будет вложен, если /else, match или fold, который становится немного уродливым, отступы с каждым вложенным условием.
Ответы
Ответ 1
Так как Хаскеллер, очевидно, на мой взгляд, решение для всех - это Монады. Подойдите ко мне на мгновение в упрощенном мире (упрощенном для меня, то есть), где ваша проблема находится в 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
, потому что это обернуто вокруг почти всего; Я немного притворяюсь, но концепции все равно будут переведены точно так же. Доверьтесь мне; В конце концов я вернусь к 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
Хорошо, теперь я собираюсь сделать что-то немного отвратительное, и позвольте мне сказать вам, почему. Либо монада работает так: как только вы нажмете 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
В этот момент, в Haskell, я хотел бы поговорить о монадных трансформаторах и укладке EitherT
поверх IO
. Однако в Scala это не вызывает беспокойства, поэтому везде, где мы видим IO Foo
, мы можем просто притвориться, что это Foo
.
Хорошо, напишите save
. Мы будем использовать синтаксис do
, а позже переведем его в синтаксис Scala
for
. Напомним, что в синтаксисе for
вам разрешено делать три вещи:
- назначить из генератора с помощью
<-
(это сопоставимо с Haskell <-
)
- присвойте имя результату вычисления с помощью
=
(это сопоставимо с Haskell let
)
- используйте фильтр с ключевым словом
if
(это сопоставимо с функцией Haskell guard
, но мы не будем использовать это, потому что оно не дает нам контроль над созданным "исключительным" значением)
И тогда в конце мы можем yield
, что совпадает с return
в Haskell. Мы ограничимся этими вещами, чтобы убедиться, что перевод из 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. Ну, это потому, что мне нравятся typecheck мои ответы, и я довольно хорошо знаю, как это сделать в Haskell. Вот файл, который typechecks и имеет все подписи типов, которые я только что указал (sans IO
): http://hpaste.org/69442
ОК, так что теперь переведите это на Scala. Во-первых, помощники Either
.
Здесь начинается Scala
// be careful how you use this.
// Scala 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.
В заключение я просто хотел указать, что цель монадического сахара заключается в том, чтобы сгладить вложенный функциональный код. Так что используйте его!
[edit: in Scala, вам нужно "правое смещение" Either
, чтобы заставить их работать с синтаксисом for
. Это делается добавлением .right
к значениям Either
в правой части <-
. Никаких дополнительных импортных поставок не требуется. Это можно сделать внутри onLeft
для более красивого кода. См. Также: fooobar.com/questions/159621/...]
Ответ 2
Как насчет некоторых вложенных 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
}
}
}
Ответ 3
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")
}
}
}
(Если вы хотите вставить 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
, которая использует что-то очень похожее на эту стратегию.)
Ответ 4
IMHO, похоже, проблема заключается в том, что вы выполняете бизнес-логику в контроллере, а Play-сигнатуры не играют в игру с хорошими значениями возврата, такими как вторичные.
Я бы порекомендовал вам инкапсулировать
generateOrderNum,
processPayment,
createMember
вызывает за фасадом, и это возвращаемое значение может вернуть соответствующее состояние бизнес-транзакции, которое затем может быть использовано для возврата правильного состояния контроллера.
Немного обновит этот ответ с помощью примера.
Изменить:
Это довольно неряшливо, поэтому дважды проверьте синтаксис, но суть моего ответа состоит в том, чтобы переместить последовательность бизнес-логики во внешний класс, который будет использовать A/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))
}
}
Единственное, что немного hokey здесь, это if/else для bound.hasErrors, но не уверенный в чистом способе свернуть это в соответствие.
Имеют смысл?