Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Monad for-comprehensions with implicit Monad fails, use inheritance?

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"]
like image 666
rausted Avatar asked Nov 25 '25 21:11

rausted


1 Answers

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

like image 151
triggerNZ Avatar answered Nov 27 '25 15:11

triggerNZ



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!