scalascala-catstagless-final

Transform F[A] to Future[A]


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?


Solution

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