[Haskell] Re: Global Variables and IO initializers
Ben Rudiak-Gould
Benjamin.Rudiak-Gould at cl.cam.ac.uk
Thu Nov 4 17:29:15 EST 2004
Koen Claessen wrote:
>Ben Rudiak-Gould wrote:
>
> | I'm not convinced this is a problem either. All you have
> | to do is use a single parameter (?MyModule.globals ::
> | MyModule.Globals), where MyModule.Globals is an abstract
> | type, and you've hidden your implementation as completely
> | as if you had used unexported global variables.
>
>Are you suggesting to always add the context
>(?MyModule.globals :: MyModule.Globals) to every function in
>every module you implement? (My example concerned a module
>that was previously implemented without global variables,
>and now was going to be implemented with global variables.)
Okay, I see. The implicit parameter approach gives you more flexibility
than the global variable approach, since you can create and use more
than one set of "globals", and supply arguments to the factory function.
If you need that flexibility, obviously you can't avoid changing the
public interface. If you don't need that flexibility, I think real
global variables are fine. I have my own pet proposal for those, after
all. :-)
>I think hiding the fact that certain objects are not
>constants but functions is a bad idea, because it will break
>sharing in a lazy implementation.
Okay, this is a problem. We'd have to tweak the monomorphism restriction
a bit.
> | Adrian Hey proposed a "SafeIO" monad with similar
> | properties to yours. I have the same objection to both of
> | them: a whole new monad and a bunch of interconversion
> | functions seems like overkill for such a minor new
> | language feature.
>
>I was not aware of his proposal. I don't think it is that
>bad:
>
> * 1 new monad
>
> * for each current safe IO operation, 1 new operation
> (read: newIORef. What else?)
At least newMVar, newEmptyMVar, newArray, newArray_, and newListArray.
I'm not sure how you'd handle the last three, since they're overloaded
and I don't think that all of the instances of MArray are safe to create
in CIO.
> | And I have the same counter-proposal: why not use (forall
> | s. ST s)? It's not commutative, but I think it has all of
> | the properties we need.
>
>Interesting idea. However, when I then provide a function
>for creating an IORef (which is what this extension would be
>used for mostly), I get this:
>
> newIORefST :: a -> ST s (IORef a)
>
>Which is probably not what you want.
This is solved by merging the IO and ST monads, something that ought to
be done anyway:
type IO = ST RealWorld
type IORef a = Ref RealWorld a
type STRef s a = Ref s a
newRef :: a -> ST s (Ref s a) -- replaces newIORef and newSTRef
readRef :: Ref s a -> ST s a
writeRef :: Ref s a -> a -> ST s ()
...
A top-level init action would look like
r <- newRef 'x'
The RHS has type (forall s. ST s (Ref s Char)). The runtime system runs
it through (id :: forall a. (forall s. ST s a) -> ST RealWorld a), with
a resulting type of ST RealWorld (Ref RealWorld Char), which is the same
as IO (IORef Char). So r ends up with the type IORef Char.
The same newRef function works in ST monad and IO monad computations.
You don't have to decide ahead of time whether you want the versatility
of ST or the convenience of IO. The compiler will automatically infer a
type of IO x for any function which actually does I/O, and (forall s. ST
s x) for a function which just mucks around with Refs and MArrays. This
is one small step towards getting rid of the current status of IO as a
dumping ground for everything that might need to be used alongside
genuine I/O.
I don't think this even breaks existing code -- though I'm prepared to
be presented with counterexamples.
A slight wart is that we have to move MVars into ST as well if we want
to create them in init actions. This doesn't break anything, but it's a
bit silly because they're basically useless outside IO.
> | So importing a module doesn't have side effects, and init
> | actions can be implemented easily using unsafePerformIO
> | without affecting the semantics.
>
>I don't understand this remark.
This isn't specific to my proposal. I just meant that if we allow
unrestricted IO actions then we have to worry about which ones get run
and when they get run. If we run all actions at the beginning, then
importing a module into your program has side effects (versus not
mentioning it at all). On the other hand if those actions are
appropriately restricted, then the program can't tell whether they've
been run or not, and so importing a module doesn't have side effects,
and also we don't have to worry about the (difficult, inefficient)
engineering problem of making all the actions run before main; we can
run them on demand, as though they were individually wrapped in
unsafePerformIO.
> | Note that the ST monad does not require higher-order
> | polymorphism -- only the runST function requires that. ST
> | is still useful without runST, as this example
> | demonstrates.
>
>So, if I get it right, you want to use (forall s . ST s)
>because it avoids adding yet another monad to Haskell?
Better than that, it reduces the number of monads in Haskell. :-)
John Peterson's post intrigues me, though: maybe there is good reason to
add a CIO monad if we get other benefits from it as well. But I don't
(yet) understand what those benefits are. I'd like to see an example of
what CIO can do that (forall s. ST s) can't. (There are definitely
things that ST can do that CIO can't -- write values into those mutable
arrays before returning them, for example.)
-- Ben
More information about the Haskell
mailing list