
My intuition says that none of the side-effects in my implementation are visible from the abstract level of the module. However, the use of unsafePerformIO certainly modifies the behaviour of the module. For example, the following definitions at the beginning of a GHCi session on the attached code lead to the subsequent behaviour:
foo1 <- return (unsafePerformIO (mkAtom "foo")) foo2 <- return (unsafePerformIO (mkAtom "foo")) bar <- return (unsafePerformIO (mkAtom "bar")) safefoo1 <- mkAtom "foo" safefoo2 <- mkAtom "foo" safebar <- mkAtom "bar" list <- return [safefoo1, safefoo2, safebar, foo1, foo2, bar]
These uses of unsafePerformIO really *are* unsafe though :-) Your implementation of mkAtom seems to be similar the the StableName library, in that it has the property that if mkAtom e == mkAtom e', then e == e' but the reverse isn't necessarily true (it may or may not be true). Which is why mkAtom has to be in the IO monad: it has observable non-determinism. FastString works differently: it guarantees that both mkFastString s == mkFastString s' => s == s' s == s' => mkFastString s == mkFastString s' hold. So it is safe for mkFastString to be a pure non-I/O function, because it doesn't have any observable non-deterministic behaviour. Cheers, Simon