Absolutely! Bringing a monad into a design incurs a heavy cost that IMO can sometimes go unnoticed. Say you have f :: (Monad m) => a -> m b
. Calling mapM f xs
for some xs
implies an explicit execution order which might forbid parallel implementations whereas map
is inherently parallel. It also becomes more difficult to reason about your code. Is mapM (f <=< g) xs
, in fact, equal to mapM g xs >>= mapM f
? Hopefully! But if m ~ IO
, for example, I don’t know what “equal” should mean.
Another aspect is the API design. Up to which point is it a good idea to burden all users and all uses of a library to deal with implementation specific (i.e.: non-essential) complexity just because the implementors couldn’t solve an implementation puzzle?
A great example is the bytestring
library. It provides an API designed to be pure and elegant and hides a lot of the implementation concerns away where they belong: inside the implementation. How would ByteString
look like if all functions returned IO ByteString
instead?
In the particular example of the SMT solver, we need IO
just because there is no SMT solver written purely in Haskell, hence we need to talk to another process. Otherwise, it is an operation that has a very clear specification: solve env exp = True
iff exp
is SAT.
Miss some possible usage of my code that is, in fact, unsafe.
In this case, cvc4_ALL_SUPPORTED
calls simple-smt
's newSolver
, but that is just a small example, my question is more general.
That’s alright; I certainly appreciate the difficulty of that problem. But maybe we can collect a set of rules that start establishing the good practices even if its impossible to come up with a complete treatment. I feel a little dissatisfied with the usual “its bad, don’t do it”, especially because the alternative of permeating the code with IO
can be bad too.
I guess my question, in general, is whether we have a set of resources that shed light on how does one judge whether a certain unsafePerformIO
call is “safe” or not. For example:
- never forget
{-# NOINLINE ... #-}
- I really like the point on referential transparency you brought up. In that context, the rule-of-thumb I mentioned was just one single context:
E[v] = let x = v in (x, x)
. Are there some other contexts that work particularly well to illustrate other situations? How about concurrency:E[f] = (v1 `par` v2, v1) where v1 = f 1; v2 = f 2
? - Do you care if side-effects are performed at all? I.e., how about
E[v] = let x = v in True
? If you do care for side-effects, how to get around that and ensure theE[unsafePerformIO k]
actually triggers the side-effects? Maybe you don’t get around and just embraceIO
in that case. - How do exceptions get handled?
- Is a
unsafePerformIO k
executed atomically?