4

I am reading chapter 13.2.1 and came across the example that can handle IO input and get rid of side effect in the meantime:

object IO extends Monad[IO] {
  def unit[A](a: => A): IO[A] = new IO[A] { def run = a }
  def flatMap[A,B](fa: IO[A])(f: A => IO[B]) = fa flatMap f
  def apply[A](a: => A): IO[A] = unit(a)    
}

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(fahrenheitToCelsius(d).toString)
} yield ()

I have couple of questions regarding this piece of code:

  1. In the unit function, what does def run = a really do?
  2. In the ReadLine function, what does IO { readLine } really do? Will it really execute the println function or just return an IO type?
  3. What does _ in the for comprehension mean (_ <- PrintLine("Enter a temperature in degrees Fahrenheit: ")) ?
  4. Why it removes the IO side effects? I saw these functions still interact with inputs and outputs.
jwvh
  • 50,871
  • 7
  • 38
  • 64
injoy
  • 3,993
  • 10
  • 40
  • 69

2 Answers2

4
  1. The definition of your IO is as follows:

    trait IO { def run: Unit }
    

    Following that definition, you can understand that writing new IO[A] { def run = a } means initialising an anonymous class from your trait, and assigning a to be the method that runs when you call IO.run. Because a is a by name parameter, nothing is actually ran at creation time.

  2. Any object or class in Scala which follows a contract of an apply method, can be called as: ClassName(args), where the compiler will search for an apply method on the object/class and convert it to a ClassName.apply(args) call. A more elaborate answer can be found here. As such, because the IO companion object posses such a method:

    def apply[A](a: => A): IO[A] = unit(a)    
    

    The expansion is allowed to happen. Thus we actually call IO.apply(readLine) instead.

  3. _ has many overloaded uses in Scala. This occurrence means "I don't care about the value returned from PrintLine, discard it". It is so because the value returned is of type Unit, which we have nothing to do with.

  4. It is not that the IO datatype removes the part of doing IO, it's that it defers it to a later point in time. We usually say IO runs at the "edges" of the application, in the Main method. These interactions with the out side world will still occur, but since we encapsulate them inside IO, we can reason about them as values in our program, which brings a lot of benefit. For example, we can now compose side effects and depend on the success/failure of their execution. We can mock out these IO effects (using other data types such as Const), and many other surprisingly nice properties.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • Hi @Yuval Thanks so much! BTW, can I know which function would be assigned to `a` within IO? And how we'll invoke the `IO.run`? – injoy Jan 06 '19 at 20:11
  • @injoy `a` will be whichever function you supply. For example, one of your expressions is `IO { readLine }`, where we assign the read lines function as `a`. – Yuval Itzchakov Jan 07 '19 at 03:04
  • Sorry, I mean the three lines inside the for comprehension all return IO types. They not really print lines to stdout, right? So how can we really print those lines? thanks. – injoy Jan 07 '19 at 06:29
  • @injoy After you get back the `IO[Unit]` value from the `converter` method, you need to call `run`: `converter.run`. Note again that running the IO should happen at the edges of your program. – Yuval Itzchakov Jan 07 '19 at 11:02
  • Hi @Yuval, how the for comprehension return the IO[Unit] value? I thought the `_ <- PrintLine(fahrenheitToCelsius(d).toString)` will discard the IO value, right? Also, what does the `yield ()` do? Thanks. – injoy Jan 07 '19 at 18:42
  • @injoy It will not discard IO, it will discard the `Unit` value returned inside IO. `yield ()` will yield the unit value. – Yuval Itzchakov Jan 09 '19 at 03:01
2

The simplest way to look at IO monad as a small piece of program definition.

Thus:

  1. This is IO definition, run method defines what IO monad does. new IO[A] { def run = a } is scala way of creating an instance of class and defining method run.
  2. There is a bit of syntactical sugar is going on. IO { readLine } is same as IO.apply { readLine } or IO.apply(readLine) where readLine is call-by-name function of type => String. This calls the unit method from object IO and thus this is just creation of instance of IO class that does not run yet.
  3. Since IO is a monad, for comprehension can be used. It requires storing a result of each monad operation in a syntax like result <- someMonad. To ignore the result, _ can be used, thus _ <- someMonad reads as execute the monad but ignore the result.
  4. This methods are all IO definitions, they don't run anything and thus there is no side effect. Side effects only appears when IO.run is called.
Ivan Stanislavciuc
  • 7,140
  • 15
  • 18