A way to avoid a common use of unsafePerformIO
Asked Answered
A

5

19

I often find this pattern in Haskell code:

options :: MVar OptionRecord
options = unsafePerformIO $ newEmptyMVar

...

doSomething :: Foo -> Bar
doSomething = unsafePerformIO $ do
  opt <- readMVar options
  doSomething' where ...

Basically, one has a record of options or something similar, that is initially set at the program's beginning. As the programmer is lazy, he doesn't want to carry the options record all over the program. He defines an MVar to keep it - defined by an ugly use of unsafePerformIO. The programmer ensures, that the state is set only once and before any operation has taken place. Now each part of the program has to use unsafePerformIO again, just to extract the options.

In my opinion, such a variable is considered pragmatically pure (don't beat me). Is there a library that abstracts this concept away and ensures that the variable is set only once, i.e. that no call is done before that initialization and that one doesn't have to write unsafeFireZeMissilesAndMakeYourCodeUglyAnd DisgustingBecauseOfThisLongFunctionName

Allx answered 20/5, 2011 at 18:23 Comment(11)
Much was said on the simpler issue of just IO initializers in an old thread. I don't think anything was ever happily solved.Landsman
Is this the "configuration" problem? Oleg Kiselyov and Chung-chieh Shan had a functional pearl about it a few years ago called "Implicit Configurations" - cs.rutgers.edu/~ccshan/prepose/p1214-kiselyov.pdfBristow
@stephen tetley: That's useful. +1Allx
common use? citation needed..Ctesiphon
@yairchu: I saw this pattern quite often. Some people described it as "the only way in Haskell to use global variables"Allx
@yairchu: Yeah, I don't really buy "common use" either. unsafePerformIO in top-level bindings for certain limited kinds of initialization I can imagine, like creating an IORef. Top level bindings doing arbitrary IO wouldn't surprise me, but it seems unwise. Using unsafePerformIO to read bits of potentially mutable data from inside assorted functions is just ridiculous.Roselynroseman
My philosophical response to this question: lukepalmer.wordpress.com/2011/05/20/the-whole-program-fallacyLienlienhard
+1 for a good question. I don't think that it's a good way to design things, but people wanting to do things like this is common enough to deserve some thoughtful responses. A SO question is as good a place as any to ask the dual questions of whether and how to do this and record the various responses.Rioux
A similar problem (a mutable global variable) is further discussed on the Haskell wiki.Humidity
@FUZxxl - I'm trying to clean up the pure tag - see meta for more info. I don't know anything about haskall, so can I ask for your advice on other tags to use on this question? Would purely-functional or pure-function or pure-procedure work on this question?Feign
@RichardJPLeGuen [purely functional] is okay. I'm going to fix that up.Allx
S
11

If you are using MVar for holding settings or something similar, why don't you try reader monad?

foo :: ReaderT OptionRecord IO ()
foo = do
    options <- ask
    fireMissiles

main = runReaderT foo (OptionRecord "foo")

(And regular Reader if you don't require IO :P)

Scherzo answered 20/5, 2011 at 18:37 Comment(12)
You miss the point. A pragmatic programmer wants to avoid putting an extra layer on each and every function.Allx
@FUZxxl: Only functions that actually need the options would have an extra layer, and hiding the fact that some functions depend on outside data is not "pragmatic", it's sloppy. This is no different than tossing global variables around in an impure language.Roselynroseman
@camccann: Let's consider I have a language setting for translatable error messages as state. I need them almost everywhere. I don't want to add an extra 10 kB of boilerplate, just to take this into account.Allx
@FUZxxl: In order to do anything with the error messages, you'll have to be in IO at some point. So instead of using strings for errors, use type TranslatableError = OptionRecord -> IO String, then apply that to the options before displaying the messages. Just a few extra lines, not piles of boilerplate.Roselynroseman
@camccann: It is the same reason why one uses the function error: Because one don't wants to carry the environment needed to throw an error.Allx
@FUZxxl: Using error for anything other than "this can never happen" failures is also a terrible idea and indicates sloppy code. And I just explained how to produce errors without carrying the environment around.Roselynroseman
So... you mean by throwing an exception?Allx
@FUZxxl: Not to mention that if a function can plausibly encounter errors that force it to give up and let a caller's error handler deal with it, you should be in some sort of error monad anyway, in which case adding on a ReaderT is no big deal; this is why many applications define their own monad transformer stack. If you're using actual exceptions and catching them in IO then, again, there's no reason you can't get the options there instead of using unsafePerformIO. The problems you are trying to solve here don't actually exist.Roselynroseman
The idea that there is information like this which must be accessible to every point in the program in fallacious. We have many tools available to factor out this dependency -- code which does not essentially depend on this data can be given a type which does not (to accurately reflect that), and code which does can be minimized in scope. Try to think of your program not as a great beast of tens of thousands of lines of specification, but instead definitions of vocabulary and no more than a few hundred lines of specification.Lienlienhard
I had this nagging feeling that MVars are a bad idea unless for concurrency, but I'm getting from your comments that even Reader is a bad idea, even though I've seen lots of examples where it's been used, or recommended to be used exactly for these kind of read only variables. Was I mistaken?Scherzo
@Masse: I don't think anyone's saying not to use a reader monad for these kinds of variables, because that's exactly what it's for. If anything, I believe @Lienlienhard is suggesting that if large sections of a program need access to a bunch of external context, the program as a whole is probably badly structured. Using a reader monad would be merely treating the symptom, then.Roselynroseman
I've seen them all: global variables, singletons, dynamic parameters, thread locals... Whenever they're used to hide the actual data flow [the programmer being lazy], those idioms have disastrous consequences on the code. Yes, it can be a burden to add a parameter to many intermediary functions because you need it in a low-level function. However, it has the merit to clearly expose the data dependency (i.e. what input is actually required to perform the function calculation) while remaining pure and non-monadic. So, ReaderT is just another mechanism to hide the data (w/ potential abuse).Bifurcate
V
21

Those who would trade essential referential transparency for a little temporary convenience deserve neither purity nor convenience.

This is a bad idea. The code that you're finding this in is bad code.*

There's no way to fully wrap this pattern up safely, because it is not a safe pattern. Do not do this in your code. Do not look for a safe way to do this. There is not a safe way to do this. Put the unsafePerformIO down on the floor, slowly, and back away from the console...

*There are legitimate reasons that people do use top level MVars, but those reasons have to do with bindings to foreign code for the most part, or a few other things where the alternative is very messy. In those instances, as far as I know, however, the top level MVars are not accessed from behind unsafePerformIO.

Vaduz answered 20/5, 2011 at 18:38 Comment(4)
So JHC has bad code. GHC had a lot of that a long time ago, and they have slowly been converting to the pure way. It's the only sensible way.Diarmit
Can you give concrete examples of the unsafety?Courage
@Ganesh: You can't force that the initializer is run before the ref is accessed. If you thread through an record on the other hand (via implicit params or directly) then that can serve as a "proof obligation" of sorts. On top of that, there's the other usual problems with top level IORefs.Vaduz
@Vaduz that's not true: the argument of unsafePerformIO is run the first time the result is evaluated, even if it's only evaluated to WHNF using seqJarred
S
11

If you are using MVar for holding settings or something similar, why don't you try reader monad?

foo :: ReaderT OptionRecord IO ()
foo = do
    options <- ask
    fireMissiles

main = runReaderT foo (OptionRecord "foo")

(And regular Reader if you don't require IO :P)

Scherzo answered 20/5, 2011 at 18:37 Comment(12)
You miss the point. A pragmatic programmer wants to avoid putting an extra layer on each and every function.Allx
@FUZxxl: Only functions that actually need the options would have an extra layer, and hiding the fact that some functions depend on outside data is not "pragmatic", it's sloppy. This is no different than tossing global variables around in an impure language.Roselynroseman
@camccann: Let's consider I have a language setting for translatable error messages as state. I need them almost everywhere. I don't want to add an extra 10 kB of boilerplate, just to take this into account.Allx
@FUZxxl: In order to do anything with the error messages, you'll have to be in IO at some point. So instead of using strings for errors, use type TranslatableError = OptionRecord -> IO String, then apply that to the options before displaying the messages. Just a few extra lines, not piles of boilerplate.Roselynroseman
@camccann: It is the same reason why one uses the function error: Because one don't wants to carry the environment needed to throw an error.Allx
@FUZxxl: Using error for anything other than "this can never happen" failures is also a terrible idea and indicates sloppy code. And I just explained how to produce errors without carrying the environment around.Roselynroseman
So... you mean by throwing an exception?Allx
@FUZxxl: Not to mention that if a function can plausibly encounter errors that force it to give up and let a caller's error handler deal with it, you should be in some sort of error monad anyway, in which case adding on a ReaderT is no big deal; this is why many applications define their own monad transformer stack. If you're using actual exceptions and catching them in IO then, again, there's no reason you can't get the options there instead of using unsafePerformIO. The problems you are trying to solve here don't actually exist.Roselynroseman
The idea that there is information like this which must be accessible to every point in the program in fallacious. We have many tools available to factor out this dependency -- code which does not essentially depend on this data can be given a type which does not (to accurately reflect that), and code which does can be minimized in scope. Try to think of your program not as a great beast of tens of thousands of lines of specification, but instead definitions of vocabulary and no more than a few hundred lines of specification.Lienlienhard
I had this nagging feeling that MVars are a bad idea unless for concurrency, but I'm getting from your comments that even Reader is a bad idea, even though I've seen lots of examples where it's been used, or recommended to be used exactly for these kind of read only variables. Was I mistaken?Scherzo
@Masse: I don't think anyone's saying not to use a reader monad for these kinds of variables, because that's exactly what it's for. If anything, I believe @Lienlienhard is suggesting that if large sections of a program need access to a bunch of external context, the program as a whole is probably badly structured. Using a reader monad would be merely treating the symptom, then.Roselynroseman
I've seen them all: global variables, singletons, dynamic parameters, thread locals... Whenever they're used to hide the actual data flow [the programmer being lazy], those idioms have disastrous consequences on the code. Yes, it can be a burden to add a parameter to many intermediary functions because you need it in a low-level function. However, it has the merit to clearly expose the data dependency (i.e. what input is actually required to perform the function calculation) while remaining pure and non-monadic. So, ReaderT is just another mechanism to hide the data (w/ potential abuse).Bifurcate
H
5

Use implicit parameters. They're slightly less heavyweight than making every function have Reader or ReaderT in its type. You do have to change the type signatures of your functions, but I think such a change can be scripted. (Would make a nice feature for a Haskell IDE.)

Humidity answered 20/5, 2011 at 19:43 Comment(3)
The idea of implicit parameters is quite nice. They solve the problem exactly: One can set parameters, but don't has to care, when they are initialized.Allx
Can you include a link to an article on implicit parameters? That would allow people scanning these answers to quickly look at this method and see if it is correct for them.Revere
@Bryan: The GHC User's Guide covers it, well enough to get the idea at least.Roselynroseman
D
3

There is an important reason for not using this pattern. As far as I know, in

options :: MVar OptionRecord
options = unsafePerformIO $ newEmptyMVar

Haskell gives no guarantees that options will be evaluated only once. Since the result of option is a pure value, it can be memoized and reused, but it can also be recomputed for every call (i.e. inlined) and the meaning of the program must not change (contrary to your case).

If you still decide to use this pattern, be sure to add {-# NOINLINE options #-}, otherwise it might get inlined and your program will fail! (And by this we're getting out of the guarantees given by the language and the type system and relying solely on the implementation of a particular compiler.)

This topic has been widely discussed and possible solutions are nicely summarized on Haskell Wiki in Top level mutable state. Currently it's not possible to safely abstract this pattern without some additional compiler support.

Desdamona answered 24/5, 2014 at 6:49 Comment(0)
S
2

I often find this pattern in Haskell code:

Read different code.

As the programmer is lazy, he doesn't want to carry the options record all over the program. He defines an MVar to keep it - defined by an ugly use of unsafePerformIO. The programmer ensures, that the state is set only once and before any operation has taken place. Now each part of the program has to use unsafePerformIO again, just to extract the options.

Sounds like literally exactly what the reader monad accomplishes, except that the reader monad does it in a safe way. Instead of accommodating your own laziness, just write actual good code.

Stagemanage answered 13/4, 2018 at 23:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.