jon hanson
jon hanson

Reputation: 9408

More idiomatic (monadic?) way to express this Scala

I have several blocks of code that follow this pattern:

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

Where a, b, & c are calculated in turn by the corresponding functions and then the values are passed to the result function. If at any stage an exception occurs then a default value is used in place of the remaining variables.

Is there a more idiomatic way to express this code. It reminds me of Monads in that it's a series of chained computations which bail out immediately if any computation fails.

Upvotes: 8

Views: 2196

Answers (8)

sparker
sparker

Reputation: 1325

I generally solve most try-catch problems using a combination of map + recover or flatMap + recoverWith.

In general, I prefer chaining over nesting

There are many ways to skin a cat. Here are 3:

I presume fA, fB and fC are contrived examples of more complex logic. If they are indeed as noddy as the example then option 3 is simplest.

  1. This one is my preferred way. Readability here is a lot better than nested flatMaps
def test(s: String): Double =
  (for {
    a <- Try(fA(s)).recover { case _ => 0 }
    b <- Try(fB(s, a)) recover { case _ => 0.0 }
    c <- Try(result(a, b, fC(s, a, b))) recover { case _ => result(a, b, false) }
  } yield c).get
  1. This one is the same as the previous one after de-sugaring the for-expressions into flatMaps although it looks more nested than chained.
def test(s: String): Double =
  Try(fA(s)).recover { case _ => 0 }
    .flatMap(a =>
      (Try(fB(s, a)) recover { case _ => 0.0 })
        .flatMap(b =>
          Try(result(a, b, fC(s, a, b))) 
            recover { case _ => result(a, b, false) }
        )
    ).get

Readability is not great because args into flatMap are written as anonymous functions. If I rewrite this without using anon functions then I have:

def test(s: String): Double = {
  val evalA = Try(fA(s)).recover { case _ => 0 }
  val evalB: Int => Try[Double] = a => 
    Try(fB(s, a)) recover { case _ => 0.0 }
  val evalC: (Int, Double) => Try[Double] = (a,b) =>
    Try(result(a, b, fC(s, a, b))) recover { case _ => result(a, b, false) }

  evalA.flatMap(a =>
    evalB(a).flatMap(b =>
      evalC(a, b))
  ).get
}
  1. You could use getOrElse() if no other side-effects required.
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)
}

Upvotes: 2

Heather Miller
Heather Miller

Reputation: 3901

These types of problems are just what Try aims to solve a bit more monadically (than nested try/catch blocks).

Try represents a computation that may either result in an exception, or return a successfully computed value. It has two subclasses for these-- Success and Failure.

Very funny that this question popped up when it did-- a few days ago, I finished up some additions and refactoring to scala.util.Try, for the 2.10 release and this SO question helps to illustrate an important use-case for a combinator that we eventually decided to include; transform.

(As of writing this, transform is currently in the nightly and will be in Scala from 2.10-M5 onward, due out today or tomorrow. More info about Try and usage examples can be found in the nightly docs)

With transform (by nesting them), this can be implemented using Trys as follows:

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
}

Upvotes: 5

Yo Eight
Yo Eight

Reputation: 467

By defining those utility functions

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

and simplifying yours

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

We'll have:

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)

Edit

Here's an executable but sadly, more verbose code snippet

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

Upvotes: 3

tgr
tgr

Reputation: 3608

I changed the example to use 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))))

Note: The for-comprehension is interpreted as chained flatMap. So the type of res is Option[(Int, Double, Boolean)]. Therefore there is no need to write map or flatMap by yourself. The compiler does the work for you. :)

Edit

I edited my code to make it fit to all possibilitys. I will improve it, if I find a better way. Thank you for all your comments.

Upvotes: 4

huynhjl
huynhjl

Reputation: 41646

I'm not sure you can use monads as at each step you have two alternatives (exception or result) and to be faithful to your original code, on exception you don't want to be calling the fB or fC functions.

I was not able to elegantly remove the duplication of default values so I left it as I think it's clearer. Here is my non-monadic version based on either.fold and control.Exception:

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

Upvotes: 5

fanf42
fanf42

Reputation: 1868

The previous answers seem to miss the fact that you want default result at each level. No need to be fancy with for expression here, you just need an helper function:

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

OK, optTry is a bad name (I'm not good at that game), but then, you can just:

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

Notice that Scala 2.10 will have a Try data structure that basically does the same thing with a pimped Either in place of Option, see: http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/index.html#scala.util.Try

Also notice that try { ... } catch { case _ => ... } is a bad idea, you certainly don't want to catch some system Exception like OutOfMemory and the like.

EDIT: also, see Scalaz Validation data structure for a world of awe for all that kind of problems. See: http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/example/ExampleValidation.scala.html

Upvotes: 0

Frank
Frank

Reputation: 10571

You can use the catching idiom as follows:

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

However, similarly to other solutions, this does make a slight executional change in that fB and fC will always be evaluated, whereas your original code only evaluates them if the prior calls succeeded.

Upvotes: 1

incrop
incrop

Reputation: 2738

Tried to make it more functional. Not sure if this solution is clearer than yours, but I think it shoud fit better if you'll have more computation steps.

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

Upvotes: 0

Related Questions