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
IO Monad is simply a Monad which: Allows you to safely manipulate effects. Transform the effects into data and further manipulate it before it actually gets evaluated.
The I/O monad contains primitives which build composite actions, a process similar to joining statements in sequential order using `;' in other languages. Thus the monad serves as the glue which binds together the actions in a program.
Scala Language Monads Monad Definition Informally, a monad is a container of elements, notated as F[_] , packed with 2 functions: flatMap (to transform this container) and unit (to create this container). Common library examples include List[T] , Set[T] and Option[T] .
We've used Option as an example above because Scala's Option type is a monad, which we will cover in more detail soon.
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 (December 2015): re "swap in different implementations of what the IO actions do", these days more and more people are using the "free monad" for this kind of thing; see e.g. John De Goes's blog post “A Modern Architecture for FP”.
The benefit of using the IO monad is having pure programs. You do not push the side-effects higher up the chain, but eliminate them. If you have an impure function like the following:
def greet { println("What is your name?") val name = readLine println(s"Hello, $name!") }
You can remove the side-effect by rewriting it to:
def greet: IO[Unit] = for { _ <- putStrLn("What is your name?") name <- readLn _ <- putStrLn(s"Hello, $name!") } yield ()
The second function is referentially transparent.
A very good explanation why using the IO monads leads to pure programs can be found in Rúnar Bjarnason's slides from scala.io (video can be found here).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With