Search code examples
scalafor-comprehensionio-monad

How the sample Simple IO Type get rid of side effects in "FP in Scala"?


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.

Solution

    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.