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()
}
}
@etorreborre
Copy link

I'm not sure this is such a problem to be able to change the configuration along the way. It can also be an opportunity. You could have default arguments in your configuration which are overriden locally with more specific arguments dynamically computed.

@raichoo
Copy link
Author

raichoo commented Apr 10, 2012

It's not a problem to be able to do that, a StateMonad can do that job. Reader only gives read-only access to the "implicit parameter", Writer write-only and State read-write. The types signal your intention while implicits pretty much act like a StateMonad all the time.

@przemek-pokrywka
Copy link

I don't really like the example. On the plus, the types communicate your intentions and doBadStuff is impossible now. Though I don't see the point of applying the heavy machinery of ReaderMonad (yeah, you stop noticing it when you get familiar with it, but still) when you have simpler means to achieve the goal:

The problem with the original example is that doXXXStuff functions take a config parameter only to pass it to doStuff function. In OOP you would call it a Demeter Law's violation. Due to it doBadStuff is able to mess up the config by changing it along the way.
You could fix it in a simply by changing the doXXXStuff functions like that:

def doXXXStuff(msg: String)(implicit prettyPrint: (String, String) => String) = 
    prettyPrint("XXX", msg)

The usage becomes then straightforward:

val config = Config("somehost.com", 1337)
implicit val prettyPrint = (prefix: String, msg: String) =>
    config.prettyPrint(prefix, string)

println(doCoolStuff("foo"))
println(doMoreStuff("bar"))
// println(doBadStuff("baz")) // you cannot mess up the config, 
// because doXXXStuff don't even know about it

That is much simpler than ReaderMonad. You don't need to import it from Scalaz, nor write it for your own. You don't even need to know the M* word.
What's more important: your doXXXStuff functions are more general now. They don't make a single assumption on whether any config will be read or not. You don't even couple your design to the ReaderMonad concept.
So I warmly recommend the simpler path (while not questioning the value of ReaderMonad concept in general, in this case it's an overengineering).

@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