13

参考資料:
ScalaコントローラーでのScalaリターンキーワード
処理エラー

EDIT3
これも「最終的な」解決策です。これもDanBurtonのおかげです。

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 whichプロジェクトメソッド(私の場合、sbtルートと子プロジェクトがベースパッケージオブジェクトを拡張する暗黙の特性:

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は、埋め込まれたreturnステートメントから、この小さな白色矮星の密度に移行しました(kudosから@DanBurton、Haskellラスカル;-))

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

ダンのonLeftEitherプロジェクションをEitherにポン引きとして追加しました。上記の「プロジェクト」メソッドを使用すると、右バイアスが可能になりますeitherResult project(left-outcome)。基本的に、フェイルファーストエラーは左として、成功は右として取得されます。これは、オプションの結果を理解のためにフィードする場合には機能しません(一部/なしの結果のみが取得されます)。

私がわくわくしないのは、project(Conflict(param));のタイプを指定する必要があることだけです。コンパイラーは、渡されているEitherから左の条件タイプを推測できると思いました。明らかにそうではありません。

いずれにせよ、私がif / else命令型アプローチでやろうとしていたように、機能的アプローチによって組み込みのreturnステートメントが不要になることは明らかです。

編集
機能的に同等のものは次のとおりです。

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

この場合、いくつかのif / elseブロック、フォールド、マッチ、または空白を埋める非命令型アプローチをネストしないようにするため、returnの使用が好きです。もちろん、問題はそれが機能しないことです。明示的な戻り型を指定する必要があります。これには、Playマジックが機能しているものをすべて満たす型を指定する方法がまだわからないため、独自の問題があります。def save: Result、コンパイラimplicit resultが明示的な型を持たないことについて文句を言うので機能しません;-(

とにかく、Playフレームワークの例は、la、la、la、laの幸せなワンショットディールフォールド(エラー、成功)条件を提供しますが、これは現実の世界では常に当てはまるとは限りません™;-)

では、上記のコードブロックと(returnを使用せずに)慣用的に同等なものは何ですか?if / else、match、またはfoldはネストされていると思いますが、ネストされた条件ごとにインデントされて少し見苦しくなります。

4

4 に答える 4

27

ですから、ハスケラーとして、明らかに私の心の中では、すべての解決策はモナドです。あなたの問題がHaskellにある単純化された世界(私にとっては単純化された)に少し足を踏み入れてください、そしてあなたは以下のタイプに対処する必要があります(Haskellerとして、私はタイプのためにこのフェチを持っています):

bindFormRequest :: Request -> Form -> BoundForm
hasErrors :: BoundForm -> Bool

processPayment :: Model -> OrderNum -> TransID
isEmpty :: TransID -> Bool

ここで一時停止しましょう。この時点で、私はとに少しうんざりしていboundFormHasErrorsますtransIDisEmpty。これらのことは両方とも、失敗の可能性がそれぞれとに注入されることを意味します 。それは良くないね。代わりに、障害の可能性を別に維持する必要があります。この代替案を提案させてください。BoundFormTransID

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ので、aで停止しても効果FormBindErrorはありません。それでは、を発見した場合にもう少し「処理」をインストールできるように、Eithersを処理できるようにする2つの関数を定義しましょう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では、モナド変換子とその上に積み重ねることについて話します。ただし、Scalaでは、これは問題ではないため、どこを見ても、それがであるかのように見せかけることができます。EitherTIOIO FooFoo

さて、書きましょうsave。構文を使用し、後でそれをの構文doに変換します。構文で、次の3つのことを実行できることを思い出してください。Scalaforfor

  • を使用してジェネレータから割り当て<-ます(これはHaskellのものに相当します<-
  • を使用して計算結果に名前を割り当てます=(これはHaskellのものに相当しますlet
  • キーワードでフィルターを使用しますif(これはHaskellの関数に相当しguardますが、生成される「例外的な」値を制御できないため、これは使用しません)

そして最後に、Haskellyieldと同じようにできます。returnHaskellから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で答えを書くためにこのような努力をしたのか不思議に思うかもしれません。ええと、それは私が自分の答えをタイプチェックするのが好きだからです、そして私はHaskellでこれを行う方法にかなり精通しています。これがタイプチェックを行うファイルで、指定したすべてのタイプシグネチャが含まれています(sans IO): http: //hpaste.org/69442

では、それを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) ) }

<-またはの左側にある変数は、ブロック内にあるため、=暗黙的にsと見なされることに注意してください。よりきれいに使用するための値に反映されるように、自由に変更する必要があります。また、sに適切な「モナドインスタンス」をインポートしてくださいvalforonLeftEitherEither

結論として、モナディックシュガーの全体的な目的は、ネストされた関数型コードをフラット化することであるということを指摘したいと思います。だからそれを使ってください!

Either[編集:Scalaでは、構文で機能させるために「正しいバイアス」をかける必要がありforます。これは、の右側.rightの値に追加することによって行われます。追加のインポートは必要ありません。これは、見栄えの良いコードの内部で行うことができます。参照:https ://stackoverflow.com/a/10866844/208257 ]Either<-onLeft

于 2012-06-02T22:21:09.203 に答える
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
    }
  }
}
于 2012-06-01T22:35:50.793 に答える
2

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

(sをネストできるようにしたい場合は、タイプが一致しないときにをスローするのではなくreturning、単に再スローします。)次のように使用できます。ReturnIllegalArgumentException

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

于 2012-06-02T04:08:56.940 に答える
1

IMHO、ここでの問題は、コントローラーでビジネスロジックを実行していることであり、Playシグネチャは、このような2次的な戻り値ではうまく機能しないようです。

ファサードの背後にあるgenerateOrderNum、processPayment、createMember呼び出しをカプセル化することをお勧めします。その戻り値は、ビジネストランザクションの適切な状態を返すことができ、これを使用して適切なコントローラーの状態を返すことができます。

この回答を少し例を挙げて更新します。

編集:これはかなりずさんなので、構文を再確認してください。しかし、私の答えの要点は、ビジネスロジックシーケンスを、すでに使用しているEither / 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))
  }
}

ここで少し厄介なのは、bound.hasErrorsのif / elseだけですが、それを一致に折りたたむためのクリーンな方法はわかりません。

わかる?

于 2012-06-02T00:05:36.787 に答える