Nailing down what we expect IO to do and not do - and why

Christopher Allen cma at
Sun Jan 31 01:56:56 UTC 2016

I'm writing a book, I'd like to get this nailed down and to get it right.
If anyone on here that's familiar with the various ways in which
IO/State#/realWorld# work in GHC and you have time to reply, anything at
all would be welcome. Any pointers, links, references, details, anecdotes,
or faint memories of GHC bugs will be greatly appreciated! Getting this
written up (possibly for addition to Michael Snoyman's wiki article?) would
make me, and I imagine others, a lot happier with trying to understand how
the different bits and bobs fit together.

I will be dumping my notes as I don't want to get linked to stuff that can
be googled because I've already lost 10-15 hours to just that in the past
3-4 days. Digging it up in the compiler is hard because compiler behavior
that influences how IO actions are treated don't necessarily have "IO" or
"realWorld" mentioned in the relevant parts of the compiler, optimizations,

What I'm hoping for is answers on what specifically preserves the listed
properties we want from IO in the compiler, prims, or structure of how we
write IO actions.

What we expect IO to do:

- Disable sharing of results, even when it's not a lambda and is evaluated
multiple times by the same name. ie, getCurrentTime :: IO UTCTime   should
get evaluated more than once.

- Not reorder sequential IO actions, such as in a do-block. Called
"linearity" below

- Not duplicate the effects of IO actions. Effects shouldn't be spuriously
duplicated during optimization passes.

- Effects should not be discarded separately of the value returned by an IO
action, merged, or elided.

# Sharing

A friend suggested that perhaps one-shot semantics via the state hack for
State# in the IO type is responsible for disabling sharing, I don't believe
so, but here are my notes.

>Turn off the "state hack" whereby any lambda with a State# token as
argument is considered to be single-entry, hence it is considered OK to
inline things inside it. This can improve performance of IO and ST monad
code, but it runs the risk of reducing sharing.

>A one shot lambda
>State hack, makes the lambda over State# assume it's one-shot universally
by default.
>one-shot/state hack is an anti-inlining heuristic, suggesting that
inlining is costly.

Also I found this on Trac, does anyone know the answer to this? Is the
summary above accurate?

>Can the IO state hack be avoided if oneShot is used in the right places in
library code, e.g. in IO’s definition of >>=?

This seems related how the state token works, for differentiating which IO
action is which and how many times an IO action should run, when it should
run, etc.

>From the prims:

>data State# s

>State# is the primitive, unlifted type of states. It has one type
parameter, thus State# RealWorld, or State# s, where s is a type variable.
The only purpose of the type parameter is to keep different state threads
separate. It is represented by nothing at all.

>data RealWorld

>RealWorld is deeply magical. It is primitive, but it is not unlifted
(hence ptrArg). We never manipulate values of type RealWorld; it's only
used in the type system, to parameterise State#.

# Linearity

Is this from the nesting of lambdas? It doesn't seem like that's enough
based on the various examples using State/State# in GHC Trac bug tickets.
The RealWorld token seems to be what's driving this but precisely how that
works hasn't been easy to find.

# Discarding, not inlining effects

I believe these are addressed by has_side_effects in the prim ops. I could
very well be wrong.

            can_fail     has_side_effects
Discard        NO            NO
Float in       YES           YES
Float out      NO            NO
Duplicate      YES           NO

* Duplication.  You cannot duplicate a has_side_effect primop.  You
  might wonder how this can occur given the state token threading, but
  just look at Control.Monad.ST.Lazy.Imp.strictToLazy!  We get
  something like this
        p = case readMutVar# s v of
              (# s', r #) -> (S# s', r)
        s' = case p of (s', r) -> s'
        r  = case p of (s', r) -> r

I believe duplication addresses inlining IO actions more generally but I
could be wrong. Here's a note I found regarding elision/merging:

  * Use the compiler flag @-fno-cse@ to prevent common sub-expression
        elimination being performed on the module, which might combine
        two side effects that were meant to be separate.  A good example
        is using multiple global variables (like @test@ in the example

Any help or pointers for nailing down and documenting this would be greatly
appreciated. Also if there's a more detailed explanation of what behavior
is expected out of each unsafe function, that would help as well. There are
bits and pieces I've been able to aggregate from the GHC trac tickets.

References used (not exhaustive):

- Referential Transparency; Haskell Wiki

- IO Inside; Haskell Wiki

- Unraveling the mystery of the IO Monad; Edward Z. Yang

- Evaluation order and state tokens; Michael Snoyman

- Haskell GHC Illustrated; Takenobu Tani

- Tackling the Awkward Squad; Simon Peyton Jones

- Note [IO hack in the demand analyser]; GHC source code

- Monadic I/O in Haskell 1.3; Andrew D. Gordon and Kevin Hammond

- Haskell Report 1.2

Thank you for your time,
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <>

More information about the ghc-devs mailing list