I always thought that replacing an expression x :: () with () :: () would be one of the most basic optimizations during compiling Haskell programs. Since () has a single inhabitant, no matter what x is, it’s result is (). This optimization seemed to me as a significant consequence of referential transparency. And we could do such an optimization for any type with just a single inhabitant.
(Update: My reasoning in this matter comes from natural deduction rules. There the unit type corresponds to truth (⊤) and we have an expansion rule “if x : ⊤ then () : ⊤“. For example see this text p. 20. I assumed that it’s safe to replace an expression with its expansion or contractum.)
One consequence of this optimization would be that undefined :: () would be replaced by () :: (), but I don’t see this as a problem – it would simply make programs a bit lazier (and relying on undefined :: () is certainly a bad programming practice).
However today I realized that such optimization would break Control.Seq completely. Strategy is defined as
type Strategy a = a -> ()
and we have
-- | 'rseq' evaluates its argument to weak head normal form.
rseq :: Strategy a
rseq x = x `seq` ()
But rseq x :: () so the optimization would simply discard the required evaluation of x to WHNF.
So where is the problem?
- Does the existence of
rseqandseqbreak referential transparency (even if we consider only terminating expressions)? - Or is this a flaw of the design of
Strategyand we could devise a better way how to force expressions to WHNF compatible with such optimizations?
Referential transparency is about equality statements and variable references. When you say x = y and your language is referentially transparent, then you can replace every occurence of x by y (modulo scope).
If you haven’t specified
x = (), then you can’t replacexby()safely, such as in your case. This is because you are wrong about the inhabitants of(), because in Haskell there are two of them: One is the only constructor of(), namely(). The other is the value that is never calculated. You may call it bottom or undefined:You certainly can’t replace any occurrence of
xby()here, because that would chance semantics. The existence of bottom in the language semantics allows some awkward edge cases, especially when you have aseqcombinator, where you can even prove every monad wrong. That’s why for many formal discussions we disregard the existence of bottom.However, referential transparency does not suffer from that. Haskell is still referentially transparent and as such a purely functional language.