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