S-Mach / effectful

An open source Scala library for effectful services

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

effectful

UNDER CONSTRUCTION

Overview

An open source Scala library for effectful services.

Effectful services are generic trait/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, decouple callers from service dependencies and implementations and allow 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 such as Play, akka-http, scala-js, http4s, etc.

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 inputs 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 are captured by the monad.

    • Which effects are captured depends on the monad 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.

  • 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.

class UsersImpl[E[_]](
  usersDao: SqlDocDao[UUID,UserData,E],
  passwords: Passwords[E],
  logger: Logger[E]
)(implicit
  E:Monad[E]
) extends Users[E] {
  import Monad.ops._
  ...

  def create(id: UUID, username: String, plainTextPassword: String) =
    findById(id).flatMap {
      case Some(_) => E.pure(false)
      case None =>
        findByUsername(username).flatMap {
          case Some(_) => E.pure(false)
          case None =>
            for {
              digest <- passwords.mkDigest(plainTextPassword)
              result <- usersDao.insert(id,UserData(
                username = username,
                passwordDigest = digest
              ))
              _ <- if(result) {
                info(s"Created user $id with username $username")
              } else {
                E.pure(())
              }
            } yield 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:

    • Test pure services with the identity monad for simplicity

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

    • Compare performance of similar monads such as Future and scalaz.Task.

    • Try out new frameworks easily.

    • Migrate between frameworks with minimal code changes.

  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

  • Completely capture all effects using the free monad (without changing UserImpl)

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

  type Cmd[A] = 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.12.1 ...
[info] Done updating.
[info] Compiling 2 Scala sources to /Users/lancegatlin/Code/effectful/target/scala-2.11/test-classes...
[info] Starting scala interpreter...
[info]
Welcome to Scala 2.11.8 (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: effectful.Id[effectful.examples.pure.uuid.UUIDs.UUID] = f54214e6-2054-4717-b2bb-b0f9c0e7fbb1

scala> users.create(res0,"lance","password")
21:53:16.293 [MLog-Init-Reporter] INFO com.mchange.v2.log.MLog - MLog clients using slf4j logging.
...
21:53:16.736 [run-main-0] INFO users - Created user f54214e6-2054-4717-b2bb-b0f9c0e7fbb1 with username lance
res1: effectful.Id[Boolean] = true

scala> userLogins.login("lance","not my password")
21:53:30.595 [run-main-0] WARN passwords - Password mismatch delaying 5 seconds
21:53:35.600 [run-main-0] WARN userLogins - User f54214e6-2054-4717-b2bb-b0f9c0e7fbb1 password mismatch
res2: effectful.Id[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = -\/(PasswordMismatch)

scala> userLogins.login("lance","password")
21:53:45.645 [run-main-0] INFO tokens - Issued token 8a8055cd-04e6-4e06-bd17-7a2bebce192c to user f54214e6-2054-4717-b2bb-b0f9c0e7fbb1
21:53:45.646 [run-main-0] INFO userLogins - User f54214e6-2054-4717-b2bb-b0f9c0e7fbb1 logged in, issued token 8a8055cd-04e6-4e06-bd17-7a2bebce192c
res3: effectful.Id[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = \/-(8a8055cd-04e6-4e06-bd17-7a2bebce192c)

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.11.8 (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: effectful.Id[effectful.examples.pure.uuid.UUIDs.UUID] = 6cff63f8-1294-4e1a-9943-f7c5b5598f3d

scala> users.create(res0,"lance","password")
res1: effectful.examples.FutureLogWriterExample.E[Boolean] = List()

scala> 21:57:51.026 [MLog-Init-Reporter] INFO com.mchange.v2.log.MLog - MLog clients using slf4j logging.
...
Verified test user is inserted...

scala> Await.result(res1,Duration.Inf)
res2: effectful.examples.adapter.scalaz.writer.LogWriter[Boolean] = WriterT((List(LogEntry(users,Info,Created user 6cff63f8-1294-4e1a-9943-f7c5b5598f3d with username lance,None,2016-06-08T01:57:51.943Z)),true))

scala> userLogins.login("lance","not my password")
res3: effectful.examples.FutureLogWriterExample.E[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = List()

scala> Await.result(res3,Duration.Inf)
res4: effectful.examples.adapter.scalaz.writer.LogWriter[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = WriterT((List(LogEntry(passwords,Warn,Password mismatch delaying 5 seconds,None,2016-06-08T01:59:14.130Z), LogEntry(userLogins,Warn,User 6cff63f8-1294-4e1a-9943-f7c5b5598f3d password mismatch,None,2016-06-08T01:59:19.146Z)),-\/(PasswordMismatch)))

scala> userLogins.login("lance","password")
res5: effectful.examples.FutureLogWriterExample.E[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = List()

scala> Await.result(res6,Duration.Inf)
res6: effectful.examples.adapter.scalaz.writer.LogWriter[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = WriterT((List(LogEntry(tokens,Info,Issued token 273a4ec0-571c-4cfe-93c2-53198717a6b6 to user 6cff63f8-1294-4e1a-9943-f7c5b5598f3d,None,2016-06-08T01:59:37.725Z), LogEntry(userLogins,Info,User 6cff63f8-1294-4e1a-9943-f7c5b5598f3d logged in, issued token 273a4ec0-571c-4cfe-93c2-53198717a6b6,None,2016-06-08T01:59:37.725Z)),\/-(273a4ec0-571c-4cfe-93c2-53198717a6b6)))

scala>

Demo: UserLogin with Free monad

Welcome to Scala 2.11.8 (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,effectful.Id]{type EE[A] = effectful.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$2@5f3d5850

scala> uuids.gen()
res0: effectful.Id[effectful.examples.pure.uuid.UUIDs.UUID] = b9510471-2986-4826-9e9d-93a104b54801

scala> users.create(res0,"lance","password")
res1: effectful.examples.FreeMonadExample.E[Boolean] = FlatMap(Map(Command(\/-(Prepare(SELECT `Users`.`id`,`Users`.`username`,`Users`.`password_digest`,`Users`.`created`,`Users`.`last_updated`,`Users`.`removed` FROM `Users`  WHERE `id`=?,AutoCommit))),<function1>),<function1>)

scala> res1.run
22:24:15.364 [MLog-Init-Reporter] INFO com.mchange.v2.log.MLog - MLog clients using slf4j logging.
...
Verified test user is inserted...
22:24:16.122 [run-main-0] INFO users - Created user b9510471-2986-4826-9e9d-93a104b54801 with username lance
res2: effectful.Id[Boolean] = true

scala> userLogins.login("lance","password")
res3: effectful.examples.FreeMonadExample.E[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = FlatMap(Command(\/-(ExecuteQuery(SELECT `Users`.`id`,`Users`.`username`,`Users`.`password_digest`,`Users`.`created`,`Users`.`last_updated`,`Users`.`removed` FROM `Users`  WHERE `username`='lance',AutoCommit))),<function1>)

scala> res3.run
22:25:10.211 [run-main-0] INFO tokens - Issued token 7c7ae8fb-a7e4-4e08-ba64-65bb6249dc6e to user b9510471-2986-4826-9e9d-93a104b54801
22:25:10.212 [run-main-0] INFO userLogins - User b9510471-2986-4826-9e9d-93a104b54801 logged in, issued token 7c7ae8fb-a7e4-4e08-ba64-65bb6249dc6e
res4: effectful.Id[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = \/-(7c7ae8fb-a7e4-4e08-ba64-65bb6249dc6e)

scala> userLogins.login("lance","not my password")
res5: effectful.examples.FreeMonadExample.E[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = FlatMap(Command(\/-(ExecuteQuery(SELECT `Users`.`id`,`Users`.`username`,`Users`.`password_digest`,`Users`.`created`,`Users`.`last_updated`,`Users`.`removed` FROM `Users`  WHERE `username`='lance',AutoCommit))),<function1>)

scala> res5.run
22:25:20.704 [run-main-0] WARN passwords - Password mismatch delaying 5 seconds
22:25:25.711 [run-main-0] WARN userLogins - User b9510471-2986-4826-9e9d-93a104b54801 password mismatch
res6: effectful.Id[scalaz.\/[effectful.examples.pure.user.UserLogins.LoginFailure,effectful.examples.pure.user.UserLogins.Token]] = -\/(PasswordMismatch)

scala>

About

An open source Scala library for effectful services

License:MIT License


Languages

Language:Scala 100.0%