Skip to content

Instantly share code, notes, and snippets.

@harrylaou
Last active November 20, 2016 18:14
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 harrylaou/a1ffff7a9511b1f15f3d9729c933561f to your computer and use it in GitHub Desktop.
Save harrylaou/a1ffff7a9511b1f15f3d9729c933561f to your computer and use it in GitHub Desktop.
Emm Example
package controllers
import application.AppComponents.DatabaseSB
import cats.data.Xor
import cats.implicits._
import emm._
import emm.compat.cats._
import models.User
import play.api.Logger.logger
import play.api.libs.json.{Format, JsValue, Json, Reads, Writes}
import play.api.mvc.{AnyContent, Request, Results, _}
import services.postgres.PostgresDriverSB
import scala.concurrent.{ExecutionContext, Future}
import play.api.mvc.Results.Ok
/**
*
* This code demonstrates the practicality and usefulness of advanced functional programming techniques
* like generalized effect composition libraries, in this example Emm. https://github.com/djspiewak/emm
* It also proves by example and comparison that even is simple operations like parsing a json
* and updating a model in db, the usage of Emm produces much clearer and concrete code.
*
* We need a definition of the monad stack
* type FXE = Future |: Xor[Erratum, ?] |: Base
*
* and use a for-comprehension by lifting the values in the effect stack using :
*
* pointM[FXE] – in the case of a Base type , like Unit or User
* liftM[FXE] – in the case of a Xor[Erratum, Base]
* wrapM[FXE] – Future[Xor[Erratum, Base]]
*
* You can compare how much more complex the code is when not using Emm ,
* but with map, flatMap and embedded pattern matching.
*
* Without using Emm the function is 26-lines of code long
* and much more complex because of the embedded pattern matching
*
* Using Emm, the code becomes very clean and 8-lines long.
*
* @author harrylaou
*
*/
class DummyController(implicit db: DatabaseSB, ec: ExecutionContext) extends Controller {
import Helper._
/**
* demo of using Emm
*
* - calls functions with different return types
* - lifs them in the effect stack
* - gets the value from the effect stack
* - transforms the asynchronous value to asynchronous result
*
* @return
*/
def user = Action.async { implicit request =>
val savedUserE: Emm[FXE, User] = for {
jsValue <- parseRequest(request).liftM[FXE]
user <- fromJson(jsValue).liftM[FXE]
savedUser <- upsertXor(user).wrapM[FXE]
_ <- logger.debug(s"User $user updated").pointM[FXE]
} yield savedUser
stackToResult[User](savedUserE.run)
}
/**
* Same functionality without Emm ,
* much more complex when using map, flatMap and embedded pattern matching.
*
*/
def userWithoutEmm = Action.async { implicit request =>
val jsonOpt: Option[JsValue] = request.body.asJson
jsonOpt match {
case None =>
logger.error(s" cannot parse json for ${request.body}")
Future.successful(UnprocessableEntity(" cannot parse json"))
case Some(jsValue) =>
val userXor = Json.fromJson(jsValue).asEither.toXor
userXor match {
case Xor.Right(user) =>
upsert(user).map { (uOpt: Option[User]) =>
uOpt match {
case None =>
logger.error(s"couldn't write in db user:$user")
InternalServerError(s"couldn't write in db user:$user")
case Some(u) =>
logger.debug(s"User $user updated")
Ok(Json.toJson(u))
}
}
case Xor.Left(errorData) =>
logger.error(s"$errorData")
Future.successful(UnprocessableEntity(s"$errorData"))
}
}
}
}
/**
* This is just a holder of functions and implicits.
* These can be anywhere in the codebase but for demo reasons
* they are in this example.
*/
object Helper {
/**
* extending the default Postgres database
*/
type DatabaseSB = PostgresDriverSB#Backend#Database
/**
* Definition of the monad stack
*/
type FXE = Future |: Xor[Erratum, ?] |: Base
/**
* Generic (polymorphic) function that parses a JsValue and returns the disjunction of an error and a value of A
*
* @param jsValue
* @param reads implicit parameter of a play.api.libs.json.Reads[A]
* @tparam A
* @return
*/
def fromJson[A](jsValue: JsValue)(implicit reads: Reads[A]): Xor[Erratum, A] =
Json.fromJson[A](jsValue)(reads).asEither.toXor.leftMap(e =>
Erratum(status = Results.UnprocessableEntity, message = e.toString()))
implicit val userJsonF: Format[User] = Json.format[User]
/**
* helper function to parse a request and return the Xor (disjunction) of either an error or a JsValue
*
* @param request
* @return Xor[Erratum, JsValue]
*/
def parseRequest(request: Request[AnyContent]): Xor[Erratum, JsValue] = {
val jsonOpt: Option[JsValue] = request.body.asJson
jsonOpt.toRightXor(Erratum(status = Results.UnprocessableEntity, message = s"cannot parse json for ${request.body}"))
}
/**
* this belongs in a slick class , but is here to demo its return type.
*/
def upsert(u: User)(implicit db: DatabaseSB, ec: ExecutionContext): Future[Option[User]] = ???
/**
* this belongs in a slick class , but is here to demo its return type.
*/
def upsertXor(u: User)(implicit db: DatabaseSB, ec: ExecutionContext): Future[Xor[Erratum, User]] = ???
/**
*
* Polymorphic function that tranforms the stacked monad into an synchronous result.
*
* @param fxo the stacked monad
* @param jsWrites the implicit play.api.libs.json.Writes
* @param ec implicit ExecutionContext
* @tparam A type parameter of a model
* @return
*/
def stackToResult[A](fxo: Future[Xor[Erratum, A]])(implicit jsWrites: Writes[A], ec: ExecutionContext): Future[Result] =
fxo.map(xo => xo match {
case Xor.Left(erratum) =>
logger.error(erratum.toString)
erratum.toResult
case Xor.Right(user) => Ok(Json.toJson(user))
})
}
/**
* Represents an error.
*
* The name was chosen so as not to class with
* a bazillion other `Error` classes in other libraries.
* So here the Error (Erratum) is the product of status , which its type is a Play type,
* but can be anything else that represents an HTTP status code
* and a simple String message
*
*/
case class Erratum(status: Results.Status, message: String) {
def toResult: Result = {
status(message)
}
}
object Erratum {
def withMessage(msg: String) = Erratum(Results.InternalServerError, msg)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment