Search code examples
scalagenericstypesscala-catscats-effect

Understanding cats-effect forceR definition?


def forceR[B](that: IO[B]): IO[B] =
    // cast is needed here to trick the compiler into avoiding the IO[Any]
    asInstanceOf[IO[Unit]].handleError(_ => ()).productR(that)

Source

  • Self asInstanceOf[IO[Unit]] turns self into a IO[Unit]
  • and then handleError has the type def handleError[B >: A](f: Throwable => B): IO[B] which normally stats the type bound of returning IO[B] should be super type of IO[Unit]

Then why this forceR definition on the call site have the true IO[B] type such as IO[Int]?


Solution

  • Your question is slightly confusing.

    Then why this forceR definition on the call site have the true IO[B] type such as IO[Int]?

    Because the returned type of forceR is specified to be IO[B]. So it can't be anything else at the call site (if the code compiles).

    I guess you mis-interprete the comment

    // cast is needed here to trick the compiler into avoiding the IO[Any]
    

    This is not to avoid return type IO[Any]. The return type will anyway be IO[B] because of productR, @GaëlJ is correct

    sealed abstract class IO[+A] ... {
      ...
    
      def productR[B](that: IO[B]): IO[B] =
        flatMap(_ => that)
    

    So this is not intended to make this.handleError(_ => ()).productR(that) not IO[Any], this seems to be intended to make this.handleError(_ => ()) not IO[Any].

    If this is IO[A] and A <: AnyVal then this.handleError(_ => ()) has type IO[AnyVal]. If not A <: AnyVal then this.handleError(_ => ()) has type IO[Any]. Not sure why it's important in handleError though. Some reflection (.getClass), caching, stack tracing is used in handleError. Maybe error message is undesired for IO[Any].

    def handleError[B >: A](f: Throwable => B): IO[B] =
      handleErrorWith[B](t => IO.pure(f(t)))
    
    def handleErrorWith[B >: A](f: Throwable => IO[B]): IO[B] =
      IO.HandleErrorWith(this, f, Tracing.calculateTracingEvent(f))
    
    def calculateTracingEvent(key: Any): TracingEvent = {
      if (isCachedStackTracing) {
        val cls = key.getClass
        get(cls)
      } else if (isFullStackTracing) {
        buildEvent()
      } else {
        null
      }
    }
    
    /**
     * Holds platform-specific flags that control tracing behavior.
     *
     * <p>The Scala compiler inserts a volatile bitmap access for module field accesses. Because the
     * `tracingMode` flag is read in various IO constructors, we are opting to define it in a Java
     * source file to avoid the volatile access.
     *
     * <p>INTERNAL API with no source or binary compatibility guarantees.
     */
    public final class TracingConstants {...}
    

    Otherwise maybe casting was just a precaution and is not actually necessary :)

    The specific commit where @DanielSpiewak added the casting was here

    https://github.com/typelevel/cats-effect/commit/66d30c4ccdfdb8aca5f6632f5506dda192fc2f03#diff-2dec4bb5f2b890f966e62cfdfcc6d32746235e45b4a14113f56e38b49923132aL367-R368


    The interesting question is why actually Daniel needed not IO[Any] in the part before productR and whether he actually needed that. I'm not 100% sure.

    @LuisMiguelMejíaSuárez:

    This is just speculation, but I am fairly sure is just to allow compilation in the presence of strict compiler flags. As you may know, there is a flag to make the compiler warn if it infers Any, and another flag that turns all compiler warning into errors; thus stopping compilation. Additionally, as you may also be aware the sbt-tpolecat plugin enable those warnings and I am pretty sure most typelevel project either use that plugin, or enable a similar set of flags by other means. So, that is why they needed to avoid the inferred Any while recovering from the possible errors.

    Makes sense.