Created
April 9, 2012 18:50
-
-
Save raichoo/2345414 to your computer and use it in GitHub Desktop.
Monads vs. implicit values in Scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
case class Config(host: String, port: Int) { | |
def prettyPrint(prefix: String, msg: String): String = | |
List(prefix, ": ", msg, " on ", host, ":", port.toString).mkString | |
} | |
/** | |
* Passing a configuration with implicits is like | |
* working in the state monad. You can "put" a | |
* new configuration even though we don't want that | |
* to happen in this particular example. We don't | |
* signal our intention that we just want to read | |
* from the configuration. | |
*/ | |
object ImplicitExample { | |
private def doStuff(prefix: String, msg: String) | |
(implicit c: Config): String = | |
c.prettyPrint(prefix, msg) | |
private def doCoolStuff(msg: String) | |
(implicit c: Config): String = | |
doStuff("cool", msg) | |
private def doMoreStuff(msg: String) | |
(implicit c: Config): String = | |
doStuff("more", msg) | |
/** | |
* Putting a new state. This is actually something | |
* that we don't want to happen, but the compiler | |
* allows it since it doesn't know what our intentions | |
* are. | |
*/ | |
private def doBadStuff(msg: String) | |
(implicit c: Config): String = | |
doStuff("bad", msg)(c.copy(host = "badhost.com")) | |
def run() { | |
implicit val config = Config("somehost.com", 1337) | |
println(doCoolStuff("foo")) | |
println(doMoreStuff("bar")) | |
println(doBadStuff("baz")) | |
} | |
} | |
/** | |
* Example from above without implicits but with a ReaderMonad | |
* that secures our configuration. | |
*/ | |
object MonadExample { | |
/** | |
* Introducing the reader monad. It only allows to read | |
* the configuration. Changing it as we go is not possible. | |
*/ | |
class ReaderMonad[A, B](private val f: A => B) { | |
def map[C](g: B => C): ReaderMonad[A, C] = | |
new ReaderMonad(a => g(f(a))) | |
def flatMap[C](g: B => ReaderMonad[A, C]): ReaderMonad[A, C] = | |
new ReaderMonad(a => g(f(a)).f(a)) | |
def run(a: A) = f(a) | |
} | |
object ReaderMonad { | |
def apply[A, B](b: B) = | |
new ReaderMonad[A, B](a => b) | |
def ask[A]: ReaderMonad[A, A] = | |
new ReaderMonad(identity) | |
} | |
type ConfigReader[A] = ReaderMonad[Config, A] | |
private def doStuff(prefix: String, msg: String): ConfigReader[String] = | |
ReaderMonad.ask.map(_.prettyPrint(prefix, msg)) | |
private def doCoolStuff(msg: String): ConfigReader[String] = | |
doStuff("cool", msg) | |
private def doMoreStuff(msg: String): ConfigReader[String] = | |
doStuff("more", msg) | |
def run() { | |
val config = Config("somehost.com", 1337) | |
/** | |
* compose all our actions with the intention of just reading | |
* the configuration. | |
*/ | |
val doAllTheStuff = for { | |
cool <- doCoolStuff("foo") | |
more <- doMoreStuff("bar") | |
} yield List(cool, more) | |
// execute all functions with one configuration | |
doAllTheStuff.run(config).foreach(println) | |
} | |
} | |
/** | |
* Conclusion: don't use implicits to pass around information. Use | |
* Reader/Writer/State Monads, since they signal our intentions and | |
* enforce them. They ship with Scalaz and you don't have to write | |
* them yourself like I did in this example. | |
*/ | |
object Main { | |
def main(args: Array[String]) { | |
ImplicitExample.run() | |
MonadExample.run() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If all you wanted to do was to show, that monads give you finer control over parameter passing, then you made the point.
However you also seem to advise to use reader/writer/state monads in similar cases, even though they are not free of issues and moreover, much simpler alternative exists. I believe, that it just comes from this particular example, which you've admitted to be completely contrived. I'm coming from OO background, I'm still learning FP concepts and would be disappointed, if reader/writer/state monads didn't shine sometimes :) It just doesn't in this example. Your solution involving monads suffers from unnecessary coupling to the monad datatype. True, it gives you finer control over parameter passing, but as I've demonstrated, there's a simpler way and monads add you the cost of accidental complexity. Instead of just solving the problem, suddenly you're shaving the monad yaks.
I would be really happy to see the reader/writer/state monads in a context, where they really fit.
I'm also very curious, how would you apply typeclasses to this example, because despite I think I know the pattern, I can't yet imagine applying it succesfully here.
I have nothing against monads, like I'm not against OOP GoF design patterns in Java. The context is the king, though. You can overuse anything, including monads. At the extremum, in OOP you get something like http://chaosinmotion.com/blog/?p=622 - by extrapolating it to FP I expect similar results. The point I'm trying to make is that you should strive for maximum simplicity (and to use various constructs, like monads, where they really fit).