Last active
June 10, 2018 15:33
-
-
Save dadepo/4138175b64dca46ca64158727ca3278e to your computer and use it in GitHub Desktop.
Implementation of a program that requires configuration using Reader Monad (via Cats), Implicit Parameters, and Implicit Function.
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
/** | |
* Chapter 4 of the Scala with Cats book (https://underscore.io/books/scala-with-cats/) has got | |
* an exercise regarding the Reader monad which is supposed to showcase the classic use case of Reader Monad: | |
* which is to build programs that accepts a configuration as parameter. | |
* | |
* The exercise can be summarised as follows. | |
* | |
* Given a DB configuration with the following shape: | |
* case class Db(usernames: Map[Int, String], passwords: Map[String, String]) | |
* where usernames is Map of userid to username | |
* and passwords is a Map of username to password | |
* | |
* Create a method that generate DbReaders to look up the username for an Int user ID. The method will look this this: | |
* def findUsername(userId: Int): DbReader[Option[String]] = ??? | |
* | |
* Create a method that looks up the password for a String username. The method will look like this: | |
* def checkPassword(username: String, password: String): DbReader[Boolean] = ??? | |
* | |
* Then making use of the findUsername and checkPassowrd method, create a checkLogin method to check the password for a given user | |
* ID. The method should be as follows: | |
* def checkLogin(userId: Int, password: String): DbReader[Boolean] = ??? | |
* | |
* | |
* After reading about the Reader monad and implementing the exercise I had a feeling Reader monad is unnecessary in | |
* Scala since the language has Implicits which can easily be used to achieve what is said to be the primary use | |
* case of Reader Monads. | |
* | |
* In this gist I note down the solution to the exercise implemented using: | |
* | |
* 1. Reader Monad | |
* 2. Implicit parameters | |
* 3. Implicit functions (requires Scala 3) | |
* | |
* And yes, I think, for this particular use case, it is better to eschew the Reader monad and just got with implicits | |
*/ | |
// 1. Using Reader Monad from Cats. | |
package example.chapter4 | |
import cats.data.Reader | |
import cats.implicits._ | |
object readerImplementation extends App { | |
type DbReader[A] = Reader[Db, A] | |
case class Db(usernames: Map[Int, String], passwords: Map[String, String]) | |
def findUsername(userId: Int): DbReader[Option[String]] = Reader {db:Db => db.usernames.get(userId)} | |
def checkPassword(username: String, password: String): DbReader[Boolean] = Reader { | |
db:Db => db.passwords.get(username).contains(password) | |
} | |
def checkLogin(userId: Int, password: String): DbReader[Boolean] = for { | |
username <- findUsername(userId) | |
allowed <- username.map{uname => checkPassword(uname, password)}.getOrElse(false.pure[DbReader]) | |
} yield allowed | |
val users = Map( | |
1 -> "dade", | |
2 -> "kate", | |
3 -> "margo" | |
) | |
val passwords = Map( | |
"dade" -> "zerocool", | |
"kate" -> "acidburn", | |
"margo" -> "secret" | |
) | |
val db = Db(users, passwords) | |
println(checkLogin(1, "zerocool").run(db)) // prints true | |
println(checkLogin(4, "davinci").run(db)) // prints false | |
} | |
//2. Using Plain Scala with Implicit Parameters | |
package example.chapter4 | |
object implicitParameterImplementation extends App { | |
case class Db(usernames: Map[Int, String], passwords: Map[String, String]) | |
def findUsername(userId: Int)(implicit db: Db): Option[String] = | |
db.usernames.get(userId) | |
def checkPassword(username: String, password: String)(implicit db: Db): Boolean = | |
db.passwords.get(username).contains(password) | |
val users = Map( | |
1 -> "dade", | |
2 -> "kate", | |
3 -> "margo" | |
) | |
val passwords = Map( | |
"dade" -> "zerocool", | |
"kate" -> "acidburn", | |
"margo" -> "secret" | |
) | |
implicit val db = Db(users, passwords) | |
def checkLogin(userId: Int, password: String)(implicit db: Db): Boolean = { | |
for { | |
username <- findUsername(userId) | |
allowed <- Some(checkPassword(username, password)) | |
} yield allowed | |
}.getOrElse(false) | |
println(checkLogin(1, "zerocool")) // prints true | |
println(checkLogin(4, "davinci")) // prints false | |
} | |
// 3. Implementation using Implicit Functions. | |
// Implicit function is a feature only available in the next Scala release. ie Scala 3 (aka Dotty) | |
// So implementation below would only compile if you use dotty compiler. | |
// Find information on getting dotty here http://dotty.epfl.ch/index.html#getting-started | |
package example.chapter4 | |
object implicitFunctionImplementation extends App { | |
type ConfiguredWithDb[T] = implicit Db => T | |
// usernames Map of userid to username | |
// passwords Map of username to password | |
case class Db(usernames: Map[Int, String], passwords: Map[String, String]) | |
def findUsername(userId: Int): ConfiguredWithDb[Option[String]] = | |
db.usernames.get(userId) | |
def checkPassword(username: String, password: String): ConfiguredWithDb[Boolean] = | |
db.passwords.get(username).contains(password) | |
val users = Map( | |
1 -> "dade", | |
2 -> "kate", | |
3 -> "margo" | |
) | |
val passwords = Map( | |
"dade" -> "zerocool", | |
"kate" -> "acidburn", | |
"margo" -> "secret" | |
) | |
implicit val db:Db = Db(users, passwords) | |
def checkLogin(userId: Int, password: String): ConfiguredWithDb[Boolean] = { | |
for { | |
username <- findUsername(userId) | |
allowed <- Some(checkPassword(username, password)) | |
} yield allowed | |
}.getOrElse(false) | |
println(checkLogin(1, "zerocool")) // prints true | |
println(checkLogin(4, "davinci")) // prints false | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment