Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid stair-stepping with Monad Transformers in scala?

I have the following code that uses the Reader monad for configuration and also has to deal with IO[Option[String]] and I've ended up with code that stair-steps in my encode function.

How can I formulate a monad transformer for Reader and OptionT to avoid the ugly nested for comprehensions in my encode function?

def encode(fileName: String): Reader[Config, IO[Unit]] = for {
   ffmpegWrapper <- findFfmpegWrapper
   ffmpegBin <- findFfmpeg
} yield (for {
    w <- ffmpegWrapper
    b <- ffmpegBin
    stream <- callFfmpeg(getCommand(w, b, fileName)).liftM[OptionT]
} yield stream) map (_ foreach (println)) getOrElse Unit.box {}


def getCommand(ffmpegWrapper: String, ffmpegBin: String,
             videoFile: String) = s"$ffmpegWrapper $ffmpegBin $videoFile  '-vcodec libx264 -s 1024x576' /tmp/out.mp4"

def callFfmpeg(command: String): IO[Stream[String]] = IO {
  Process(command).lines_!
}

def findFile(path:List[String]): OptionT[IO,String] = OptionT[IO,String](IO{path.find(new File(_).exists)})

def findFfmpeg:Reader[Config, OptionT[IO,String]] = Reader {c=>findFile(c.ffmpegLocations)}

def findFfmpegWrapper:Reader[Config, OptionT[IO,String]] = Reader {c=>findFile(c.ffmpegWrapperLocations)}

Thank-you!

like image 344
cwmyers Avatar asked Jun 12 '13 09:06

cwmyers


1 Answers

If you look at the definition of Reader in the Scalaz source, you'll see this:

    type Reader[-E, +A] = ReaderT[Id, E, A]

Which tells us that the Reader monad you are using is just a specialization of a monad transformer where the monad being wrapped is the trivial Id monad. You can use ReaderT directly, but wrapping your OptionT[IO, _] monad instead of just wrapping everything in a Reader. For example, the following should do what you want:

type OptionIO[+A] = OptionT[IO, A]

def findFfmpeg: ReaderT[OptionIO, Config, String] =
  Kleisli[OptionIO, Config, String](c => findFile(c.ffmpegLocations))

def findFfmpegWrapper: ReaderT[OptionIO, Config, String] =
  Kleisli[OptionIO, Config, String](c => findFile(c.ffmpegWrapperLocations))

def encode(fileName: String): ReaderT[OptionIO, Config, Unit] = (for {
   w <- findFfmpegWrapper
   b <- findFfmpeg
   stream <- Kleisli[OptionIO, Config, Stream[String]](
     _ => callFfmpeg(getCommand(w, b, fileName)).liftM[OptionT]
   )
} yield stream).map(_ foreach println)

In principle you should be able to replace the part after stream <- with the following:

callFfmpeg(getCommand(w, b, fileName)).liftM[OptionT].liftReaderT[Config]

But for some reason the Unapply machinery that liftReaderT relies on doesn't seem to be working in this case. Writing the Kleisli part out explicitly isn't all that horrible, fortunately.


As a footnote: the nice liftReaderT syntax I mentioned becomes available if you define an UnapplyCo instance like this:

implicit def unapplyMFA1[TC[_[_]], F[+_], M0[F[+_], +_], A0](
  implicit TC0: TC[({ type L[x] = M0[F, x] })#L]
): UnapplyCo[TC, M0[F, A0]] {
  type M[+X] = M0[F, X]
  type A = A0
} = new UnapplyCo[TC, M0[F, A0]] {
  type M[+X] = M0[F, X]
  type A = A0
  def TC = TC0
  def leibniz = Leibniz.refl
}

I'm not sure off the top of my head whether there's a reason Scalaz 7 doesn't currently provide this instance, but it's probably worth looking into.

like image 198
Travis Brown Avatar answered Oct 24 '22 22:10

Travis Brown