softwaremill / softwaremill/sttp
converting Future backend to cats' IO backend, referential transparency and `defer`
Nobody has claimed this yet.
- Dominant language
- Scala
- Stars
- 1.5k
- Forks
- 338
- Avg merge
- 8h 59m
- Merged PRs (30d)
- 13
Description
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]toSttpBackend[IO, P]" - Is there a place for
DeferringSttpBackendin the public api so everyone can use it?
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the DeferringFutureBackendSpec example in the issue, especially createBackend and DeferringSttpBackend, and reproduce the two sends of the same IO action. Determine whether the expected referentially transparent behavior should be documented, supported by the public API, or treated as a configuration issue; done means a decided behavior with corresponding documentation or API changes and a passing regression test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- scala
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100