Подтвердить что ты не робот

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

4b9b3361

Ответ 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, но не уверенный в чистом способе свернуть это в соответствие.

Имеют смысл?