Skip to content

Instantly share code, notes, and snippets.

@cb372
Forked from jroper/Attempts.scala
Last active August 29, 2015 14:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cb372/542c8d46cd0ead6f880c to your computer and use it in GitHub Desktop.
Save cb372/542c8d46cd0ead6f880c to your computer and use it in GitHub Desktop.
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?
}
trait Attempt3 {
import scalaz._
import Scalaz._
import scala.language.higherKinds
//def userForId(id: Int): Reader[Database, User]
//def interestsForUser(user: User): Reader[Database, Seq[Interest]]
/*
* Abstract away from Reader monad -> remove dependency on Database.
* In production code, `userForIdF` and `interestsForUserF` will be `Reader[Database, ...]`.
* In tests they can be whatever kind of monad you like.
*/
def sharedInterests[M[_]: Monad](
userForIdF: Int => M[User],
interestsForUserF: User => M[Seq[Interest]])
(idA: Int, idB: Int): M[Seq[Interest]] = {
for {
userA <- userForIdF(idA)
userB <- userForIdF(idB)
interestsA <- interestsForUserF(userA)
interestsB <- interestsForUserF(userB)
} yield {
interestsA.filter(interestsB.contains)
}
}
}
/*
* In your tests you can use any Monad, so it's easy to mock up your 2 Reader monads.
* No sign of a `Database` anywhere near your test.
*/
object UnitTest extends App with Attempt3 {
import scalaz.Scalaz.Id
def userForId(id: Int): Id[User] = {
// TODO: mock impl here
User(id, "Chris")
}
def interestsForUser(user: User): Id[Seq[Interest]] = {
// TODO: mock impl here
Seq(Interest("swimming"), Interest("reading"))
}
val subjectUnderTest = sharedInterests(userForId, interestsForUser) _
val result = subjectUnderTest(1, 2)
assert(result == Seq(Interest("swimming"), Interest("reading")))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment