Sharing means that temporary data is stored if it is going to be used multiple times. That is, a function evaluates it's arguments only once. An example would be:
let x = sin x in x * x
What other features contribute to sharing and how would they interact with the need for practical programs to perform IO?
The clearest example of sharing in functional programming comes from Clean, which is based on graph rewriting. There, a computation refers to a DAG, so we can view the expression (sin x) * (sin x)
as
(*)
/ \
sin x sin x
Graph rewriting systems have an explicit notion of sharing, so we could express that computation as
(*)
/ \
\ /
sin x
pointing the multiplication node to the same node, thereby sharing the computation of sin x
. Term rewriting systems do not have so explicit a notion of sharing, but the optimization is still relevant. In GHC, one can sometimes express sharing with local variables, e.g. rewriting
f x = (sin x) * (sin x)
into
f x = sinx * sinx
where sinx = sin x
But since the two are semantically equivalent, the compiler is free to implement both the same way, with or without sharing. By my understanding, the GHC will generally preserve sharing introduced with local variables and sometimes introduce it (adding sharing to the first), but with no formal expression of sharing in term rewriting systems either behaviour is implementation dependent (see tel's comment and answer).
Sharing touches IO because side-effecting values cannot be shared. If we consider an impure language, there is a difference between
(string-append (read-line)
(read-line))
and
(let ((s (read-line)))
(string-append s s))
The first executes the IO action twice, requesting two lines from the user and appending them; the second "shares" the IO action, executing it once and appending it to itself. In general, sharing a pure computation reduces execution time without changing the result of the program, while sharing a side-effecting value (one that may change over time, or interacts with the user) changes the result. For the compiler to automatically share computations, it needs to know that they are pure, and thus that reducing the number of evaluations does not matter. Clean does this with uniqueness types; an IO action has the type attribute UNQ, which tells the compiler that it should not be shared. Haskell does the same somewhat differently with the IO monad.