Search code examples
scaladependency-injectionfunctional-programmingmonads

Scala IO monad: what's the point?


I watched a video recently on how you could come up with the IO monad, the talk was in scala. I am actually wondering what the point of having functions return IO[A] out of them. The lambda expressions wrapped in the IO object are what the mutations are and at some point higher up the change they have to be observed, I mean executed, so that something happens. Are you not just pushing the problem higher up the tree somewhere else?

The only benefit I can see is that it allows for lazy evaluation, in the sense that if you do not call the unsafePerformIO operation no side effects occur. Also I guess other parts of the program could use / share code and deciede when it wants the side effects to occur.

I was wondering if this is all? Is there any advantages in testability? I am assuming not as you would have to observe the effects which sort of negates this. If you used traits / interfaces you can control the dependencies but not when the effects take place on these dependencies.

I put together the following example in code.

case class IO[+A](val ra: () => A){
  def unsafePerformIO() : A = ra();
  def map[B](f: A => B) : IO[B] = IO[B]( () => f(unsafePerformIO()))
  def flatMap[B](f: A => IO[B]) : IO[B] = {
    IO( () =>  f(ra()).unsafePerformIO())
  }
}



case class Person(age: Int, name: String)

object Runner {

  def getOlderPerson(p1: Person,p2:Person) : Person = 
    if(p1.age > p2.age) 
        p1
      else
        p2

  def printOlder(p1: Person, p2: Person): IO[Unit] = {
    IO( () => println(getOlderPerson(p1,p2)) ).map( x => println("Next") )
  }

  def printPerson(p:Person) = IO(() => {
    println(p)
    p
  })

  def main(args: Array[String]): Unit = {

    val result = printPerson(Person(31,"Blair")).flatMap(a => printPerson(Person(23,"Tom"))
                                   .flatMap(b => printOlder(a,b)))

   result.unsafePerformIO()
  }

}

You can see how the effects are deferred until main which I guess is cool. I came up with this after getting a feel for this from the video.

Is my implementation correct and Is my understanding correct.

I am also wondering whether to get milage it should be combined with the ValidationMonad, as in ValidationMonad[IO[Person]] so we can short circuit when exceptions occurs? Thoughts please.

Blair


Solution

  • It is valuable for the type signature of a function to record whether or not it has side effects. Your implementation of IO has value because it does accomplish that much. It makes your code better documented; and if you refactor your code to separate, as much as possible, logic which involves IO from logic that doesn't, you've made the non-IO-involving functions more composable and more testable. You could do that same refactoring without an explicit IO type; but using an explicit type means the compiler can help you do the separation.

    But that's only the beginning. In the code in your question, IO actions are encoded as lambdas, and therefore are opaque; there is nothing you can do with an IO action except run it, and its effect when run is hardcoded.

    That is not the only possible way to implement the IO monad.

    For example, I might make my IO actions case classes that extend a common trait. Then I can, for example, write a test that runs a function and sees whether it returns the right kind of IO action.

    In those case classes representing different kinds of IO actions, I might not include hard coded implementations of what the actions do when I run. Instead, I could decouple that using the typeclass pattern. That would allow swapping in different implementations of what the IO actions do. For example, I might have one set of implementations that talk to a production database, and another set that talks to a mock in-memory database for testing purposes.

    There is a good treatment of these issues in Chapter 13 ("External Effects and I/O") of Bjarnason & Chiusano's book Functional Programming in Scala. See especially 13.2.2, “Benefits and drawbacks of the simple IO type”.

    UPDATE: re "swap in different implementations of what the IO actions do", you might look up "free monad", which is one way to arrange for that. Also relevant is "tagless final" style, in which you write monadic code independently of a concrete type such as IO.