Search code examples
scalafunctioncomposition

Keeping intermediate results of function composition


Suppose I've got the following three functions:

 val f1: Int => Option[String] = ???
 val f2: String => Option[Int] = ???
 val f3: Int => Option[Int]    = ???

I can compose them as follows:

 val f: Int => Option[Int] = x =>
   for {
     x1 <- f1(x)
     x2 <- f2(x1)
     x3 <- f3(x2)
   } yield x3

Suppose now that I need to keep the intermediate results of execution f1, f2, f3 and pass them to the caller:

class Result(x: Int) {
  val r1 = f1(x)
  val r2 = r1 flatMap f2
  val r3 = r2 flatMap f3
  def apply = r3 
}

val f: Int => Result = x => new Result(x)

Does it make sense ? How would you improve/simplify this solution ?


Solution

  • Homogenous List

    It's pretty simple for single type, suppose

    val g1: Int => Option[Int] = x => if (x % 2 == 1) None else Some(x / 2)
    val g2: Int => Option[Int] = x => Some(x * 3 + 1)
    val g3: Int => Option[Int] = x => if (x >= 4) Some(x - 4) else None
    

    You can define

    def bind[T]: (Option[T], T => Option[T]) => Option[T] = _ flatMap _
    def chain[T](x: T, fs: List[T => Option[T]]) = fs.scanLeft(Some(x): Option[T])(bind)
    

    And now

    chain(4, g1 :: g2 :: g3 :: Nil)
    

    will be

    List(Some(4), Some(2), Some(7), Some(3))

    preserving all intermediate values.

    Heterogenous List

    But we can do if there are multiple types involved?

    Fortunately there is shapeless library for special structures named Heterogenous List which could handle list-like multi-typed sequences of values.

    So suppose we have

    import scala.util.Try
    
    val f1: Int => Option[String] = x => Some(x.toString)
    val f2: String => Option[Int] = x => Try(x.toInt).toOption
    val f3: Int => Option[Int] = x => if (x % 2 == 1) None else Some(x / 2)
    

    Lets define heterogenous analogues to previous functions:

    import shapeless._
    import ops.hlist.LeftScanner._
    import shapeless.ops.hlist._
    
    object hBind extends Poly2 {
      implicit def bind[T, G] = at[T => Option[G], Option[T]]((f, o) => o flatMap f)
    }
    def hChain[Z, L <: HList](z: Z, fs: L)
                             (implicit lScan: LeftScanner[L, Option[Z], hBind.type]) =
      lScan(fs, Some(z))
    

    And now

    hChain(4, f1 :: f2 :: f3 :: HNil)
    

    Evaluates to

    Some(4) :: Some("4") :: Some(4) :: Some(2) :: HNil

    Class converter

    Now if you urged to save your result in some class like

    case class Result(init: Option[Int], 
                      x1: Option[String], 
                      x2: Option[Int], 
                      x3: Option[Int])
    

    You could easily use it's Generic representation

    just ensure yourself that

    Generic[Result].from(hChain(4, f1 :: f2 :: f3 :: HNil)) == 
      Result(Some(4),Some("4"),Some(4),Some(2))