Initialisation without unsafePerformIO
What ideas do people have for getting rid of unsafePerformIO? The most common use of unsafePerformIO, for me at least, is initialisation. There *surely* must be a better way of doing this, but I haven't really seen much discussion of the topic. Here is my back-of-the-envelope suggestion for a new interface, can anyone do better?
type Dict -- a collection of initialised data.
register :: Typeable a => Dict -> a -> IO () -- register a value of type (a) in the dictionary. Only one value of each -- type is allowed in the dictionary; registering the same type twice will -- cause an exception.
defaultDict :: IO Dict -- Each Haskell "main" program will have one of these.
lookup :: Typeable a => Dict -> IO a -- Get the value of (a) registered in the Dict, or raise an exception if it -- isn't.
Thus, libraries which rely on internal initialised state will also have to provide a function which initialises that state. I don't think this is very painful. IME it's often required anyway, if you want to provide the library with additional parameters. Also the library could catch the exception from (lookup) and give a helpful error message of the form "You forgot to run Gadgets.initialise". We can also provide additional dictionaries.
thisThreadDict :: IO Dict
newEmptyDict :: IO Dict
runWithDifferentDefaultDict :: Dict -> IO a -> IO a
This would allow the programmer much more control over initialisation. For example, programs distributed over a large number of processors are no longer obliged to use a single global dictionary, which is effectively what is required now. (How else can you make sure that two processors do not try to evaluate the same unsafePerformIO value at once?) And runWithDifferentDefaultDict allows you to change the value returned by defaultDict during an action, meaning that programs can for example initialise the same library multiple times, which would be useful during debugging. Well it's a start I think. Can anyone do better?
On Tue, Jun 01, 2004 at 06:06:36PM +0200, George Russell wrote:
What ideas do people have for getting rid of unsafePerformIO?
The most common use of unsafePerformIO, for me at least, is initialisation. There *surely* must be a better way of doing this, but I haven't really seen much discussion of the topic. Here is my back-of-the-envelope suggestion for a new interface, can anyone do better?
I am a fan of allowing top level declarations of the form: foo <- newIORef "foo" which would behave as an initializer, with the semantics being that it be evaluated at most once before foos first use. (so it could be implemented via unsafePerformIO or as an init section run before main). The {-# NOINLINE foo #-} foo = unsafePeformIO $ newIORef "foo" idiom is so common and useful, it should have some compiler support. It is 'clean' too, since all we are doing is extending the "world" with new state, but in a much cleaner/safer way then writing to a file or environment variable or other methods of storing state in the world. John -- John Meacham - ⑆repetae.net⑆john⑈
In article <20040601163515.GA8357@momenergy.repetae.net>, John Meacham <john@repetae.net> wrote:
I am a fan of allowing top level declarations of the form:
foo <- newIORef "foo"
which would behave as an initializer, with the semantics being that it be evaluated at most once before foos first use. (so it could be implemented via unsafePerformIO or as an init section run before main).
The {-# NOINLINE foo #-} foo = unsafePeformIO $ newIORef "foo"
idiom is so common and useful, it should have some compiler support. It is 'clean' too, since all we are doing is extending the "world" with new state, but in a much cleaner/safer way then writing to a file or environment variable or other methods of storing state in the world.
Clean it is not: foo :: a foo <- newIORef undefined writeChar :: Int -> IO () writeChar x = writeIORef foo x readString :: IO String readString = readIORef foo cast :: Char -> IO String cast c = (writeChar c) >> readString -- Ashley Yakeley, Seattle WA
On Fri, 04 Jun 2004 00:35:14 -0700, Ashley Yakeley <ashley@semantic.org> wrote:
In article <20040601163515.GA8357@momenergy.repetae.net>, John Meacham <john@repetae.net> wrote:
I am a fan of allowing top level declarations of the form:
foo <- newIORef "foo"
which would behave as an initializer, with the semantics being that it be evaluated at most once before foos first use. (so it could be implemented via unsafePerformIO or as an init section run before main).
The {-# NOINLINE foo #-} foo = unsafePeformIO $ newIORef "foo"
idiom is so common and useful, it should have some compiler support. It is 'clean' too, since all we are doing is extending the "world" with new state, but in a much cleaner/safer way then writing to a file or environment variable or other methods of storing state in the world.
Clean it is not:
foo :: a foo <- newIORef undefined
writeChar :: Int -> IO () writeChar x = writeIORef foo x
readString :: IO String readString = readIORef foo
cast :: Char -> IO String cast c = (writeChar c) >> readString
If we check that the type of "foo" is monomorphic, this is no longer unsafe. -- Daan.
On Fri, Jun 04, 2004 at 12:35:14AM -0700, Ashley Yakeley wrote:
In article <20040601163515.GA8357@momenergy.repetae.net>, John Meacham <john@repetae.net> wrote:
I am a fan of allowing top level declarations of the form:
foo <- newIORef "foo"
which would behave as an initializer, with the semantics being that it be evaluated at most once before foos first use. (so it could be implemented via unsafePerformIO or as an init section run before main).
The {-# NOINLINE foo #-} foo = unsafePeformIO $ newIORef "foo"
idiom is so common and useful, it should have some compiler support. It is 'clean' too, since all we are doing is extending the "world" with new state, but in a much cleaner/safer way then writing to a file or environment variable or other methods of storing state in the world.
Clean it is not:
foo :: a foo <- newIORef undefined
writeChar :: Int -> IO () writeChar x = writeIORef foo x
readString :: IO String readString = readIORef foo
cast :: Char -> IO String cast c = (writeChar c) >> readString
Yeah, such an extension would need to ensure initializers are monomorphic. another advantage of a special syntax rather than unsafePerformIO. John -- John Meacham - ⑆repetae.net⑆john⑈
On 2004-06-04T03:35:25-0700, John Meacham wrote:
Yeah, such an extension would need to ensure initializers are monomorphic. another advantage of a special syntax rather than unsafePerformIO.
A next thing to worry about is the order of initialization, especially when mutually recursive imports are involved. -- Edit this signature at http://www.digitas.harvard.edu/cgi-bin/ken/sig unsafePerformIO is NOT Haskell!
I don't see how this technique can be at all safe: instance ReflectStorable s => Reflect (Stable s a) a where reflect = unsafePerformIO $ do a <- deRefStablePtr p freeStablePtr p return (const a) where p = reflectStorable (undefined :: s p) reify :: a -> (forall s. Reflect s a => s -> w) -> w reify (a :: a) k = unsafePerformIO $ do p <- newStablePtr a reifyStorable p (\(_ :: s p) -> k' (undefined :: Stable s a)) where k' (s :: s) = (reflect :: s -> a) `seq` return (k s) The above means that a StablePtr will be allocated once per reify and destroyed once per reflect; I don't see how the code can guarantee that there will be only one reflect per reify. In fact, it seems quite likely that there will be far more reflects than reifys. On Fri, 4 Jun 2004 03:35:25 -0700, John Meacham <john@repetae.net> wrote:
On Fri, Jun 04, 2004 at 12:35:14AM -0700, Ashley Yakeley wrote:
In article <20040601163515.GA8357@momenergy.repetae.net>, John Meacham <john@repetae.net> wrote:
I am a fan of allowing top level declarations of the form:
foo <- newIORef "foo"
which would behave as an initializer, with the semantics being that it be evaluated at most once before foos first use. (so it could be implemented via unsafePerformIO or as an init section run before main).
The {-# NOINLINE foo #-} foo = unsafePeformIO $ newIORef "foo"
idiom is so common and useful, it should have some compiler support. It is 'clean' too, since all we are doing is extending the "world" with new state, but in a much cleaner/safer way then writing to a file or environment variable or other methods of storing state in the world.
Clean it is not:
foo :: a foo <- newIORef undefined
writeChar :: Int -> IO () writeChar x = writeIORef foo x
readString :: IO String readString = readIORef foo
cast :: Char -> IO String cast c = (writeChar c) >> readString
Yeah, such an extension would need to ensure initializers are monomorphic. another advantage of a special syntax rather than unsafePerformIO.
John
-- John Meacham - ⑆repetae.net⑆john⑈ _______________________________________________ Haskell mailing list Haskell@haskell.org http://www.haskell.org/mailman/listinfo/haskell
On 2004-06-04T13:56:34-0400, Abraham Egnor wrote:
I don't see how this technique can be at all safe:
instance ReflectStorable s => Reflect (Stable s a) a where reflect = unsafePerformIO $ do a <- deRefStablePtr p freeStablePtr p return (const a) where p = reflectStorable (undefined :: s p)
reify :: a -> (forall s. Reflect s a => s -> w) -> w reify (a :: a) k = unsafePerformIO $ do p <- newStablePtr a reifyStorable p (\(_ :: s p) -> k' (undefined :: Stable s a)) where k' (s :: s) = (reflect :: s -> a) `seq` return (k s)
The above means that a StablePtr will be allocated once per reify and destroyed once per reflect; I don't see how the code can guarantee that there will be only one reflect per reify. In fact, it seems quite likely that there will be far more reflects than reifys.
But (in a typical Haskell implementation) the action supplied as an argument to unsafePerformIO is only performed once. The result is memoized for future use. In particular, "reflect" only calls "freeStablePtr" the first time it is invoked; thereafter it simply produces the function "const a" that was computed the first time. You can easily verify for yourself that "deRefStablePtr" is called exactly once per "newStablePtr", regardless of whether the reified value is looked up once, multiple times, or not at all. (The "not at all" case is dealt with by the "seq".) In any case, if you don't trust this explanation, then you can use the preceding pieces of code in Section 4. The memory leak that this code eliminates only exists for non-serializable data types, and only significant if your program generates and discards many sets of non-serializable parameters outside the IO monad over its lifetime. Oleg + Ken -- Edit this signature at http://www.digitas.harvard.edu/cgi-bin/ken/sig I HATE SIGNATURES. AND I HATE MYSELF FOR USING THEM.
On 2004-06-01T18:06:36+0200, George Russell wrote:
The most common use of unsafePerformIO, for me at least, is initialisation. There *surely* must be a better way of doing this, but I haven't really seen much discussion of the topic. Here is my back-of-the-envelope suggestion for a new interface, can anyone do better?
Oleg and I summarize the existing solutions to the configuration problem in Section 2 of our draft paper at http://www.eecs.harvard.edu/~ccshan/prepose/ We then propose a new solution. Implicit configuration -- or, type classes reflect the value of types The _configuration problem_ is to propagate run-time preferences throughout a program, allowing multiple concurrent configuration sets to coexist safely under staticly guaranteed separation. This problem is common in all software systems, but particularly acute in Haskell, where currently the most popular solution relies on unsafe operations and compiler pragmas. We solve the configuration problem in Haskell using only widely implemented and stable language features like the type class system. In our approach, a term expression can refer to run-time configuration parameters just as it refers to compile-time constants in global scope. Besides supporting such intuitive term notation, our solution also helps improve the program's performance by transparently dispatching to specialized code at run-time. We can propagate any type of configuration data -- numbers, strings, IO actions, polymorphic functions, closures, and abstract data types. The enabling technique behind our solution is to propagate values via types (literally), with the help of polymorphic recursion and higher-ranked polymorphism. The technique essentially emulates local type-class instance declarations. Configuration parameters are propagated throughout the code implicitly as part of type inference rather than explicitly by the programmer. We are thinking of submitting this paper to the Haskell workshop. Any comments would be appreciated, especially before Friday. (: Ken -- Edit this signature at http://www.digitas.harvard.edu/cgi-bin/ken/sig 2004-06-05: World Environment Day http://www.unep.org/wed/2004/ 1 000 000 Europeans demand the exit of nuclear power www.atomstopp.at/1million/
What ideas do people have for getting rid of unsafePerformIO?
Hope my suggestion is not too naive. I get along quite fine using implicit parameters in many cases, it's just tedious explicitly typing them in every function context. I'd be pretty happy if it was possible to define the 'scope' of some implicit parameters in a module and/or define their scope as being a whole module. The 2nd option would be something like:
module (?par :: Parameter) => A where ...
Functions in A could have ?par in their context without having it explicitly typed. Now the import of A could be done with:
module B where
import A -- simple, ?par unbound import A as Ak where ?par = k -- ?par bound to k import A as Am where ?par = m -- ?par bound to m
...
k :: Parameter k = ...
m :: Parameter m = ... ...
Also,
module (?par :: Parameter) => C where import A -- both A and C paremeterised by ?par
Since both modules share the same parameter, instantiation on ?par in the import of C would propagate to the import of A. At first glance it seems simple syntactic sugar and therefore doable. Along with some options in the interpreter to hide/show (this kind of) implicit parameters when displaying signatures, check module context, etc. probably also quite usable. J.A.
participants (7)
-
Abraham Egnor -
Ashley Yakeley -
Chung-chieh Shan -
Daan Leijen -
George Russell -
John Meacham -
Jorge Adriano Aires