Effect cats and the IO monad

I have been trying to understand the IO monad for a while and it makes sense. If I am not mistaken, the goal is to separate the description of the side effect and the actual execution. As with the example below, Scala has a way to get an environment variable that is not referential transparency. There were two questions.

Question 1: Is this link transparent

Question 2: How to properly (unit / property-based) check this? There is no way to check for equality because it checks for the existence of a memory reference, and there is no way to check on an inner function because comparison of functions is not possible if I am not wrong. But I don't want to run the actual side effect in my unit test. Also, is this a design bug or misuse of the IO monad?

case class EnvironmentVariableNotFoundException(message: String) extends Exception(message)

object Env {
  def get(envKey: String): IO[Try[String]] = IO.unit.flatMap((_) => IO.pure(tryGetEnv(envKey)))

  private[this] def tryGetEnv(envKey: String): Try[String] =
    Try(System.getenv(envKey))
      .flatMap(
        (x) =>
          if (x == null) Failure(EnvironmentVariableNotFoundException(s"$envKey environment variable does not exist"))
          else Success(x)
      )

      

}

+3


source to share


1 answer


Good to use IO

to wrap values ​​in your program that came from an impure source, just like the system call in your example. This will return you IO[A]

, which reads "I can get A

through impure means." After that, you can use transparent / referentially transparent functions acting on this A

, through map

, flatMap

etc.

This leads to two answers. I would ask what property are you trying to test?

Looking at this code, I notice flatMap

in tryGetEnv

as something that can be complex enough to warrant testing. You can do this by extracting this logic into a pure function. You can (for example) overwrite this to have a function that returns IO[String]

and then write a (testable) function that converts it to the type you want.

IO does exactly what you say, but that clearly doesn't mean the code is truly transparent! If you want to check the actual side effects here, you might want to consider passing System as an argument and mocking it, just like in a program that doesn't use IO

.



So, I would consider creating a minimal function that makes a call System

to create IO[A]

(in this case, IO[Try[String]]

). You can test this minimal feature with ridicule, but only if you feel you are adding value by doing so. Around this, you can write functions that take A

and test them by passing pure values ​​to those functions. Remember the signature for map

on is as IO

follows, but f

here is a pure (testable) function!

sealed abstract class IO[+A] {

  def map[B](f: A => B): IO[B]
             ^ f is a pure function!
               test it by passing A values and verifying the Bs

      

In short, this pattern encourages you to create values IO

only at the very edge of your program (for example, your function main

). The rest of your program can be created from pure functions that act on the values ​​that will come from the I / O types when the program starts.

+2


source







All Articles