/effectful-demo

A demonstration library and ongoing sandbox project for effectful services.

Primary LanguageScalaMIT LicenseMIT

effectful-demo

Overview

A demonstration project and ongoing sandbox for "effectful services".

Effectful services are monad-generic service trait and service implementation class pairs that can be used with any monad or nested monad combination without code changes to the trait or class. They follow standard service orientation patterns which offer callers a stable service contract, decouples them from service dependencies and implementation details and allows composition of services to create new services. Effectful services allow the creation of generic business logic libraries that can be reused in any Scala framework or environment such as Play, Scalatra, akka-http, http4s, scala-js, scala-native, monix, cats-effect, etc.

Note: this demo uses cats for the standard functional type-classes such as Monad, Applicative, etc. Scalaz (or any other similar library) could have been used.

Note: this demo uses monadless (https://github.com/monadless/monadless) for service implementations

Declare an effectful service

  • Declare an effectful service by creating a trait that accepts a generic monad parameter.

    • Any monad or nested monad combination could be substituted for the generic monad parameter (such as Future, IO or free monad).

  • Declare only unimplemented methods or vals to decouple implementation details and dependencies from callers.

  • Restrict method parameter types to primitives and simple ADTs of primitives to allow for easily exposing as a web service or serialization in the Free monad.

  • Wrap each method’s return type with the generic monad type to ensure that effects generated by method calls can be captured by the monad.

    • Which effects are captured depends on the monad stack selected and the implementation of the service’s dependencies.

trait Users[E[_]] {
  def findByUsername(username: String) : E[Option[User]]
  def findById(id: UUID) : E[Option[User]]
  def findAll(start: Int, batchSize: Int) : E[Seq[User]]
  def create(id: UUID,username: String,plainTextPassword: String) : E[Boolean]
  def rename(userId: UUID, newUsername: String) : E[Boolean]
  def setPassword(userId: UUID, plainTextPassword: String) : E[Boolean]
  def remove(userId: UUID) : E[Boolean]
}

Full source: Users.scala

Implement an effectful service

  • Implement the service by creating a class that inherits from the trait and also has a generic monad parameter that is propagated to the trait.

  • Compose service from other services by constructor-injecting required dependencies.

    • Decouple and hide dependencies from callers of the trait by not exposing them in the trait.

    • Propagate the generic monad type to the other effectful services to ensure that only implementations that use the same monad type can be passed.

  • Implicitly constructor-inject Monad type-class for generic monad type and import monad operations to support for-comprehensions with monad type (and map/flatMap).

    • Add extra operations to monad type by injecting monad "augments" (such as Exceptions or Par ) to ensure monad supports operations such as exception handling, parallel execution or time delays (see PasswordsImpl.scala or SqlDocDaoImpl.scala for examples)

class UsersImpl[E[_]](
  usersDao: SqlDocDao[UUID,UserData,E],
  passwords: Passwords[E],
  logger: Logger[E]
)(implicit
  E:Monad[E]
) extends Users[E] {
  ...
  • Keep service pure by deferring effect capture to other effectful services or augments that capture the effects inside the monad.

  • Use map/flatMap and for-comprehensions on monad type (even if it is nested) without the need for monad transformers or lifting every statement.

  • Re-use service implementation for any monad or nested monad combination.

  ...
  def create(id: UUID, username: String, plainTextPassword: String) : E[Boolean] =
    lift {
      unlift(findById(id)) match {
        case Some(_) => false
        case None =>
          unlift(findByUsername(username)) match {
            case Some(_) => false
            case None =>
              val digest = unlift(passwords.mkDigest(plainTextPassword))
              val result = unlift(usersDao.insert(id,UserData(
                username = username,
                passwordDigest = digest
              )))
              if(result) {
                unlift(info(s"Created user $id with username $username"))
              }
              result
          }
      }
    }
   ...
}

Full source: UsersImpl.scala

Use an effectful service

  • Decide on a monad or nested monad combination then wire and inject.

  • Lift services that are implemented with another monad into the desired monad.

  • Avoid creating implicits at every call site by injecting them once at service creation.

  type E[A] = Future[LogWriter[A]]
...
  val passwords = new PasswordsImpl[E](
    passwordMismatchDelay = 5.seconds,
    logger = WriterLogger("passwords").liftService[E]
  )

  val userDao = new SqlDocDaoImpl[UUID,UsersImpl.UserData,E](
    sql = sqlDriver.liftService[E],
    recordMapping = userDataRecordMapping,
    metadataMapping = userDataMetadataRecordMapping
  )
  val users = new UsersImpl[E](
    usersDao = userDao,
    passwords = passwords,
    logger = WriterLogger("users").liftService[E]
  )
...

Re-use effectful services with any monad

  • Use different monads for different circumstances, some examples:

    • Simplify testing by testing services using the identity monad.

    • Use immediate logging for local service callers and LogWriter for remote service callers to return logs back to remote callers.

    • Easily modify how errors are captured later (e.g. convert some/all exception to an explicit type: Future[Either[Error,A]]

    • Compare performance of similar monads such as Future, scalaz Task or monix Task

    • Try out new frameworks easily.

    • Migrate between frameworks with minimal code changes.

    • Call effectful services from normal, non-monadic code by using the identity monad.

  type Id[A] = A
...
  val passwords = new PasswordsImpl[Id](
    passwordMismatchDelay = 5.seconds,
    logger = Slf4jLogger("passwords")
  )

  val userDao = new SqlDocDaoImpl[UUID,UsersImpl.UserData,Id](
    sql = sqlDriver,
    recordMapping = userDataRecordMapping,
    metadataMapping = userDataMetadataRecordMapping
  )
  val users = new UsersImpl[Id](
    usersDao = userDao,
    passwords = passwords,
    logger = Slf4jLogger("users")
  )
...

Full source: IdExample

Use effectful services with the free monad

  • Capture your program’s execution completely using the free monad.

    • Free monad can be executed later or serialized for execution elsewhere.

  type Cmd[A] = Either[LoggerCmd[A],SqlDriverCmd[A]]
  type E[A] = Free[Cmd,A]
...
  val passwords = new PasswordsImpl[E](
    passwordMismatchDelay = 5.seconds,
    logger = FreeLogger("passwords").liftService[E]
  )

  val userDao = new SqlDocDaoImpl[UUID,UsersImpl.UserData,E](
    sql = sqlDriver.liftService[E],
    recordMapping = userDataRecordMapping,
    metadataMapping = userDataMetadataRecordMapping
  )

  val users = new UsersImpl[E](
    usersDao = userDao,
    passwords = passwords,
    logger = FreeLogger("users").liftService[E]
  )
...

Full source: FreeMonadExample.scala

Demo: UserLogin with identity monad

$ sbt
[info] Loading project definition from /Users/lancegatlin/Code/effectful/project
[info] Set current project to effectful-demo (in build file:/Users/lancegatlin/Code/effectful/)
> test:console
[info] Updating {file:/Users/lancegatlin/Code/effectful/}effectful...
[info] Resolving jline#jline;2.14.3 ...
[info] Done updating.
[info] Starting scala interpreter...
[info]
Welcome to Scala 2.12.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.

scala> import effectful.examples.IdExample._
import effectful.examples.IdExample._

scala> uuids.gen()
res0: cats.Id[effectful.examples.pure.uuid.UUIDs.UUID] = 892f1c6e-9108-4e95-8757-e23f6728854a

scala> users.create(res0,"lance","password")
...
10:45:41.459 [run-main-0] INFO users - Created user 892f1c6e-9108-4e95-8757-e23f6728854a with username lance
res1: cats.Id[Boolean] = true

scala> userLogins.login("lance","not my password")
10:45:47.230 [run-main-0] WARN passwords - Password mismatch delaying 5 seconds
10:45:52.234 [run-main-0] WARN userLogins - User 892f1c6e-9108-4e95-8757-e23f6728854a password mismatch
res2: cats.Id[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = Left(PasswordMismatch)

scala> userLogins.login("lance","password")
10:45:53.588 [run-main-0] INFO tokens - Issued token bb2f19ad-45ab-4663-8d1f-170f77486fdc to user 892f1c6e-9108-4e95-8757-e23f6728854a
10:45:53.589 [run-main-0] INFO userLogins - User 892f1c6e-9108-4e95-8757-e23f6728854a logged in, issued token bb2f19ad-45ab-4663-8d1f-170f77486fdc
res3: cats.Id[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = Right(bb2f19ad-45ab-4663-8d1f-170f77486fdc)

scala>

Demo: UserLogin with Future + LogWriter

$ sbt
[info] Loading project definition from /Users/lancegatlin/Code/effectful/project
[info] Set current project to effectful-demo (in build file:/Users/lancegatlin/Code/effectful/)
> test:console
[info] Starting scala interpreter...
[info]
Welcome to Scala 2.12.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.

scala> import scala.concurrent._
import scala.concurrent._

scala> import scala.concurrent.duration._
import scala.concurrent.duration._

scala> import effectful.examples.FutureLogWriterExample._
import effectful.examples.FutureLogWriterExample._

scala> uuids.gen()
res0: cats.Id[effectful.examples.pure.uuid.UUIDs.UUID] = 434af6f7-4230-4873-840a-527bbe719491

scala> users.create(res0,"lance","password")
res1: effectful.examples.FutureLogWriterExample.E[Boolean] = Future(<not completed>)
...
Verified test user is inserted...

scala> Await.result(res1,Duration.Inf)
res2: effectful.examples.adapter.writer.LogWriter[Boolean] = WriterT((List(LogEntry(users,Info,Created user 434af6f7-4230-4873-840a-527bbe719491 with username lance,None,2017-06-01T14:58:34.593Z)),true))

scala> userLogins.login("lance","not my password")
res3: effectful.examples.FutureLogWriterExample.E[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = Future(<not completed>)

scala> Await.result(res3,Duration.Inf)
res4: effectful.examples.adapter.writer.LogWriter[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = WriterT((List(LogEntry(passwords,Warn,Password mismatch delaying 5 seconds,None,2017-06-01T14:58:59.481Z), LogEntry(userLogins,Warn,User 434af6f7-4230-4873-840a-527bbe719491 password mismatch,None,2017-06-01T14:59:04.497Z)),Left(PasswordMismatch)))

scala> userLogins.login("lance","password")
res5: effectful.examples.FutureLogWriterExample.E[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = Future(<not completed>)

scala> Await.result(res5,Duration.Inf)
res6: effectful.examples.adapter.writer.LogWriter[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = WriterT((List(LogEntry(tokens,Info,Issued token e95a047a-2698-49b5-a1af-29c4d92d46cd to user 434af6f7-4230-4873-840a-527bbe719491,None,2017-06-01T14:59:07.078Z), LogEntry(userLogins,Info,User 434af6f7-4230-4873-840a-527bbe719491 logged in, issued token e95a047a-2698-49b5-a1af-29c4d92d46cd,None,2017-06-01T14:59:07.078Z)),Right(e95a047a-2698-49b5-a1af-29c4d92d46cd)))

scala>

Demo: UserLogin with Free monad

$ sbt
[info] Loading project definition from /Users/lancegatlin/Code/effectful/project
[info] Set current project to effectful-demo (in build file:/Users/lancegatlin/Code/effectful/)
> test:console
[info] Starting scala interpreter...
[info]
Welcome to Scala 2.12.2 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_77).
Type in expressions for evaluation. Or try :help.

scala> import effectful.examples.FreeMonadExample._
import effectful.examples.FreeMonadExample._

scala> implicit val interpreter = idInterpreter
interpreter: effectful.free.Interpreter[effectful.examples.FreeMonadExample.Cmd,cats.Id]{type EE[A] = cats.Id[A]; val sqlInterpreter: effectful.examples.effects.sql.free.SqlDriverCmdInterpreter[this.EE]; val logInterpreter: effectful.examples.effects.logging.free.LoggerCmdInterpreter[this.EE]} = effectful.examples.FreeMonadExample$$anon$4@2f1bf

scala> uuids.gen()
res0: cats.Id[effectful.examples.pure.uuid.UUIDs.UUID] = bd414bb7-6cae-43be-91d7-67f119929c02

scala> users.create(res0,"lance","password")
res1: effectful.examples.FreeMonadExample.E[Boolean] = FlatMap(Map(Command(Right(Prepare(SELECT `Users`.`id`,`Users`.`username`,`Users`.`password_digest`,`Users`.`created`,`Users`.`last_updated`,`Users`.`removed` FROM `Users`  WHERE `id`=?,AutoCommit))),effectful.examples.pure.dao.sql.impl.SqlDocDaoImpl$$Lambda$1090/735115390@29baf1e1),scala.Function1$$Lambda$1094/400118104@75a494cc)

scala> res1.run
...
Verified test user is inserted...
11:01:41.201 [run-main-0] INFO users - Created user bd414bb7-6cae-43be-91d7-67f119929c02 with username lance
res2: cats.Id[Boolean] = true

scala> userLogins.login("lance","password")
res3: effectful.examples.FreeMonadExample.E[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = FlatMap(Command(Right(ExecuteQuery(SELECT `Users`.`id`,`Users`.`username`,`Users`.`password_digest`,`Users`.`created`,`Users`.`last_updated`,`Users`.`removed` FROM `Users`  WHERE `username`='lance',AutoCommit))),scala.Function1$$Lambda$1094/400118104@5bf4df65)

scala> res3.run
11:01:50.991 [run-main-0] INFO tokens - Issued token c90b6c3e-e1a1-4753-963d-f4959c9f43a0 to user bd414bb7-6cae-43be-91d7-67f119929c02
11:01:50.992 [run-main-0] INFO userLogins - User bd414bb7-6cae-43be-91d7-67f119929c02 logged in, issued token c90b6c3e-e1a1-4753-963d-f4959c9f43a0
res4: cats.Id[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = Right(c90b6c3e-e1a1-4753-963d-f4959c9f43a0)

scala> userLogins.login("lance","not my password")
res5: effectful.examples.FreeMonadExample.E[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = FlatMap(Command(Right(ExecuteQuery(SELECT `Users`.`id`,`Users`.`username`,`Users`.`password_digest`,`Users`.`created`,`Users`.`last_updated`,`Users`.`removed` FROM `Users`  WHERE `username`='lance',AutoCommit))),scala.Function1$$Lambda$1094/400118104@4ecf61d3)

scala> res5.run
11:01:58.103 [run-main-0] WARN passwords - Password mismatch delaying 5 seconds
11:02:03.107 [run-main-0] WARN userLogins - User bd414bb7-6cae-43be-91d7-67f119929c02 password mismatch
res6: cats.Id[Either[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = Left(PasswordMismatch)

scala>