Scalaz monad transformers. Applying f1:A => G[B], f2:B => G[C] function to F[G[A]] object
Asked Answered
H

1

5

I have two (or more) functions defined as:

val functionM: String => Option[Int] = s => Some(s.length)
val functionM2: Int => Option[String] = i => Some(i.toString)

I also have some data defined as:

val data: List[Option[String]] = List(Option("abc"))

My question is how to compose (in a nice way) the functions to get a result like:

data.map(_.flatMap(functionM).flatMap(functionM2))
res0: List[Option[String]] = List(Some(3))

I don't like the syntax of the above function calls. If i have many places like this then the code is very unreadable.

I tried to play with OptionT scalaz monad transformer, but it still has nested maps and also generates nested Options like:

OptionT(data).map(a => functionM(a).map(functionM2)).run
res2: List[Option[Option[Option[String]]]] = List(Some(Some(Some(3))))

What I want to achieve is something more or less like this:

Something(data).map(functionM).map(functionM2)

or even better:

val functions = functionM andThenSomething functionM2
Something(data).map(functions)

It would be nice if it could work with Try. As I know scalaz doesn't have TryT monad transformer, so is there any way to nicely compose functions which operates on Try?

Horrify answered 15/4, 2016 at 9:23 Comment(1)
You can accomplish val functions = functionM andThenSomething functionM2 with Kleisli(functionM) >=> Kleisli(functionM2) but I am not sure what do to next with this...Bowknot
M
7

As Łukasz mentions, Kleisli seems most relevant here. Any time you have some functions of the shape A => F[B] and you want to compose them as if they were ordinary functions A => B (and you have a flatMap for F), you can represent the functions as Kleisli arrows:

import scalaz._, Scalaz._

val f1: Kleisli[Option, String, Int] = Kleisli(s => Some(s.length))
val f2: Kleisli[Option, Int, String] = Kleisli(i => Some(i.toString))

And then:

scala> f1.andThen(f2).run("test")
res0: Option[String] = Some(4)

If you're familiar with the idea of the reader monad, Kleisli is exactly the same thing as ReaderT—it's just a slightly more generic way of framing the idea (see my answer here for more detail).

In this case it seems unlikely that monad transformers are what you're looking for, since you're not reaching all the way inside the List[Option[A]] to work directly with the As—you're keeping the two levels distinct. Given the definitions of f1 and f2 above, I'd probably just write the following:

scala> val data: List[Option[String]] = List(Option("abc"))
data: List[Option[String]] = List(Some(abc))

scala> data.map(_.flatMap(f1.andThen(f2)))
res1: List[Option[String]] = List(Some(3))

Lastly, just because Scalaz doesn't provide a Monad (or Bind, which is what you'd need here) instance for Try, that doesn't mean you can't write your own. For example:

import scala.util.{ Success, Try }

implicit val bindTry: Bind[Try] = new Bind[Try] {
  def map[A, B](fa: Try[A])(f: A => B): Try[B] = fa.map(f)
  def bind[A, B](fa: Try[A])(f: A => Try[B]): Try[B] = fa.flatMap(f)
}

val f1: Kleisli[Try, String, Int] = Kleisli(s => Success(s.length))
val f2: Kleisli[Try, Int, String] = Kleisli(i => Success(i.toString))

And then:

scala> val data: List[Try[String]] = List(Try("abc"))
data: List[scala.util.Try[String]] = List(Success(abc))

scala> data.map(_.flatMap(f1.andThen(f2)))
res5: List[scala.util.Try[String]] = List(Success(3))

Some people have some concerns about the lawfulness of a Functor or Monad or Bind instance like this for Try in the presence of exceptions, and these people tend to be loud people, but I find it hard to care (in my view there are better reasons to avoid Try altogether).

Misshape answered 15/4, 2016 at 11:13 Comment(1)
There is an improvement for using Kleisli. If we really want to avoid using nested map we can use OptionT transformer. import scalaz._, Scalaz._ val f1: Kleisli[Option, String, Int] = Kleisli(s => Some(s.length)) val f2: Kleisli[Option, Int, String] = Kleisli(i => Some(i.toString)) val f3: Kleisli[Option, String, String] = Kleisli(s => Some(s + " hello")) val functions: Kleisli[Option, String, String] = f1 andThen f2 andThen f3 val data: List[Option[String]] = List(Option("abc")) OptionT.optionT(data).map(functions).run.flattenHorrify

© 2022 - 2024 — McMap. All rights reserved.