Scala: return has its place

前端 未结 4 1767
北荒
北荒 2020-12-14 11:14

References:
Scala return keyword
handling errors in scala controllers

EDIT3
This is the \"final\" solution, again thanks to Dan Burton.<

相关标签:
4条回答
  • 2020-12-14 11:29

    What about some nested 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
        }
      }
    }
    
    0 讨论(0)
  • 2020-12-14 11:29

    Scala internally uses the throw/catch mechanism to handle returns in places where returns are syntactically okay but it actually has to jump out of several methods. So you can either let it do this:

    def save = Action { implicit request =>
      def result(): Foo = {
        /* All your logic goes in here, including returns */
      }
      result()
    }
    

    or, if you prefer, you can use your own data-passing throwable class (without stack trace):

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

    Or you could get a little fancier and add your own exception handling:

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

    (If you want to be able to nest the returnings, just rethrow the Return instead of throwing an IllegalArgumentException when the type doesn't match.) You can use this like so:

    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"
    

    or in your particular case

    def save = Action{ implicit request => returning[Foo] { ret =>
      /* Logic goes here, using ret(foo) as needed */
    }}
    

    It's not built in, but it shouldn't be terribly hard to explain to people how to use this even if it's not so easy to understand how the capability is built. (Note: Scala does have built in break capability in scala.util.control.Breaks which uses something very much like this strategy.)

    0 讨论(0)
  • 2020-12-14 11:35

    IMHO, seems the problem here is that you are executing business logic in a controller, and Play signatures don't ahem play nice with return values like this is secondary.

    I'd recommend you incapsulate the generateOrderNum, processPayment, createMember calls behind a facade, and that return value can return the appropriate state of the business transaction, which can then be used to return the proper controller state.

    Will update this answer with an example in a bit.

    Edit: This is pretty sloppy so double-check the syntax, but the gist of my answer is to move your business logic sequence into an external class which will leverage the Either/Left/Right you are already using, but now includes your check for empty Transaction ID in the Left response.

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

    The only thing a little hokey here is the if/else for bound.hasErrors, but not sure of a clean way to fold that into the match.

    Make sense?

    0 讨论(0)
  • 2020-12-14 11:42

    So as a Haskeller, obviously in my mind, the solution to everything is Monads. Step with me for a moment into a simplified world (simplified for me, that is) where your problem is in Haskell, and you have the following types to deal with (as a Haskeller, I sort of have this fetish for types):

    bindFormRequest :: Request -> Form -> BoundForm
    hasErrors :: BoundForm -> Bool
    
    processPayment :: Model -> OrderNum -> TransID
    isEmpty :: TransID -> Bool
    

    Let's pause here. At this point, I'm sort of cringing a bit at boundFormHasErrors and transIDisEmpty. Both of these things imply that the possibility of failure is injected into BoundForm and TransID respectively. That's bad. Instead, the possibility of failure should be maintained separate. Allow me to propose this alternative:

    bindFormRequest :: Request -> Form -> Either FormBindError BoundForm
    processPayment :: Model -> OrderNum -> Either TransError TransID 
    

    That feels a bit better, and these Eithers are leading into the use of the Either monad. Let's write up some more types though. I'm going to ignore OK because that is wrapped around pretty much everything; I'm fudging a little bit but the concepts will still translate just the same. Trust me; I'm bringing this back around to Scala in the end.

    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
    

    OK, now I'm going to do something a bit wonky, and let me tell you why. The Either monad works like this: as soon as you hit a Left, you stop. (Is it any surprise I chose this monad to emulate early returns?) This is all well and good, but we want to always stop with an Action, and so stopping with a FormBindError isn't going to cut it. So let's define two functions that will let us deal with Eithers in such a way that we can install a little more "handling" if we discover a 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
    

    At this point, in Haskell, I would talk about monad transformers, and stacking EitherT on top of IO. However, in Scala, this is not a concern, so wherever we see IO Foo, we can just pretend it is a Foo.

    Alright, let's write save. We will use do syntax, and later will translate it to Scala's for syntax. Recall in for syntax you are allowed to do three things:

    • assign from a generator using <- (this is comparable to Haskell's <-)
    • assign a name to the result of a computation using = (this is comparable to Haskell's let)
    • use a filter with the keyword if (this is comparable to Haskell's guard function, but we won't use this because it doesn't give us control of the "exceptional" value produced)

    And then at the end we can yield, which is the same as return in Haskell. We will restrict ourselves to these things to make sure that the translation from Haskell to Scala is smooth.

    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
    

    Notice something? It looks almost identical to the code you wrote in imperative style!

    You may be wondering why I went through all this effort to write up an answer in Haskell. Well, it's because I like to typecheck my answers, and I'm rather familiar with how to do this in Haskell. Here's a file that typechecks, and has all of the type signatures I just specified (sans IO): http://hpaste.org/69442

    OK, so now let's translate that to Scala. First, the Either helpers.

    Here begins the Scala

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

    Now, the save method

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

    Note that variables on the left hand side of <- or = are implicitly considered to be vals since they are inside of a for block. You should feel free to change onLeft so that it is pimped onto Either values for prettier usage. Also, make sure you import an appropriate "Monad instance" for Eithers.

    In conclusion, I just wanted to point out that the whole purpose of monadic sugar is to flatten out nested functional code. So use it!

    [edit: in Scala, you have to "right bias" Eithers to make them work with for syntax. This is done by adding .right to the Either values on the right-hand side of the <-. No extra imports necessary. This could be done inside of onLeft for prettier-looking code. See also: https://stackoverflow.com/a/10866844/208257 ]

    0 讨论(0)
提交回复
热议问题