【问题标题】:Chaining method calls with Either使用 Either 链接方法调用
【发布时间】:2012-08-19 21:24:55
【问题描述】:

我想知道是否可以创建某种“方法调用链”,所有方法都返回相同的 Either[Error,Result]。

我要做的是:依次调用所有方法,当方法返回Left(Error)时,停止方法调用并返回调用链中找到的第一个Left。

我尝试了一些东西,包括折叠、地图、投影……但我是 Scala 新手,找不到任何优雅的解决方案。

我尝试过这样的事情:

def createUserAndMandatoryCategories(user: User) : Either[Error,User] = {
    User.create(user).right.map {
      Logger.info("User created")
      Category.create( Category.buildRootCategory(user) ).right.map {
        Logger.info("Root category created")
        Category.create( Category.buildInboxCategory(user) ).right.map {
          Logger.info("Inbox category created")
          Category.create( Category.buildPeopleCategory(user) ).right.map {
            Logger.info("People category created")
            Category.create( Category.buildTrashCategory(user) ).right.map {
              Logger.info("Trash category created")
              Logger.info("All categories successfully created created")
              Right(user)
            }
          }
        }
      }
    }
  }

但它不起作用。 无论如何,我真的不喜欢它需要的缩进。 此外,我想将错误转换为描述问题的新字符串(我想我应该使用折叠?)

我正在寻找这样写的东西:

val result : Either[String,CallResult] = call1.something("error 1 description")
.call2.something("error 2 description")
.call3.something("error 3 description")
.call4.something("error 4 description")

Scala 可以做这样的事情吗?也许同时使用 Either 和 Option?

还有一个限制是,如果第一次调用失败,则不应进行其他调用。我不想要一个我调用所有东西然后加入其中一个的解决方案。

谢谢!

【问题讨论】:

  • 嗨,你正在寻找来自 scalaz 的特征验证
  • 这是 Either monad 的描述。

标签: scala functional-programming monads either


【解决方案1】:

有更好、更实用的方法可以做到这一点(主要涉及 Scalaz 的验证和遍历/序列),但您的代码大致相当于:

def createUserAndMandatoryCategories(user: User) : Either[Error,User] = for {
  _ <- User.create(user).right.map(Logger.info("User created")).right
  _ <- Category.create( Category.buildRootCategory(user) ).right.map(Logger.info("Root category created")).right
  _ <- Category.create( Category.buildInboxCategory(user) ).right.map(Logger.info("Inbox category created")).right
} yield user

这至少摆脱了所有的嵌套。由于 Scala 的 Either 默认不是右偏的,因此您必须手动指定很多次,这会降低可读性。

【讨论】:

【解决方案2】:

您已经在使用的RightProjection 允许您使用它的flatMap 方法来做您需要的事情。

(按照惯例,计算结果存储在Right 中,计算失败的错误值存储在Left 中。但没有其他原因,您可以对LeftProjection 执行相同的操作。)

实际上,我们这里有RightProjection 形成一个monad。您可以使用Right(x).right 将值x 转换为投影。如果您有一个投影p,您可以通过调用p.flatMap(f)p 上应用一个可能失败的计算f。这样,您可以链接多个这样的方法。

这可以通过for 理解进一步简化。举个完整的例子:

object EitherTest extends App {
  // we define some methods that can either fail 
  // and return a String description of the error,
  // or return a value

  def sqrt(x: Double): Either[String,Double] =
    if (x >= 0) Right(math.sqrt(x));
    else Left("Negative value " + x + " cannot be square-rooted.");

  // or you could have, if you want to avoid typing .right inside `for` later
  def sqrt0(x: Double): Either.RightProjection[String,Double] =
    ( if (x >= 0) Right(math.sqrt(x));
      else Left("Negative value " + x + " cannot be square-rooted.")
    ).right;

  def asin(x: Double): Either[String,Double] =
    if (x > 1) Left("Too high for asin")
    else if (x < -1) Left("Too low for asin")
    else Right(math.asin(x));


  // Now we try to chain some computations.
  // In particular, we'll be computing sqrt(asin(x)).
  // If one of them fails, the rest will be skipped
  // and the error of the failing one will be returned
  // as Left.

  { // try some computations
    for(i <- -5 to 5) {
      val input: Double = i / 4.0;
      val d: Either[String,Double] = Right(input);
      val result: Either[String,Double] =
        for(v <- d.right;
            r1 <- asin(v).right;
            r2 <- sqrt(r1).right
            // or you could use:
            // r2 <- sqrt0(r1)
        ) yield r2;
      println(input + "\t->\t" + result);
    }
  }
}

输出是:

-1.25       ->      Left(Too low for asin)
-1.0        ->      Left(Negative value -1.5707963267948966 cannot be square-rooted.)
-0.75       ->      Left(Negative value -0.848062078981481 cannot be square-rooted.)
-0.5        ->      Left(Negative value -0.5235987755982989 cannot be square-rooted.)
-0.25       ->      Left(Negative value -0.25268025514207865 cannot be square-rooted.)
0.0         ->      Right(0.0)
0.25        ->      Right(0.5026731096270007)
0.5         ->      Right(0.7236012545582677)
0.75        ->      Right(0.9209028607738609)
1.0         ->      Right(1.2533141373155001)
1.25        ->      Left(Too high for asin)

【讨论】:

    【解决方案3】:

    Debilski 在实现功能方面有“最佳”答案,但我会用一些帮助代码进一步精简它:

    // trait PackageBase (applicable package objects extend)
    /*
     * not used in this example but can use below implicit to do something like:
     * for { x <- eitherResult as json }
     */
    class RightBiasedEither[A,B](e: Either[A,B]) {
      def as[A1](f: A => A1) = e match {
        case Left(l)    => Left(f(l)).right
        case Right(r) => Right(r).right
      }
    }
    @inline implicit final def either2Projection[L,R](e: Either[L,R]) = new RightBiasedEither(e)
    
    class Catching[T](f: => T) extends grizzled.slf4j.Logging {
      def either(msg: String) = { // add your own logging here
        try { Right(f).right }
        catch { case e: Exception => error( e.getMessage ); Left(msg).right }
      }
    }
    def catching[T](f: => T) = new Catching(f)
    
    // in your query wrapper equivalent
    protected def either[T](result: => T, msg: String)(implicit ss: Session) = {
      catching(result) either(msg)
    }
    
    // and then your DAO create methods will do something like:
    def create(foo: Foo)(implicit ss: Session) {
      either[Int]( Foos.insert(foo), i18n("not created") )
    }
    
    // with the above code you can then strip things down to:
    def createUserAndMandatoryCategories(user: User) : Either[Error,User] = {
      db.handle withSession { implicit ss: Session =>
        ss.withTransaction { 
          val result = for {
            _ <- User.create(user)
            _ <- Category.create( Category.buildRootCategory(user) )
            _ <- Category.create( Category.buildInboxCategory(user) )
          } yield user
          result fold ( e => { ss.rollback; Left(e) }, u => Right(u) )
        }
      }
    }
    

    在我看来,没有必要记录成功的创建事件(只有失败),因为整个事务在失败时回滚,但是 YMMV,根据需要添加日志记录。

    【讨论】:

    • 谢谢会检查,但我不明白你的代码的一切:)。顺便说一句,我不需要记录,只是添加了它,因为我无法使调用链返回适当的错误消息
    • 正如每个人都指出的那样,您需要一个 Either 投影才能 for{...} 通过一组计算。这是一种方法。所有查询隐式会话的东西都是ScalaQuery,顺便说一句。对于此示例,您不需要隐式 RightBiasedEither 类,但如果您想对 Left 错误条件 "val result = for{ x 进行编码,它会派上用场
    猜你喜欢
    • 1970-01-01
    • 2021-09-08
    • 2017-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-04
    • 1970-01-01
    • 2013-11-16
    相关资源
    最近更新 更多