Created
June 9, 2015 14:16
-
-
Save jroper/a75aa4e8ef356545a355 to your computer and use it in GitHub Desktop.
Reader monads
This file contains 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
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? | |
} | |
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
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