Simple example using tagless final functional pattern leveraging cats-effect. Taken from here and updated a bit (basically using explicit types)
Also it can be worth to have a look to https://www.baeldung.com/scala/tagless-final-pattern.
Simple example using tagless final functional pattern leveraging cats-effect. Taken from here and updated a bit (basically using explicit types)
Also it can be worth to have a look to https://www.baeldung.com/scala/tagless-final-pattern.
import cats.Monad | |
import cats.data.EitherT | |
import cats.effect.{IO, Sync} | |
import cats.implicits._ | |
import cats.effect.unsafe.implicits.global | |
import scala.collection.mutable.ListBuffer | |
case class User(nick: String, points: Int) | |
// Algebra | |
trait UserRepositoryAlg[F[_]] { | |
def create(nick: String): F[Either[Error, User]] | |
def find(nick: String): F[Either[Error, User]] | |
def update(user: User): F[Either[Error, User]] | |
} | |
// Algebra(s) implementation - Interpreter | |
// UserRepositoryInterpreter is parametrized, but we require that F has typeclass Sync, | |
// which would allow us to delay effects with `Sync[F].delay`. | |
// Sync extends Monad, so we don't need to request is explicitly to be able to use for-comprehension | |
class UserRepositoryInterpreter[F[_]: Sync] extends UserRepositoryAlg[F] { | |
private val defaultUser = User("nick", 12) | |
val users: ListBuffer[User] = ListBuffer() | |
override def create(nick: String): F[Either[Error, User]] = { | |
val user = User(nick, 0) | |
users += user | |
for { | |
res <- Sync[F].delay(Right(user)) | |
} yield res | |
} | |
override def find(nick: String): F[Either[Error, User]] = for { | |
// Finding user will be delayed, until we interpret and run our program. | |
// Delaying execution is useful for side-effecting effects, like requesting data from database, writting to console etc. | |
res <- Sync[F].delay(Either.fromOption(users.find(_.nick == nick), new Error("Couldn't find user"))) | |
} yield res | |
// We can reuse find method from UserRepositoryInterpreter, | |
// but we have to wrap find in EitherT to access returned user | |
override def update(user: User): F[Either[Error, User]] = { | |
val result = for { | |
found <- EitherT(find(user.nick)) | |
updated = found.copy(points = found.points + user.points) | |
} yield updated | |
result.value | |
} | |
} | |
// Program (not wrapped in any object to use in a REPL!!!) | |
class Pointer[F[_] : Monad](repo: UserRepositoryAlg[F]) { | |
def addPlayer(nick: String): EitherT[F, Error, User] = { | |
val res: EitherT[F, Error, User] = EitherT(repo.create(nick)) | |
res | |
} | |
def addPoints(nick: String, points: Int): EitherT[F, Error, User] = { | |
for { | |
user <- EitherT(repo.find(nick)) | |
updated <- EitherT(repo.update(user.copy(points = points))) | |
} yield updated | |
} | |
} | |
// At this point we define, that we want to use IO as our effect monad | |
val pointer = new Pointer[IO](new UserRepositoryInterpreter[IO]) // .addPoints("nick") | |
val computation: EitherT[IO, Error, User] = for { | |
user <- pointer.addPlayer("nikki") | |
res <- pointer.addPoints("nikki", 3) | |
} yield res | |
val computationValue: IO[Either[Error, User]] = computation.value | |
val result: Either[Error, User] = computationValue.unsafeRunSync() | |
result match { | |
case Right(user) => println(s"$user") | |
case Left(error) => println(s"ERR: $error") | |
} | |
println("EOS") |