Skip to content

converting Future backend to cats' IO backend, referential transparency and defer #907

Description

@bwiercinski

Hello
In my application I'm using AkkaHttpBackend which is SttpBackend[Future,...]. In the logic of the application I'm using cats.effect.IO. The sttp3 provides new syntax for my use case, which is mapK, so I was happy to use that while I was migrating from sttp2 to sttp3. After mapping I have SttpBackend[IO, Any], so my expectation was: if I invoke .send then I have got IO[Response[...]] and since IO is referential transparent I can use the benefits of that in my application.

The bellow example shows that it is not true:

import cats.effect._
import cats.~>
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec
import sttp.capabilities.Effect
import sttp.client3._
import sttp.client3.asynchttpclient.future.AsyncHttpClientFutureBackend
import sttp.client3.impl.cats.implicits._
import sttp.model.Uri
import scala.concurrent._

class DeferringFutureBackendSpec extends AsyncWordSpec with Matchers {
  implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)

  def createBackend(): SttpBackend[IO, Any] = {
    val futureToIO = λ[Future ~> IO](future => Async.fromFuture(IO(future)))
    val ioToFuture = λ[IO ~> Future](_.unsafeToFuture())
    val mockBackend: SttpBackend[Future, Any] = AsyncHttpClientFutureBackend.stub().whenAnyRequest.thenRespondCyclic("1", "2")
    val ioBackend: SttpBackend[IO, Any] = mockBackend.mapK(futureToIO, ioToFuture)

    ioBackend
    // new DeferringSttpBackend(ioBackend) // this will work
  }

  class DeferringSttpBackend(delegate: SttpBackend[IO, Any]) extends DelegateSttpBackend(delegate) {
    override def send[T, R >: Effect[IO]](request: Request[T, R]): IO[Response[T]] = Sync[IO].defer(delegate.send(request))
    override def close(): IO[Unit] = Sync[IO].defer(delegate.close())
  }

  "defer action for each request" in {
    val backend: SttpBackend[IO, Any] = createBackend()
    val sendAction: IO[Response[String]] = backend.send(basicRequest.response(asStringAlways).get(Uri("localhost")))
    for {
      a <- sendAction
      b <- sendAction
    } yield {
      a.body shouldBe "1"
      b.body shouldBe "2" // TestFailedException: expected "2" actual "1"
    }
  }.unsafeToFuture()
}

after wrapping ioBackend using DeferringSttpBackend the test is passing.

after thinking about it for a while I understand why it is happening, because futureToIO is impure.

my questions are:

  • If you would see this code for the first time what behavior would you expect. If I'm using SttpBackend[IO,...] I'm expecting it to be referential transparent.
  • Is this a bug or bad configuration?
  • Is this worth mentioning in the documentation? Maybe add a new example called "converting SttpBackend[Future, P] to SttpBackend[IO, P]"
  • Is there a place for DeferringSttpBackend in the public api so everyone can use it?

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementtodoissue is still valid and waiting for contributor

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions