3

Looking at an IO Monad example from Functional Programming in Scala:

def ReadLine: IO[String] = IO { readLine }
def PrintLine(msg: String): IO[Unit] = IO { println(msg) }

def converter: IO[Unit] = for {
  _ <- PrintLine("enter a temperature in degrees fahrenheit")
  d <- ReadLine.map(_.toDouble)
  _ <- PrintLine((d + 32).toString)
} yield ()

I decided to re-write converter with a flatMap.

def converterFlatMap: IO[Unit] = PrintLine("enter a temperate in degrees F").
     flatMap(x => ReadLine.map(_.toDouble)).
       flatMap(y => PrintLine((y + 32).toString))

When I replaced the last flatMap with map, I did not see the result of the readLine printed out on the console.

With flatMap:

enter a temperate in degrees 
37.0

With map:

enter a temperate in degrees

Why? Also, how is the signature (IO[Unit]) still the same with map or flatMap?

Here's the IO monad from this book.

  sealed trait IO[A] { self =>
    def run: A
    def map[B](f: A => B): IO[B] =
      new IO[B] { def run = f(self.run) }
    def flatMap[B](f: A => IO[B]): IO[B] =
      new IO[B] { def run = f(self.run).run }
  }
Kevin Meredith
  • 41,036
  • 63
  • 209
  • 384

1 Answers1

5

I think Scala converts IO[IO[Unit]] into the IO[Unit] in the second case. Try to run both variants in scala console, and don't specify type for the def converterFlatMap: IO[Unit], and you'll see the difference.

As for why map doesn't work, it is clearly seen from the definition of IO: when you map over IO[IO[T]], map inside will call run only on the outer IO, result will be IO[IO[T]], so only first two PrintLine and ReadLine will be executed.

flatMap will also execute inner IO, and result will be IO[T] where T is the type parameter A of the inner IO, so all three of the statements will be executed.

P.S.: I think you incorrectly expanded for-comprehension. according to rules, for loop that you have written should be expanded to:

PrintLine("enter a temperate in degrees F").flatMap { case _ =>
    ReadLine.map(_.toDouble).flatMap { case d =>
        PrintLine((d + 32).toString).map { case _ => ()}
    }
}

Notice that in this version flatMaps/maps are nested.

P.P.S: In fact last for statement should be also flatMap, not map. If we assume that scala had a "return" operator that puts values into the monadic context,
(e.g. return(3) will create IO[Int] that does nothing and it's function run returns 3.),
then we can rewrite for (x <- a; y <- b) yield y as
a.flatMap(x => b.flatMap( y => return(y))),
but because b.flatMap( y => return(y)) works absolutely the same as b.map(y => y) last statement in the scala for comprehension is expanded into map.

Community
  • 1
  • 1
vitalii
  • 3,335
  • 14
  • 18
  • but we need to use a `flatMap` in the final line of the `for comprehension` due to, as you pointed out, `map` only executes `f(self.run)`, whereas we need `f(self.run).run` (flatMap), right? I understand that both compile since both `IOMonad#map` and `#flatMap` return `IO[Unit]`. – Kevin Meredith Feb 15 '14 at 20:07
  • [also - your example worked (thanks), but it should be `PrintLine((d + 32))...`] – Kevin Meredith Feb 15 '14 at 20:10
  • No, the final line could be a map, as in my p.s., but the rest of the comprehension should be correctly expanded. – vitalii Feb 15 '14 at 20:11
  • I'm confused as to how `PrintLine((d + 32).toString).map { case _ => ()}` results in calling the inner-most `PrintLine`. It seems that, reading only `map` and `flatMap`'s signatures, only the latter will call `run` on the inner and outer `IO[Unit]` – Kevin Meredith Feb 17 '14 at 04:00
  • There's no inner IO in the `PrintLine((d + 32).toString).map { case _ => ()}` :). It is a single `IO[Unit]` and is itself an "inner" IO. The second flatMap, which is ` ReadLine.map(_.toDouble).flatMap { case d =>` calls `run` on it. – vitalii Feb 17 '14 at 09:06