更加惯用(monadic?)方式来表达这个Scala

时间:2012-06-27 05:33:23

标签: scala monads

我有几个遵循这种模式的代码块:

// Dummy function defs.
def result(i : Int, d : Double, b : Boolean) = {
    if (b) d else i
}

def fA(s : String) = {7}
def fB(s : String, i : Int) = {1.0}
def fC(s : String, i : Int, d : Double) = {true}

// Actual code.
def test(s : String) : Double = {
    try {
        val a = fA(s) 
        try {
            val b = fB(s, a)
            try {
                val c = fC(s, a, b)
                result(a, b, c)
            } catch {
                case _ => result(a, b, false)
            }

        } catch {
            case _ => result(a, 0.0, false)
        }
    } catch {
        case _ => result(0, 0.0, false)
    }
}

a,b,& c由相应的函数依次计算,然后将值传递给结果函数。如果在任何阶段发生异常,则使用默认值代替剩余变量。

是否有更惯用的方式来表达此代码。它让我想起Monads,因为它是一系列链式计算,如果任何计算失败,它会立即纾困。

8 个答案:

答案 0 :(得分:5)

我不确定你是否可以使用monad,因为在每一步你有两个选择(例外或结果)并且忠实于原始代码,例外你不想调用fBfC函数。

我无法优雅地删除默认值的重复,所以我离开了它,因为我认为它更清晰。以下是基于either.foldcontrol.Exception的非monadic版本:

def test(s : String) = {
  import util.control.Exception._
  val args = 
    allCatch.either(fA(s)).fold(err => (0, 0.0, false), a => 
      allCatch.either(fB(s, a)).fold(err => (a, 0.0, false), b =>
        allCatch.either(fC(s, a, b)).fold(err => (a, b, false), c =>
          (a, b, c))))
  (result _).tupled(args)
}

答案 1 :(得分:5)

这些类型的问题正是Try旨在解决一些更多的问题(比嵌套try/catch块)。

Try表示可能导致异常或返回成功计算值的计算。它有两个子类 - SuccessFailure

非常有趣的是,这个问题突然出现了 - 几天前,我完成了一些补充和重构到scala.util.Try,对于2.10版本,这个SO问题有助于说明一个重要的用例对于我们最终决定包括的组合器; transform

(截至撰写本文时,transform目前位于nightly,并且将于今天或明天发布的2.10-M5之后的Scala中。有关Try和使用情况的更多信息示例可以在nightly docs

中找到

使用transform(通过嵌套),可以使用Try实现,如下所示:

def test(s: String): Double = {
  Try(fA(s)).transform(
    ea => Success(result(0, 0.0, false)), a => Try(fB(s, a)).transform(
      eb => Success(result(a, 0.0, false)), b => Try(fC(s, a, b)).transform(
        ec => Success(result(a, b, false)), c => Try(result(a, b, c))
      )
    )
  ).get
}

答案 2 :(得分:4)

我将示例更改为使用monads:

def fA(s: String) = Some(7)
def fB(i: Option[Int]) = Some(1.0)
def fC(d: Option[Double]) = true // might be false as well

def result(i: Int, d: Double, b: Boolean) = {
  if (b) d else i
}

def test(s: String) = result(fA(s).getOrElse(0), fB(fA(s)).getOrElse(0.0), fC(fB(fA(s))))

注意:for-comprehension被解释为链式flatMap。因此res的类型为Option[(Int, Double, Boolean)]。因此,您无需自己编写mapflatMap。编译器为您完成工作。 :)

修改

我编辑了我的代码以使其适合所有可能性。如果我找到更好的方法,我会改进它。感谢您的所有意见。

答案 3 :(得分:2)

通过定义那些效用函数

implicit def eitherOps[E, A](v: Either[E, A]) = new {
  def map[B](f: A => B) = v match {
    case Left(e)  => Left(e)
    case Right(a) => Right(f(a))    
  }

  def flatMap[B](f: A => Either[E, B]) = v match {
    case Left(e)  => Left(e)
    case Right(a) => f(a)
  }

  def or(a: A) = v match {
    case Left(_) => Right(a)
    case x       => x          
  }
}

def secure[A, B](f: A => B) = new {
  def run(a: A): Either[Trowable, B]  = try {
    Right(f(a))
  } catch {
    case e => Left(e)
  }
}

并简化你的

def fA(s : String) = 7
def fB(i : Int) = 1.0
def fC(d : Double) = true

我们将:

def test(s: String): Either[Throwable, Double] =  for {
  a <- secure(fA).run(s).or(0)
  b <- secure(fB).run(a).or(0.0)
  c <- secure(fC).run(b).or(false)
} yield result(a, b, c)

修改

这是一个可执行文件,但遗憾的是,更详细的代码段

object Example {
  trait EitherOps[E, A] {
    def self: Either[E, A]

    def map[B](f: A => B) = self match {
      case Left(e)  => Left(e)
      case Right(a) => Right(f(a))    
    }

    def flatMap[B](f: A => Either[E, B]) = self match {
      case Left(e)  => Left(e)
      case Right(a) => f(a)
    }

    def or(a: A) = self match {
      case Left(_) => Right(a)
      case x       => x          
    }
  }

  trait SecuredFunction[A, B] {
    def self: A => B

    def secured(a: A): Either[Throwable, B]  = try {
      Right(self(a))
    } catch {
      case e => Left(e)
    }
  }

  implicit def eitherOps[E, A](v: Either[E, A]) = new EitherOps[E, A] {
    def self = v
  }

  implicit def opsToEither[E, A](v: EitherOps[E, A]) = v.self

  implicit def secure[A, B](f: A => B) = new SecuredFunction[A, B]{
    def self = f
  }

  def fA(s : String) = 7
  def fB(i : Int) = 1.0
  def fC(d : Double) = true

  def result(i : Int, d : Double, b : Boolean) = {
    if (b) d else i
  }

  def test(s: String): Either[Throwable, Double] =  for {
    a <- (fA _).secured(s) or 0
    b <- (fB _).secured(a) or 0.0
    c <- (fC _).secured(b) or false
  } yield result(a, b, c)
}

答案 4 :(得分:1)

您可以使用catching惯用法,如下所示:

import scala.util.control.Exception._

def test(s : String) : Double = result(
  catching(classOf[Exception]).opt( fA(s) ).getOrElse(0),
  catching(classOf[Exception]).opt( fB(s, a) ).getOrElse(0.0),
  catching(classOf[Exception]).opt( fC(s, a, b) ).getOrElse(false)
)

然而,与其他解决方案类似,这确实会导致轻微的执行更改fBfC将始终进行评估,而原始代码仅在先前调用成功时才对其进行评估。

答案 5 :(得分:0)

试图让它更具功能性。 不确定这个解决方案是否比你的解决方案更清晰,但我认为如果你有更多的计算步骤,它会更合适。

def result(i : Int, d : Double, b : Boolean) = {
    if (b) d else i
}

def fA(s : String) = {7}
def fB(s : String, i : Int) = {1.0}
def fC(s : String, i : Int, d : Double) = {true}

type Data = (Int, Double, Boolean)
def test(s : String) : Double = {
  val steps = Seq[Data => Data](
    {case (_, b, c) => (fA(s), b, c)},
    {case (a, _, c) => (a, fB(s, a), c)},
    {case (a, b, _) => (a, b, fC(s, a, b))}
  )
  val defaults: Either[Data, Data] = Right((0, 0.0, false))
  val resultData = steps.foldLeft { defaults } { (eith, func) =>
    eith match {
      case left: Left[_,_] => left
      case Right(data) => try {
        Right(func(data))
      } catch {
        case _ => Left(data)
      }
    }
  } fold (identity, identity)
  (result _) tupled (resultData)
}

答案 6 :(得分:0)

之前的答案似乎错过了您希望每个级别的默认结果的事实。 这里不需要花哨的表达,你只需要一个辅助函数:

def optTry[T]( f: => T) : Option[T] = try { Some(f) } catch { case e:Exception => None }

好的,optTry是一个坏名字(我不擅长那个游戏),但是,你可以这样做:

def test(s : String) : Double = {
  val a = optTry(fA(s)) getOrElse 0
  val b = optTry(fB(s,a)) getOrElse 0.0
  val c = optTry(fC(s,a,b)) getOrElse false

  result(a,b,c)
}

请注意,Scala 2.10将具有Try数据结构,基本上用一个拉皮条Either代替Option执行相同的操作,请参阅:http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/index.html#scala.util.Try

还要注意try { ... } catch { case _ => ... }是一个坏主意,你当然不想捕获像OutOfMemory之类的系统异常。

编辑:另外,请参阅Scalaz Validation数据结构,了解所有类型的问题。请参阅:http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html

答案 7 :(得分:0)

我发现大多数尝试捕获问题都可以通过结合使用map + recoverflatMap + recoverWith来解决。优先使用链接表达式而不是嵌套表达式。

我通过两种不同的方式进行了尝试:

第一个假设您需要嵌套,并且fA,fB,fC,实际上比您的示例更复杂;尽管其中一个更现实的示例将有助于为此提供一个简洁明了的解决方案并编写一些测试用例

def test(s: String): Double =
  Try (fA(s)) flatMap (a =>
    Try (fB(s, a)) flatMap (b => 
      Try (result(a, b, fC(s, a, b))) recover { case _ => result(a, b, false) }
      ) recover { case _ => result(a, 0.0, false) }
    ) getOrElse result(0, 0.0, false)

如果fA,fB,fC的结果确实像它们一样点头,那么我们可以为该IMO提供更简单的解决方案。如果它们在处理异常方面没有任何副作用,则可以执行getOrElse如果它们确实具有诸如记录异常的副作用,则可以使用recoverrecoverWith < / p>

def test(s: String) = {
  val evalA = Try(fA(s)).getOrElse(0)
  val evalB = Try(fB(s, evalA)).getOrElse(0.0)
  val evalC = Try(fC(s, evalA, evalB)).getOrElse(false)

  result(evalA, evalB, evalC)
}