3
votes

I am running into this famous 10 year old ticket in Scala https://github.com/scala/bug/issues/2823

Because I am expecting for-comprehensions to work like do-blocks in Haskell. And why shouldn't they, Monads go great with a side of sugar. At this point I have something like this:

import scalaz.{Monad, Traverse}
import scalaz.std.either._
import scalaz.std.list._

type ErrorM[A] = Either[String, A]
def toIntSafe(s : String) : ErrorM[Int] = {
   try {
      Right(s.toInt)
   } catch {
      case e: Exception => Left(e.getMessage)
   }
}

def convert(args: List[String])(implicit m: Monad[ErrorM], tr: Traverse[List]): ErrorM[List[Int]] = {
   val expanded = for {
      arg <- args
      result <- toIntSafe(arg)
   } yield result

   tr.sequence(expanded)(m)
}

println(convert(List("1", "2", "3")))
println(convert(List("1", "foo")))

And I'm getting the error

"Value map is not a member of ErrorM[Int]" result <- toIntSafe(arg)

How do I get back to beautiful, monadic-comprehensions that I am used to? Some research shows the FilterMonadic[A, Repr] abstract class is what to extend if you want to be a comprehension, any examples of combining FilterMonadic with scalaz?

  1. Can I reuse my Monad implicit and not have to redefine map, flatMap etc?

  2. Can I keep my type alias and not have to wrap around Either or worse, redefine case classes for ErrorM?

Using Scala 2.11.8

EDIT: I am adding Haskell code to show this does indeed work in GHC without explicit Monad transformers, only traversals and default monad instances for Either and List.

type ErrorM = Either String

toIntSafe :: Read a => String -> ErrorM a
toIntSafe s = case reads s of
                  [(val, "")] -> Right val
                  _           -> Left $ "Cannot convert to int " ++ s

convert :: [String] -> ErrorM [Int]
convert = sequence . conv
    where conv s = do
            arg <- s
            return . toIntSafe $ arg

main :: IO ()
main = do
    putStrLn . show . convert $ ["1", "2", "3"]
    putStrLn . show . convert $ ["1", "foo"]
1
What Scala version are you using? Which library is Monad coming from? What are your imports? At a glance this is probably the fact that Either isn't right-biased prior to 2.12 but you haven't given us enough information to tell. - Travis Brown
Scalaz or not... Wouldn't something like args traverse toIntSafe produce essentially the same result, only faster? Also, why are you passing Monad[ErrorM] and Traverse[List] from the outside. Do you expect to use this method for some other Monad and Traverse implementations that aren't the constants from the library? - Andrey Tyukin
Added version. @AndreyTyukin Correct, but this is not my real-use case. This is an example to ask "how do I for-comprehension" for monads. - rausted
Maybe, but args: List[String] and toIntSafe(arg): ErrorM[Int], and the for comprehension above translates into args.flatMap( (arg:String) => toIntSafe(arg).map( (result:Int) => result ) )) which is likely why it complains about the map not existing. But even if it were the equivalent (assuming reasonable implementation of map) args.flatMap( (arg:String) => toIntSafe(arg)), it still shouldn't type-check. - Mor A.
What you want to do here is, both: iterate over List (one monad) and handle errors using Either (another monad) - to do both of that in one monad (which would allow doing it in one for comprehension) you would need a monad transformer e.g. EitherT[List, String, ?] - that is true in Haskell as well and no implicit magic would align this for you. - Mateusz Kubuszok

1 Answers

2
votes

Your haskell code and your scala code are not equivalent:

do
  arg <- s
  return . toIntSafe $ arg

corresponds to

for {
      arg <- args
    } yield toIntSafe(arg)

Which compiles fine.

To see why your example one doesn't compile, we can desugar it:

for {
      arg <- args
      result <- toIntSafe(arg)
   } yield result

=

args.flatMap { arg =>
  toIntSafe(arg).map {result => result}
}

Now looking at types:

args: List[String]
args.flatMap: (String => List[B]) => List[B]
arg => toIntSafe(arg).map {result => result} : String => ErrorM[Int]

Which shows the problem. flatMap is expecting a function returning a List but you are giving it a function returning an ErrorM.

Haskell code along the lines of:

do
    arg <- s
    result <- toIntSafe arg
    return result

wouldn't compile either for roughly the same reason: trying to bind across two different monads, List and Either.

A for comprehension in scala will or a do expression in haskell will only ever work for the same underlying monad, because they are both basically syntactic translations to series of flatMaps and >>=s respectively. And those still need to typecheck.

If you want to compose monads one thing you can do use monad transformers ( EitherT), although in your above example I don't think you want to, since you actually want to sequence in the end.

Finally, in my opinion the most elegant way of expressing your code is:

def convert(args: List[String]) = args.traverse(toIntSafe)

Because map followed by sequence is traverse