Reference types

John Hughes
Wed, 6 Feb 2002 10:09:50 +0100 (MET)

	John Hughes despaired:

	 | Oh no, please don't do this! I use the RefMonad class,
	 | but *without* the dependency r -> m. Why not? Because
	 | I want to manipulate (for example) STRefs in monads
	 | built on top of the ST monad via monad transformers.
	 | So I use the same reference type with *many different*
	 | monads! Your change would make this impossible.

	Ashley Yakeley soothingly suggested:

	 | I don't think so. [...]
	 |     data Ref m a = MkRef
	 |         {
	 |         get :: m a,
	 |         set :: a -> m (),
	 |         modify :: (a -> a) -> m ()
	 |         };

	Koen commented:

	Hm... this looks nice. With slight name changes this

	  data Ref m a =
	      { readRef  :: m a
	      , writeRef :: a -> m ()

No no no! This still makes the reference type depend on the monad type, which
means that I cannot manipulate the same reference in two different monads! One
of the things I want to do sometimes is to, say, add exceptions using a monad
transformer during just *part* of an ST computation. That means I want to
manipulate the *same* references in both the unadorned ST monad and the
transformed one.

	Further, Ashley writes:

	 | The point is that the m -> r dependency is also
	 | unnecessary, except when you want a new "standard" ref
	 | for a monad.

No no no! Well, preferably not. I find that removing that dependency creates a
lot of ambiguity in the overloading: if one uses a reference locally, the
type-checker cannot infer (from the type of the monad) what kind of reference
should be used. That forces a lot of explicit type information to be added,
which just clutters programs unnecessarily.

	Koen says:

	Not really, the m -> r is still there in practise, since you
	want to be able to use the 'readRef' and 'writeRef'
	operators, which work on the monad m, and you want them to
	work on the monad (t m).

	In this case one can simply lift the reference, like this:

	  liftRef :: MonadTrans t => Ref m a -> Ref (t m) a
	  liftRef ref =
	      { readRef  = lift (readRef ref)
	      , writeRef = \a -> lift (writeRef ref a)

	Look, ma, no type classes!

No no no! I don't want to have to *transform* references in order to use them
in another monad! Suppose I've built a large structure, such as a UNION-FIND
forest, and then I want to work in an extended monad for a little. Should I
transform the entire structure in order to continue working on it? Afterwards,
when I return to the underlying monad, I would need to coerce the references
back again... I would need

          dropRef :: MonadTrans t => Ref (t m) a -> Ref m a

How would you provide that?? This is beginning to sound expensive. And all of
this, just because of an arbitrary type distinction between types which are
actually the same!

I'm using monad transformers heavily now, and often switching between
monads. I find it a very modular and attractive way of programming, and in
this context I'm (still) convinced that making reference types depend on the
monad is a Really Bad Idea!

The most reasonable approach I can see if one were to do this, would be just
to manipulate references in the monad they belong to (normally ST or IO), and
explicitly lift reference operations to the current monad at each use. That
could be awkward, since if one has applied several monad transformers then the
lifting might be in several stages (and don't say "overlapping instances"). It
also would hinder one from writing generic reference-using code, that would
work in either an unadorned ST or IO monad or a transformed one. In fact, it
seems to lose most of the benefit of overloading the reference operations in
the first place!