Search code examples
scala

Compose Tagless Final Return Types in Scala


I have a repo service that uses Tagless final and it looks like this:

final class MyRepoImpl[M[_]: Async](transactor: DoobieTransactor[M])(implicit ec: Scheduler)
  extends MyRepo[M] {

  override def isKnownElement(modelName: String): M[Boolean] = ???

  override def isUpdateRequired(modelName: String): M[Boolean] = ???
    
  override def currentDatePostgres(): M[String] = ???
}

I then have a service that calls this Repo like this:

final class Myservice[M[_]] {
  
  def myMethod(id: Int): M[SomeReturnType] = {
    // How can I compose isKnownElement first and if the return is true, process the isUpdateRequired and based on the response, I would like to return SomeReturnType
  }
}

All I want to do is to compose the two mehods in the Repo class in my service class. I tried doing flatMap, but I do not find that signature. My repo trait looks like this:

trait MyRepo[M[_]] {
  override def isKnownElement(modelName: String): M[Boolean] = ???    
  override def isUpdateRequired(modelName: String): M[Boolean] = ???
  override def currentDatePostgres(): M[String] = ???
}

I want to acheive the following:

  1. If isKnownElement returns true, I want to check isUpdateRequired and if isUpdateRequired returns true, I need to return SomeResultType which contains a status field with status Rejected

  2. If isKnownElement returns false, I would like to return SomeResultType with status Rejected

  3. If isKnownElement returns true and isUpdateRequired returns false, I need to return SomeResultType with status Accepted

How can I elegantly map this elegantly?


Solution

  • If you need to ensure that isUpdateRequired is only called if and only if isKnownElement returns true

    import cats.Monad
    import cats.syntax.all.*
    
    final class Myservice[M[_]: Monad](
      repository: MyRepo[M]
    ) {
      def myMethod(id: Int): M[SomeReturnType] =
        repository.isKnownElement(modelName = ???).flatMap {
          case true =>
            repository.isUpdateRequired(modelName = ???).map {
              case true =>
                SomeReturnType(status = Rejected)
    
              case false =>
                SomeReturnType(status = Accepted)
            }
    
          case false =>
            IO.pure(SomeReturnType(status = Rejected))
        }
    }
    

    If you can just call both operations (either sequentially or in parallel)

    import cats.Monad
    import cats.syntax.all.*
    
    final class Myservice[M[_]: Monad](
      repository: MyRepo[M]
    ) {
      def myMethod(id: Int): M[SomeReturnType] =
        // You may use parMapN if you want to call both methods in parallel.
        (
          repository.isKnownElement(modelName = ???),
          repository.isUpdateRequired(modelName = ???)
        ).mapN {
          case (true, false) => SomeReturnType(status = Accepted)
          case (true, true) => SomeReturnType(status = Rejected)
          case (false, _) => SomeReturnType(status = Rejected)
        }
    }
    

    You can also use for, if / else, and many other syntactical changes depending on your style.
    But, the core idea remains, you just compose programs. Nothing fancy.

    PS: This is a good example of Boolean blindless, you may use some ADTs instead of booleans so the pattern match version reads better.