Skip to content

Instantly share code, notes, and snippets.

@jroper
Created June 9, 2015 14:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jroper/a75aa4e8ef356545a355 to your computer and use it in GitHub Desktop.
Save jroper/a75aa4e8ef356545a355 to your computer and use it in GitHub Desktop.
Reader monads
import scalaz.Reader
case class User(id: Int, name: String)
case class Interest(name: String)
trait Database
trait Attempt1 {
// How every explanation of Reader monad I've seen/read goes:
def userForId(id: Int): Reader[Database, User]
def interestsForUser(user: User): Reader[Database, Seq[Interest]]
def sharedInterests(idA: Int, idB: Int): Reader[Database, Seq[Interest]] = for {
userA <- userForId(idA)
userB <- userForId(idB)
interestsA <- interestsForUser(userA)
interestsB <- interestsForUser(userB)
} yield {
interestsA.filter(interestsB.contains)
}
// So, the database is dependency injected through the reader monad. Great. But, I want to test sharedInterests,
// and I don't want to require a database in the tests, because it shouldn't need it. The Reader monad has exposed
// the dependencies of userForId and interestsForUser, meaning testing anything that uses them requires a database,
// ie, you have all the boiler plate of a DI solution without the prime benefit - testing in isolation.
}
trait Attempt2 {
// So, this is how I might do it:
trait UserDb {
def userForId(id: Int): User
def interestsForUser(user: User): Seq[Interest]
}
def sharedInterests(idA: Int, idB: Int): Reader[UserDb, Seq[Interest]] = Reader { (userDb: UserDb) =>
val userA = userDb.userForId(idA)
val userB = userDb.userForId(idB)
val interestsA = userDb.interestsForUser(userA)
val interestsB = userDb.interestsForUser(userB)
interestsA.filter(interestsB.contains)
}
// So, now I can test sharedInterests, by supplying mocked versions of userForId and interestsForUser
def test = {
val mockUserDb = new UserDb {
val sarah = User(1, "Sarah")
val john = User(2, "John")
val users = Map(1 -> sarah, 2 -> john)
val interests = Map(sarah -> Seq(Interest("movies"), Interest("sport")), john -> Seq(Interest("sport"), Interest("music")))
def userForId(id: Int) = users(id)
def interestsForUser(user: User) = interests(user)
}
assert(sharedInterests(1, 2).run(mockUserDb) == Seq("sport"))
}
// So then, if I run my application:
class UserDbImpl(database: Database) extends UserDb {
def userForId(id: Int): User = ???
def interestsForUser(user: User): Seq[Interest] = ???
}
def run1(database: Database) = {
sharedInterests(1, 2).run(new UserDbImpl(database))
}
// This is where I get confused. I've only used the Reader monad for dependency injection of my sharedInterests
// function. My UserDbImpl is not using the Reader monad for dependency injection, it's constructor dependency
// injected. So, if I'm using constructor dependency injection there, why not just use it for my sharedInterests
// function as well?
class SharedInterestsCalculator(userDb: UserDb) {
def sharedInterests(idA: Int, idB: Int): Seq[Interest] = {
val userA = userDb.userForId(idA)
val userB = userDb.userForId(idB)
val interestsA = userDb.interestsForUser(userA)
val interestsB = userDb.interestsForUser(userB)
interestsA.filter(interestsB.contains)
}
}
def run2(database: Database) = {
new SharedInterestsCalculator(new UserDbImpl(database)).sharedInterests(1, 2)
}
// What do I gain by using the reader monad, other than added complexity of introducing a monad for no apparent reason?
}
@martinpallmann
Copy link

Maybe the UserDb methods should return Reader[Database, A] and then someone can come and compose the Readers. I'd ask Runar about that. He might be able to shine some light on the matter. My head always hurts if I do functional programming to much (but in a good way).

Maybe this helps?:

http://stackoverflow.com/questions/25947783/how-to-inject-multi-dependencies-when-i-use-reader-monad-for-dependency-inject#answer-25953101

@debasishg
Copy link

In Attempt1, function sharedInterests needs a Database as part of its contract. How can you make it disappear in tests ? At least you need to supply a mock instance (possibly a Map) from where the test will extract the data for users and interests. And that's exactly what Reader allows you to do. Possibly I am missing something about your thoughts.

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