I think it will turn out to be less surprising if you try to write a function fmapTVar :: (a -> b) -> TMVar a -> TMVar b. The problem is that TMVar itself lives in some global state, i.e. the STM Monad. We cannot map over it without modifying that state, too. So what you end up doing is just to use the state that the variable lives in.
A mutable reference cannot safely support a type-changing map operation in a non-linear context.
In the case that the implementation does not copy, the issue is that the original reference you mapped over still exists at the original type, and can still be written to or read from. Suppose we have:
mapIORef :: (a -> b) -> IORef a -> IORef b
Then:
unsafeCoerceIO :: a -> IO b
unsafeCoerceIO x = do
refb <- newIORef undefined
let refa = mapIORef (const x) refb
readIORef refb
In the case that the implementation does copy, though we can no longer unsafely coerce, there are still two issues: purity and law. The Functor-compatible signature I gave above for mapIORef cannot satisfy the Functor law fmap id = id if it copies. Worse, it’s inherently impure: mapIORef f x /= mapIORef f x. You can resolve the matter of impurity by changing the signature to (a -> b) -> IORef a -> IO (IORef b), but that’s no fmap.
With some minor translation, this applies equally to MVar and TMVar.
Fantastic answers, what a great read. What I take is that the issue is really that IORefs et al. are truly mutable, so Simon shifts focus from directly handling mutable state to mapping over a computation (and such a mapping can be pure - fmap)!