Given `() => IO[T]`, can I obtain `IO[() => T]`?

62 Views Asked by At

I have an object

    object Producer {
      def apply() :IO[Foo] = ???
    }

It is in code I do not control. I am composing (flat mapping) several functions returning IO. One of them hides a Ref, and exposes update method compatible with it:

    object State {
      def update(update: Bar => Bar) :IO[Bar] = ???
    } 

This one I do control, although introducing significant changes would cause a large refactor.

Problem:

    for {
      bar <- State.update { arg => ??? }
    } yield ()

In some cases, but not all, I need a Foo to produce a new Bar inside the lambda passed to State.update. Can it be done without always evaluating foo <- Producer as the first step?

2

There are 2 best solutions below

1
Mateusz Kubuszok On BEST ANSWER

One way of thinking of IO[A] is that it is () => A (or () => Future[A]).

So () => IO[T] and IO[() => T] are kinda the same idea, just with a different ergonomics (and different ways of evaluating what's inside). So you should be able to go from one into another and back e.g. with:

val a: () => IO[A] = ...
val b: IO[() => A] = IO { () => a.runUnsafeSync }

val c: IO[() => A] = ...
val d: () => IO[A] = () => c.map(_())

However, the devil is in the details:

  • such runUnsafeSync will not propagate cancellation from the outer IO to the inner IO
  • similarly it would not propagate state update in IOLocal

so making a bullet-proof conversion this way would be... rather difficult.

And hardly ever it would be useful as with ergonomics there comes a convention:

  • IO[A] suggest a side-effect (or at least a possibility of it)
  • X => non-IO suggest pure computation - take X, then return sth without mutation nor talking to external world
  • () => provides no new information, so such pure function would have to be constant (it might be a lazy value basically)

so if we followed the convention, we could convert fa: () => IO[A] into IO[() => A] by doing

IO.defer {
  fa()
}.map { a =>
  // a is a constant value, so function we created is constant
  // and everything that obtains it through e.g. map/flatMap
  // would get the same value no matter how many times it would be called.

  // However, evaluation whole IO twice could result in 2 functions
  // returning different constant values.
  () => a
}

It should make the conversion trivial, but it would also mean that all these () => in the context of IO[A] are kinda pointless. Unless you need to do them to adapt your code to some external interface which expects () => F[A] or F[() => A], it's just noise.

0
slouc On

Your question is: can we go from A => IO[B] to IO[A => B]. Think of it conceptually.

Let's say that we have a function String => IO[Int] that takes a username, reads the user data from a database, and returns the user's age (wrapped in IO because we have a database call). You are then looking for a higher-order function that can turn that name -> age function into a single effectful value IO[String => Int]. How could that possibly work? What could the implementation of String => Int be? Neither one of the following two things exists:

  • database lookup using a function instead of username
  • some other implementation that can obtain the age purely, without doing the database lookup

Those are two completely different things. Here's a hint on their difference:

  • Monad's flatMap takes a function of type A => F[B], or in your case A => IO[B].
  • Applicative functor's apply takes a function of type F[A => B], or in your case IO[A => B].
  • Applicative is the weaker (= more general) abstraction.

Using a for-comp like you suggest is the right way.

for {
  producedBar <- Producer.apply
  bar <- State.update { arg => producedBar }
} yield ()

or written differently

Producer.apply().flatMap(producedBar => State.update(arg => producedBar))

It clearly shows that doing a state update depends on another function that dictates the new updated value. Or in case of our earlier example - it shows that you need to obtain your number first, before you can apply your "triple" function to it.

Note that this only speaks about the order or dependency chain of operations. It says nothing about their actual execution in the real world; we're only describing the algorithm for our lazy side-effectful program. (unlike using e.g. eagerly evaluated Future, in which case your concerns would be much more justified)

Can you explain what's problematic about this?