Re: Module Initialisation? (was Re: [Haskell] (no subject))
Adrian Hey wrote:
I'm puzzled about this idea of "module init action" in a declarative language. Perhaps, if it's desirable to have some module initialisation applied to a module if anything from it is used, the way to do this would be to have a reserved identifier specially for the purpose, like "main", but at the module level. (Though this idea still seems a bit strange to me).
I don't see what's so strange about that. At least, it's not any stranger than on-demand execution of IO actions in a pure functional language. And the toplevel "<-" is definitely a natural syntax for that.
I've interpreted this correctly this means that someAction will always get executed, whether or not foo (or anything dependent on foo) is used elsewhere? This seems like a bad thing to me.
It's a feature. I'd actually _want_ to use that one independently.
It may be harmless enough if all it does is create a few IORefs which are then promptly garbage collected, but in some situations it could involve quite complex and expensive initialisation operations on foreign libraries (for example).
Well if you want those IO operations to be lazily interleaved with the rest of the program (or not executed at all), you can use valueToBeInitedLazily <- unsafeInterleaveIO $ do blah ... which explicitly says what you are doing that might be slightly unsafe.
Since a lot of the concerns expressed about this seem to centre around possible abuse of arbitrary IO operations in these top level constructions, maybe the problem could be addressed by insisting that a restricted monad was used, call it SafeIO say.
You're taking away a feature I want to use. 1) Initialising global IORefs is a good thing, no matter when it's actually done. 2) Being able to initialize things at program startup is a good thing, even if they're not explicitly referred to. 3) Lazy, on-demand initialization of things is a good thing, if you know what you're doing. 4) Lazy, on-demand initialization of things (with potential side effects) is a bad thing, if you don't know what you're doing. If we define toplevel IO bindings to be just like the unsafePerformIO hack, we get 1,3 and 4 (and 4 is actually a bad thing). If we define toplevel IO bindings as mdo-style module initialisation, we get 1 and 2 directly, and 3 with an obvious use of unsafeInterleaveIO. We don't get 4, because obviously, when you use unsafeInterleaveIO, you're already claiming to know what you're doing. Also note that if useless initialization of IORefs ever becomes a problem, we can still use lots of nasty hacks inside the compiler to optimize for those common functions. But I don't think we'll ever have to do this. Cheers, Wolfgang
Wolfgang Thaller wrote:
Adrian Hey wrote:
I'm puzzled about this idea of "module init action" in a declarative language. Perhaps, if it's desirable to have some module initialisation applied to a module if anything from it is used, the way to do this would be to have a reserved identifier specially for the purpose, like "main", but at the module level. (Though this idea still seems a bit strange to me).
I don't see what's so strange about that.
I think his point is that some module init actions are run while others aren't, and it's not entirely clear how the distinction should be made. Currently it's possible to think of import declarations as simply bringing names into scope, but in the presence of module init actions import declarations have side effects. In many languages you can refer to a function in another module by its fully qualified name even if that module hasn't been explicitly imported. This is a nice feature and I'd like to see it added to Haskell, but there's a tension between it and the module-init-action feature as currently specified: we would have to look through the whole program source for fully-qualified names in order to figure out which module actions will be run. There would be no semantic problem if all module actions were always run, but this is impossible because it would have to include modules not installed on the user's machine, as well as modules that haven't even been written yet. There is also no semantic problem if only "safe" actions like newIORef are used, but the current proposal provides no static guarantee of this. In any case, I have an idea of how to solve this problem -- see below.
Since a lot of the concerns expressed about this seem to centre around possible abuse of arbitrary IO operations in these top level constructions, maybe the problem could be addressed by insisting that a restricted monad was used, call it SafeIO say.
How about (forall s. ST s)? We can require module init actions to have a type (forall s. ST s a) instead of IO a. The compiler or RTS wraps the actions with stToIO (which is a safe function) before executing them. Benefits: * It's just as easy as before to allocate global refs (and global mutable arrays). * It's still possible to perform arbitrary IO actions (e.g. FFI calls), but you have to wrap them in unsafeIOToST -- a good thing since they really are unsafe. unsafeIOToST is much safer than unsafePerformIO when used in this way. Problems: * stToIO (newSTRef 'x') doesn't have type IO (IORef Char). This problem can be solved by adopting a reform that I've wanted for a long time anyway: make IO, IORef, etc. aliases for (ST RealWorld), (STRef RealWorld), etc. instead of completely different types. Then stToIO is the identity function and we only need a single set of state-thread functions instead of the parallel IO and ST equivalents that we have currently. -- Ben
On Sun, Oct 17, 2004 at 01:53:22PM +0100, Ben Rudiak-Gould wrote: [snip]
Since a lot of the concerns expressed about this seem to centre around possible abuse of arbitrary IO operations in these top level constructions, maybe the problem could be addressed by insisting that a restricted monad was used, call it SafeIO say.
How about (forall s. ST s)?
We can require module init actions to have a type (forall s. ST s a) instead of IO a. The compiler or RTS wraps the actions with stToIO (which is a safe function) before executing them.
Benefits:
* It's just as easy as before to allocate global refs (and global mutable arrays). * It's still possible to perform arbitrary IO actions (e.g. FFI calls), but you have to wrap them in unsafeIOToST -- a good thing since they really are unsafe. unsafeIOToST is much safer than unsafePerformIO when used in this way.
Problems:
* stToIO (newSTRef 'x') doesn't have type IO (IORef Char).
This problem can be solved by adopting a reform that I've wanted for a long time anyway: make IO, IORef, etc. aliases for (ST RealWorld), (STRef RealWorld), etc. instead of completely different types. Then stToIO is the identity function and we only need a single set of state-thread functions instead of the parallel IO and ST equivalents that we have currently.
It definitely sounds nice, but is it actually possible to generalize e.g. MVar from "RealWorld" to "forall s" or are we always going to have to say: v <- unsafeIOToST (newMVar / newChan ... ) GHC's definition: data MVar a = MVar (MVar# RealWorld a) -- Nobody can be exactly like me. Even I have trouble doing it.
Remi Turk wrote:
It definitely sounds nice, but is it actually possible to generalize e.g. MVar from "RealWorld" to "forall s" or are we always going to have to say:
v <- unsafeIOToST (newMVar / newChan ... )
I hadn't thought of that, but I don't think there's any problem with type MVar = STMVar RealWorld newMVar :: a -> ST s (STMVar s a) withMVar :: STMVar s a -> (a -> ST s b) -> ST s b ... For that matter it seems like we could (should?) have forkST :: ST s () -> ST s (STThreadId s) forkIO = forkST and so on. -- Ben
On Sun, Oct 17, 2004 at 05:11:02PM +0100, Ben Rudiak-Gould wrote:
Remi Turk wrote:
It definitely sounds nice, but is it actually possible to generalize e.g. MVar from "RealWorld" to "forall s" or are we always going to have to say:
v <- unsafeIOToST (newMVar / newChan ... )
I hadn't thought of that, but I don't think there's any problem with
type MVar = STMVar RealWorld
newMVar :: a -> ST s (STMVar s a) withMVar :: STMVar s a -> (a -> ST s b) -> ST s b ...
For that matter it seems like we could (should?) have
forkST :: ST s () -> ST s (STThreadId s) forkIO = forkST
and so on.
-- Ben
But what semantics would they have? It cannot be the normal concurrency as AFAIK runST is supposed to be deterministic. Groetjes, Remi -- Nobody can be exactly like me. Even I have trouble doing it.
Remi Turk wrote:
On Sun, Oct 17, 2004 at 05:11:02PM +0100, Ben Rudiak-Gould wrote:
I don't think there's any problem with
type MVar = STMVar RealWorld
newMVar :: a -> ST s (STMVar s a) withMVar :: STMVar s a -> (a -> ST s b) -> ST s b ...
For that matter it seems like we could (should?) have
forkST :: ST s () -> ST s (STThreadId s) forkIO = forkST
But what semantics would they have? It cannot be the normal concurrency as AFAIK runST is supposed to be deterministic.
Okay, so I'm being silly. Forget forkST then. But STMVar is still okay, isn't it? The only MVars you could use in a state thread would be those you'd created in the same state thread, and without forkST they can't be accessed in a nondeterministic way. Their presence is pointless, true, but at least not unsafe. It does seem a bit of a hack, but it still seems preferable to the other alternatives currently on the table (namely unrestricted IO, a new SafeIO, or unsafeIOToST.newMVar). -- Ben
On Sun, Oct 17, 2004 at 07:20:28PM +0100, Ben Rudiak-Gould wrote:
Remi Turk wrote:
On Sun, Oct 17, 2004 at 05:11:02PM +0100, Ben Rudiak-Gould wrote:
I don't think there's any problem with
type MVar = STMVar RealWorld
newMVar :: a -> ST s (STMVar s a) withMVar :: STMVar s a -> (a -> ST s b) -> ST s b ...
For that matter it seems like we could (should?) have
forkST :: ST s () -> ST s (STThreadId s) forkIO = forkST
But what semantics would they have? It cannot be the normal concurrency as AFAIK runST is supposed to be deterministic.
Okay, so I'm being silly. Forget forkST then. But STMVar is still okay, isn't it? The only MVars you could use in a state thread would be those
I won't ever remind you of your being silly if you tell me about the current state of your implicit-(parameter|return)-IO story ;) STMVar does indeed still seem okay, except that I have no idea if it actually makes any sense outside of IO. (That is: newSTMVar x = unsafeIOToST (newMVar x) seems a bit pointless and might be the actual way it needs to be implemented. Has a vague feeling of being silly too now...) By the way, I'm still in favour of `type IO a = ST RealWorld a': It just seems wrong to either "let's just make it IO" or having to sprinkle stToIO's around...
you'd created in the same state thread, and without forkST they can't be accessed in a nondeterministic way. Their presence is pointless, true, but at least not unsafe.
It does seem a bit of a hack, but it still seems preferable to the other alternatives currently on the table (namely unrestricted IO, a new SafeIO, or unsafeIOToST.newMVar).
-- Ben
I'm waiting to be convinced either way ;) Groetjes, Remi -- Nobody can be exactly like me. Even I have trouble doing it.
On Sunday 17 Oct 2004 4:45 am, Wolfgang Thaller wrote:
Adrian Hey wrote:
I'm puzzled about this idea of "module init action" in a declarative language. Perhaps, if it's desirable to have some module initialisation applied to a module if anything from it is used, the way to do this would be to have a reserved identifier specially for the purpose, like "main", but at the module level. (Though this idea still seems a bit strange to me).
I don't see what's so strange about that.
What's strange about it IMHO is that at the moment the mere presence of some definition in a module has no effect on actual programs. What counts is whether or not defined thing is actually connected to the top level main via some chain of explicit (I.E. named) dependency. This is a property I would like to preserve. If I've understood the proposal correctly, we'd lose this.
At least, it's not any stranger than on-demand execution of IO actions in a pure functional language. And the toplevel "<-" is definitely a natural syntax for that.
True, but maybe this isn't neccessary, and even if it is, performing the IO creation acts only if the created thing is actually used by a program still seems the lesser of two evils to me. Of course "used" could mean one of at least two different things.. 1- The compiler does some kind of dependency analysis and mops up all referenced "things which must be created" into some kind of pre-main action. 2- Things which must be created are just compiled into some kind of thunk which gets reduced (post-main), if and when it is actually required by a running program. Option 1 seems like quite a difficult thing to implement, but does have the advantage that well defined semantics could be given to arbitrary IO operations. It also has the disadvantage that a some of these may be redundant (just because something is referenced doesn't imply that is will actually be used in any given program run). Option 2 seems easier to implement and is nice and lazy, but suffers from semantic ambiguity if arbitrary IO operations are allowed (encouraged). But do we need to do this? I'm begining think maybe we don't. The only reason it seems like we do is because currently the only way of creating IORefs and wotnot is via the IO monad, but this need not be so IMO.
You're taking away a feature I want to use.
Sorry, I didn't mean to deprive you of something you would find useful. But maybe it should be considered as a separate issue. It's not clear to me how the compiler (or Joe programmer for that matter) would determine and/or control which modules initialisation actions would be executed or not, if there's no obvious "connection" to the top level main.
1) Initialising global IORefs is a good thing, no matter when it's actually done.
Yes, the hypothetical SafeIO monad allows you to do this (what you can't do is read or write IORefs).
2) Being able to initialize things at program startup is a good thing, even if they're not explicitly referred to.
Well here is where we disagree I think. Not that I think this is a bad thing as such, but the rules that determine which modules init actions do or don't get invoked seem quite unclear to me if there's no requirement that they are referenced from "main", directly or indirectly. But maybe you could clarify what you have in mind? Regards -- Adrian Hey
participants (4)
-
Adrian Hey -
Ben Rudiak-Gould -
Remi Turk -
Wolfgang Thaller