Skip to content

Instantly share code, notes, and snippets.

@raichoo
Created April 9, 2012 18:50
Show Gist options
  • Save raichoo/2345414 to your computer and use it in GitHub Desktop.
Save raichoo/2345414 to your computer and use it in GitHub Desktop.
Monads vs. implicit values in Scala
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()
}
}
@raichoo
Copy link
Author

raichoo commented Apr 11, 2012

The example itself is completely contrived so no need to refactor it. I just wanted to make a point. Passing around implicit values is what Reader/Writer/State Monads do, only more fine grained than using the implicit keyword in Scala.

Your comment however gives me the impression that you have a deeply rooted fear or misunderstanding when it comes to Monads since you call them "heavy machinery". I really recommend that you research the topic, you will soon discover how common they are and that "the M*" word just calls the child by its name. If a banana is a banana, why wouldn't you call it a banana? Oddly enough programmers seem to be scared of calling a Monad a Monad (the abstraction thou shalt not name?). Monads are everywhere just look at SQL, functions, the friggin semicolon you type at the and of a line. All of these things, and even more, are packed with what you call "the M*" word. That also applies to implicit parameters and that was the whole point of this piece of code.

By the way, what you did in your example can be achieved even better by using a typeclass.

Just try to wrap your head around it, even if it takes some time, you'll have an awesome ride.

@przemek-pokrywka
Copy link

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment