I have a repository:
trait CustomerRepo[F[_]] {
def get(id: Identifiable[Customer]): F[Option[CustomerWithId]]
def get(): F[List[CustomerWithId]]
}
I have an implementation for my database which uses Cats IO, so I have a CustomerRepoPostgres[IO]
.
class CustomerRepoPostgres(xa: Transactor[IO]) extends CustomerRepo[IO] {
import doobie.implicits._
val makeId = IO {
Identifiable[Customer](UUID.randomUUID())
}
override def get(id: Identifiable[Customer]): IO[Option[CustomerWithId]] =
sql"select id, name, active from customer where id = $id"
.query[CustomerWithId].option.transact(xa)
override def get(): IO[List[CustomerWithId]] =
sql"select id, name, active from customer"
.query[CustomerWithId].to[List].transact(xa)
}
Now, I want to use a library which cannot deal with arbitrary holder types (it only supports Future
). So I need a CustomerRepoPostgres[Future]
.
I thought to write some bridge code which can convert my CustomerRepoPostgres[IO]
to CustomerRepoPostgres[Future]
:
class RepoBridge[F[_]](repo: CustomerRepo[F])
(implicit convertList: F[List[CustomerWithId]] => Future[List[CustomerWithId]],
convertOption: F[Option[CustomerWithId]] => Future[Option[CustomerWithId]]) {
def get(id: Identifiable[Customer]): Future[Option[CustomerWithId]] = repo.get(id)
def get(): Future[List[CustomerWithId]] = repo.get()
}
I don't like that this approach requires implicit converters for every type used in the repository. Is there a better way to do this?
This is exactly what the tagless final approach is for, to abstract over F
by requiring it to follow some specific type constraints. For example, let's create a custom implementation which requires F
to be an Applicative
:
trait CustomerRepo[F[_]] {
def get(id: Identifiable[Customer]): F[Option[CustomerWithId]]
def get(): F[List[CustomerWithId]]
}
class CustorRepoImpl[F[_]](implicit A: Applicative[F]) extends CustomerRepo[F] {
def get(id: Identifiable[Customer]): F[Option[CustomerWithId]] {
A.pure(???)
}
def get(): F[List[CustomerWithId]] = {
A.pure(???)
}
}
This way, no matter the concrete type of F
, if it has an instance of Applicative[F]
then you'll be good to go, with no need to define any transformers.
The way we do this is just put the relevant constraints on F
according to the processing we need to do. If we need a sequential computation, we can use a Monad[F]
and then flatMap
the results. If no sequentiality is needed, Applicative[F]
might be strong enough for this.