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