Created
June 9, 2015 14:16
-
-
Save jroper/a75aa4e8ef356545a355 to your computer and use it in GitHub Desktop.
Reader monads
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
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? | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In
Attempt1
, functionsharedInterests
needs aDatabase
as part of its contract. How can you make it disappear in tests ? At least you need to supply a mock instance (possibly aMap
) from where the test will extract the data for users and interests. And that's exactly whatReader
allows you to do. Possibly I am missing something about your thoughts.