I'm trying to understand how to use effect monads (cats.effect.IO
or scalaz.IO
does not matter). Imagine I have the following method:
def extract(str: String): String = {
if(str.contains("123"))
"123"
else
throw new IllegalArgumentException("Boom!")
}
Since this method is impure (throws exception) and I need to combine its result with another effectful computation (network-IO) is it a good practice to just wrap it into IO
as follows:
def extract(str: String): IO[String] = IO {
if(str.contains("123"))
"123"
else
throw new IllegalArgumentException("Boom!")
}
Is it a common use case of Effect monads?
Whether this is appropriate or not depends on what you want to express.
It's not like your first def extract(str: String): String
-method definition is somehow invalid: you are just sweeping all the exceptions and side-effects under the rug, so that they aren't visible in the signature. If this is irrelevant for your current project, and if it's acceptable that a program simply crashes with a long stack trace of the thrown exception, then do it, no problem (it's easy to imagine one-off throw-away scripts where this would be appropriate).
If instead you declare def extract(str: String): IO[String] = IO { ... }
, then you at least can see in the signature that the function extract
can do something impure (throw an exception, in this case). Now the question becomes: who is responsible for dealing with this exception, or where do you want to deal with this exception? Consider this: if the exception is thrown, it will emerge in your code in the line where something like yourProgram.unsafeRunSync()
is invoked. Does it make sense to deal with this exception there? Maybe it does, maybe it doesn't: nobody can tell you. If you just want to catch the exception at the top level in your main
, log it, and exit
, then it's appropriate.
However, if you want to deal with the exception immediately, you have better options. For example, if you are writing a method that prompts for a file name, then tries to extract
something from this name, and finally does some IO
on files, then the return type IO[String]
might be too opaque. You might want to use some other monad instead (Option
, Either
, Try
etc.) to represent the failed extraction. For example, if you use Option[String]
as return type, you no longer have to deal with an obscure exception thousand miles away in your main
method, but instead you can deal with it immediately, and, for example, prompt for a new file name repeatedly.
The whole exercise is somewhat similar to coming up with a strategy how to deal with exceptions in general, only here you are expressing it explicitly in the type signatures of your methods.