Вопрос по playframework-2.0 – Скала: возвращение имеет свое место

12

Рекомендации:
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, совпадет или сложится, что немного уродливо, если отступать для каждого вложенного условия.

Предлагаю создать вопрос из заголовка. Somatik
Я бы не сказал, что это «идиоматично» - избегать возвращения, особенно в таких явно обязательных случаях, как этот, где различные эффекты generateOrderNum, dao.createMember и т. д.) зависит от того, выполняются ли определенные условия. Dan Burton
@ DanBurton, если вы посмотрите на мой обновленный вопрос, вы увидите функциональный эквивалент моего первоначального императивного примера; сгибы встречаются над Либо, частью стандартной библиотеки scala. С операциями персистентности, которые охватывают несколько таблиц, я объединил тип Option Scala с для понимания для выполнения безопасных транзакционных запросов. Переход к функциональным приложениям - это процесс, однако этот вопрос помог мне продвинуться дальше (и отойти от императивных корней) virtualeyes
Несмотря на то, что я сказал, я решил разработать монадическое решение, которое используетLeft значения для вычисления короткого замыкания вместоreturn. Я думаю, что это получилось довольно хорошо, хотя я не достаточно удобен со Scala, чтобы быть в состоянии объявить его 100% -ым звуковым решением. (Я вполне уверен в части Haskell.) Я даже не уверен, где (или если вообще) в стандартных библиотеках Scala подходящий экземпляр монады дляEither может быть найден Dan Burton
@ DanBurton точно, таким образом, return имеет свое место в Scala, но в некоторых случаях не без проблем virtualeyes

Ваш Ответ

4   ответа
26

как Хаскеллер, очевидно, на мой взгляд, решение всего - Монады. Шагните со мной на мгновение в упрощенный мир (то есть упрощенный для меня), где ваша проблема в 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 синтаксис вы можете делать три вещи:

assign от генератора с помощью<- (это сравнимо с @ Хаскел<-) присвоить имя результату вычисления, используя= (это сравнимо с @ Хаскел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 вам нужно «смещаться вправо»Eithers, чтобы заставить их работать сfor синтаксис. Это делается путем добавления.right наEither значения в правой части<-. Никакого дополнительного импорта не требуется. Это можно сделать внутриonLeft для более красивого кода. Смотрите также:https: //stackoverflow.com/a/10866844/20825 ]

+ 10, я узнал кое-что из Haskell (не знал, что стиль Scala читается так же), и вы вышли за рамки служебного долга = здесь стоит галочка virtualeyes
btw, мне нравится, как вы завернули вычисления в для понимания; Я считал это, но предполагал, несколько слепо, что любой сбой приведет к результату None, который, как вы показали, не будет иметь место с помощью возврата «сбой или успех» для условий {}, крутые вещи virtualeyes
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
    }
  }
}
Интересный. Определенно, это хороший выбор для команды, которая хорошо понимает Scala, но я бы с осторожностью попросил Java-программистов, просто изучающих Scala, поддерживать этот код, потому что они, вероятно, не поймут, какdef работа. На несвязанной ноте я бы предположил, что «все хорошо» код может быть еще одним определением, если это необходимо. Dan Burton
@ Antoras, мне не пришло в голову принять внутренний маршрут защиты, +1. Следует отметить, что вложенный if / else код без возврата, с которым я на самом деле работаю, не является просто невыносимым, просто раздражает меня немного увеличить код с отступом для каждого условного virtualeyes
@ DanBurton: я не думаю, что очень сложно понимать методы в методах. kiritsuku
@ Antoras Полагаю, я хотел сказать, что они просто не могут Ноу, и сделайте ложные предположения соответственно. Я сам не слишком знаком со Scala. Подумав об этом коде, разве он не вычисляtransID блокировать дважды? Это было бы плохо, потому что это было быgenerateOrderNum а такжеprocessPayment дважды. Возможно, это должен бытьlazy val вместо этого? Dan Burton
@ DanBurton: Да, это правда. Я этого не видел. Моя вина. Некоторые неявные знания здесь, чтобы отличаться межdef, val, var а такжеlazy val, это правда.. kiritsuku
2

тов в тех местах, где синтаксически все нормально, но на самом деле приходится выпрыгивать из нескольких методов. Так что вы можете позволить этому сделать это:

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

(Если вы хотите иметь возможность вкладыватьreturnings, просто отбросить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 который использует нечто очень похожее на эту стратегию.)

+1 вау, Рекс ... поспи немного ;-) Интересный подход. Я на самом деле pimped util.Control в «catching (операция) вариант» или «catching (операция) либо» с ведением журнала для различных типов исключений. Я снимаю для краткости здесь, что встроенный возврат делает довольно хорошо; в противном случае разбиение платежа на отдельный обработчик (как я уже сделал с членством в DAO), вероятно, является наиболее прямым подходом к этому. Спасибо за "неявный запрос => возвращающий [Foo] {ret =>", хороший обходной путь virtualeyes
Первый блок действительно работает? Бит, который говорит,/* All your logic goes in here, including returns */ ... Я только что попробовал, и кажется, что явноreturns внутри именованных локальных функций просто возвращаются из этой функции, а не из вмещающей функции, в отличие от лямбд. Я что-то пропустил? Что-то изменилось? michiakig
@ spacemanaki - есть толькооди назвал локальную функцию в этом примере, поэтому возврат из нее делает то же самое, что и в последующих примерах. Rex Kerr
о, конечно, спасибо! michiakig
1

похоже, проблема в том, что вы выполняете бизнес-логику в контроллере, а подписи 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
@ 7zark7 верно, это другой вариант, разбивающий код на отдельные обработчики. Вероятно, хороший вызов, это форма регистрации участника, которая, конечно, также потребует продления, что означает, что блок кода платежа не имеет бизнеса, внедряемого непосредственно в signup.save (). Во всяком случае, этот вопрос также касается самого ключевого слова return, которое, глядя на альтернативные ответы, довольно хорошо работает в императивном код virtualeyes
@ 7zark7, чтобы обойти, если / else bound.hasErrors, вы можете свернуть (failresult, successResultWithblock {...}), но должны представить, что сложение со встроенным анонимным блоком может привести к очень интересному байт-коду; -) virtualeyes
на самом деле не работает, кстати, создание аккаунта не должно существовать в вашем платежном сервисе; как насчет продления членства? В любом случае, Scala и Play рок, но вам приходится сталкиваться с такими крайними случаями, как это, что заставляет вас действительно думать, чего я стараюсь избегать, когда это возможн virtualeyes
Может быть, я педантичен - но независимо от фреймворка, я отвлекаю логику типа if / else от веб-слоя. Например. Что делать, если вы устали от Play и хотите перейти к чему-то другому, например, к Scalatra? В этом случае логика бизнеса может использоваться повторно: -) 7zark7

Похожие вопросы