First off, I think that for code snippets in a paper, using IORef
is perfectly sensible, particularly if the paper isn't about best practices for mutable references or concurrency. IORef
is simple to understand, has straightfoward syntax and semantics (especially in a non-concurrent setting), and is a natural choice if you want the reader to concentrate on aspects of your examples other than the IORef
s. It's unfortunate that the author's approach has backfired for you -- just ignore the IORef
s and pay attention to what the rest of what the paper is saying.
(If the paper was about best practices for mutable references or concurrency, perhaps it was written before better alternatives were available.)
Anyway, to your larger question, the main objections to using IORef
would be:
- Like any other mechanism for introducing mutable state into your program, it makes your code more difficult to reason about, maintain, test, etc. -- all the usual things that functional programming proponents would say give FP an "edge" over mutation-intensive imperative algorithms.
- It's just a newtype wrapper around a specialized
STRef RealWorld
, and the only thing it adds over STRef
are some atomic operations. In non-concurrent code, there's no good reason not to use STRef s
values in an ST s
monad, since they're more flexible -- you can run them in pure code with runST
or, if needed, in the IO monad with stToIO
.
- In concurrent code, there are more powerful abstractions, like
MVar
and STM
that are much easier to work with than IORef
s.
So, to the extent that mutable state is "bad" and -- if you really need it -- better alternatives are available depending on whether you do or don't need concurrency, there's not much to recommend IORef
.
On the other hand, if you are already working on some non-concurrent code in the IO
monad because you need to perform actual IO operations, and you genuinely need some pervasive mutable state that isn't easy to disentangle from the IO, then using IORef
s seems legitimate.
With respect to your more specific questions:
I guess it would be safe to say that using IORef
is considered "bad practice" when a weaker tool would do the job. That weaker tool might be an STRef s
, or better yet a State
monad or better yet a rewritten higher-order algorithm that doesn't need any state at all. Because IORef
combines IO with mutable references, it's kind of an imperative sledgehammer that's likely to lead to the most unidiomatic Haskell code possible, so it's best avoided unless it's "obviously" the right solution for a particular problem.
The State
monad is usually the preferred idiomatic way to add state to a program, but it provides the "illusion" of a mutable state by threading a sequence of immutable state values through the computation, and not all algorithms can be efficiently implemented this way. Where true mutable state is required, an STRef
is usually the natural choice in a non-concurrent setting. Note that you probably wouldn't use MVar
or STM
in a non-concurrent setting -- there's no reason to use them in this case, and they would force you into the IO
monad even if you didn't otherwise need it.
Yes, there are programming scenarios where either IORef
or STRef
are preferable to State
, STM
, MVar
, or pure IO
(see below). There are few scenarios where IORef
is obviously preferable to STRef
, but -- as mentioned above -- if you're already in the IO
monad and have a need for true mutable state that's entangled with IO operations, then IORef
probably has the edge over STRef
in terms of slightly cleaner syntax.
Some examples of cases where either IORef
or STRef
is a good approach:
Data.Unique
in the base
package uses an IORef
as a global counter for generating unique objects.
- In the
base
library, the file handle internals make extensive use of IORef
s for attaching buffers to handles. This is a good example of "already being in the IO monad with entangled IO operations".
- Many vector algorithms are most efficiently implemented using mutable vectors (e.g., even something as simple as counting byte frequencies in a block of data). If you use mutable vectors from the
vector
package, then technically you're using mutable byte arrays rather than STRef
or IORef
, but it's still morally equivalent.
- The
equivalence
package uses STRef
s for an efficient implementation of the union-join algorithm.
- As a somewhat on-the-nose example, if you're implementing an interpreter for an imperative language, then using
IORef
or STRef
values for the mutable variables is generally going to be most efficient.