18

I have a Play! 2 for Scala application that needs to retrieve some data in JSON format from an external service.

The Play! framework allows to make HTTP requests asynchronously by wrapping the response in a Promise. Promise is a monad that wraps a value that will be available in the future.

This is fine, but in my case what I get from the web service is a JSON string. I have to parse it and the parsing might fail. So I have to wrap whatever I get into an Option. The result is that many of my methods are returning Promise[Option[Whatever]]. That is, a value of type Whatever that will be, maybe, available later.

Now whenever I have to operate over such a value I need to map it twice. I was thinking of handling this in the following way:

  • creating a new type, say Hope[A], that wraps a Promise[Option[A]]
  • defining the relevant methods like map (or maybe I should use foreach and inherit from some collection trait?) and flatten
  • provide an implicit converter between Promise[Option[A]] and Hope[A].

It is easy to define map - the composition of two functors is again a functor - and flatten can be done explicitly in this case, or whenever composing a monad with Option.

But it is my limited understanding that I do not need to reinvent this stuff: monad transformer exist for exactly this case. Or, well, so I think - I have never used a monad tranformer - and this is the point of the question:

Can monad tranformers be used in this situation? How would I go about actually using them?

Ben James
  • 121,135
  • 26
  • 193
  • 155
Andrea
  • 20,253
  • 23
  • 114
  • 183

2 Answers2

14

Using the Scalaz library's OptionT transformer, you should be able to turn values of type Promise[Option[A]] into values of type OptionT[Promise, A].

Using Scalaz 7:

import scalaz.OptionT._
val x: OptionT[Promise, Int] = optionT(Promise.pure(Some(123)))

To use this value, for example to call map or flatMap on it, you will need to provide an appropriate typeclass for Promise (Functor for map, Monad for flatMap).

Since Promise is monadic, it should be possible to provide an instance of Monad[Promise]. (You'll get Functor and Applicative for free, because the typeclasses form an inheritance hierarchy.) For example (note: I've not tested this!):

implicit val promiseMonad = new Monad[Promise] {
  def point[A](a: => A): Promise[A] = Promise.pure(a)
  def bind[A, B](fa: Promise[A])(f: A => Promise[B]): Promise[B] = fa flatMap f
}

As a simple example, you can now use map on the OptionT[Promise, A], to apply a function of type A => B to the value inside:

def foo[A, B](x: OptionT[Promise, A], f: A => B): OptionT[Promise, B] = x map f

To retrieve the underlying Promise[Option[A]] value from an OptionT[Promise, A], call the run method.

def bar[A, B](x: Promise[Option[A]], f: A => B): Promise[Option[B]] =
  optionT(x).map(f).run

You will gain more benefit from using monad transformers when you can compose several operations of compatible types, preserving the OptionT[Promise, _] type between operations and retrieving the underlying value at the end.

To compose operations in a for-comprehension, you will need functions of type A => OptionT[Promise, B].

Ben James
  • 121,135
  • 26
  • 193
  • 155
  • I have tried it and everything works fine in my application. There is only one odd thing: if I introduce a type alias `type Hope[A] = Promise[Option[A]]` to simplify the return type of my functions, I get a compile-time error `java.lang.IllegalArgumentException: transpose requires all collections have the same size`. Do you have any clue why? – Andrea Sep 03 '12 at 17:08
  • @Andrea: If you use an explicit type parameter with `optionT` the error should go away. It looks like a compiler bug and if you have the time it might be worth checking [the issue tracker](https://issues.scala-lang.org/secure/Dashboard.jspa) or asking a follow-up question here. – Travis Brown Sep 03 '12 at 18:03
1

-- removed --

edit:

Okay, you can simply use the scalaz.OptionT here:

val x = optionT(Promise { /* api call */ some("""{ "foo": "bar" }""") })
val mapped = x.map(Json.parse).run // run return the resulting Promise[Option[T]]
drexin
  • 24,225
  • 4
  • 67
  • 81
  • Well, in writing `x map (_ * 2)` instead of your four lines. Now, in this little example it may be of little value, but in more complex cases I think it may make matters more clear. – Andrea Sep 03 '12 at 12:53
  • This for-comprehension will not compile because `x` and `opt` are not of compatible types. You would need to nest two for-comprehensions. – Ben James Sep 03 '12 at 13:25
  • Thank you! I have accepted the other answer, though, as it is more detailed, and as I said I am new to monad trasformers. – Andrea Sep 03 '12 at 13:39
  • I'm absolutely fine with that ;-) – drexin Sep 03 '12 at 13:59