Referential transparency is an oft-touted property of (pure) functional languages, which makes it easier to reason about the behavior of programs. While there is no single formal definition, a concise one is given by F. Warren Burton:
Side effects like (uncontrolled) imperative update break this desirable property. C and ML are languages with constructs that are not referentially transparent.
As an example, consider the following program in Standard ML:
puts "h"; puts "a"; puts "h"; puts "a"
"haha". In an attempt to factor out the repetition, we write
let val x = (puts "h"; puts "a") in x; x end
but now the laugh is on us, because
"ha" is only printed once. The reason is that
puts's side effect is only realized when
x gets bound, so we should have written
let fun x () = (puts "h"; puts "a") in x (); x () end
Haskell's monadic I/O system distinguishes between values and
actions like the
puts procedure above. So we do indeed have that
putStr "h" >> putStr "a" >> putStr "h" >> putStr "a"
is equivalent to
let x = putStr "h" >> putStr "a" in x >> x
 Wolfram Kahl provides a USENET post by Tom DeBoni containing a summary of various definitions for referential transparency.
 From Nondeterminism with Referential Transparency in Functional Programming Languages, The Computer Journal, 31(3):243--247, January 1988.
puts can be defined as
fun puts s = TextIO.output(TextIO.stdOut, s);
 There is some debate about whether the imprecisely-defined semantics of
Int breaks referential transparency. For instance,
even (maxBound :: Int) may be
True in some contexts and
False in others. Another example is
System.Info.os :: String.
 One perspective is that Haskell is not just one language (plus
Prelude), but a family of languages, parameterized by a collection of implementation-dependent parameters.
Each such language is referentially transparent, even if the collection as a whole might not be.
Some people are satisfied with this situation and others are not.