I still must be missing something, or not explaining myself well. foo is not expected to access x if x is not in scope in the body of foo. Passing a key is exactly what is expected. I don’t understand why that’s not an answer.
I don’t understand what that means.
I’m definitely explaining myself badly then. I am certainly suggesting nothing of the sort.
I believe you are proposing this interface to “contextual variables” which you believe is safe and implementable:
v :: Contextual t
contextual v = e :: t
getContextual :: Contextual t -> IO t
overriding :: Contextual t -> (t -> t) -> IO r -> IO r
If so, then I can implement what I want as follows
type IOScopedRef a = Vault.Key a
vault :: Contextual Vault.Vault
contextual vault = Vault.empty
withIOScopedRef :: a -> (IOScopedRef a -> IO r) -> IO r
withIOScopedRef a body = do
key <- Vault.newKey
overriding vault (Vault.insert key a) $ do
body key
readIOScopedRef :: IOScopedRef a -> IO a
readIOScopedRef key =
fmap (fromJust . Vault.lookup key) getContextual vault
modifyIOScopedRef ::
(a -> a) -> IOScopedRef a -> IO r -> IO r
modifyIOScopedRef f key body =
overriding vault (Vault.adjust f) key body
If your “contextual” API exists then my IOScopedRef can be layered on top. Seems straightforward. And it’s roughly what I explain at A reference implementation of IOScopedRef. What remains to be resolved?
The conclusion would be that dynamic allocation of these kinds of references is unnecessary, as the language being pure already solves for the wanted behavior.
If it’s unnecessary then what is the alternative way that I can instantiate the Logger argument to writeUserData to get the behaviour I want, in haskells-missing-mutable-ref?
OK, so are we agreed, then, that if IOScopedRef existed then I could instantiate the Logger argument to writeUserData to get the behaviour I desire? And that with only the primitives provided by Haskell as it currently stands, the Logger argument to writeUserData cannot be instantiated in a way that gives me the behaviour that I desire. Instead Logger or writeUserData or both would need to be changed?
Sorry, what feature? Do you mean I shouldn’t use IOScopedRef (if it existed) to get the behaviour I desire, or IOScopedRef shouldn’t be implemented? In either case, what do you object to?
With “references”, the libraries providing Logger and writeUserData need not know anything about the existence of “references”. Only the caller need use them.
Not exactly, my point is that tacking dynamic allocations onto this feature is at best unnecessary and at worst would pollute the language with a second way to do something it already excels at.
A static reference table is the “missing” thing in the language as I see it.
I don’t think this has anything to do with whether the references are static or dynamic, you can wrap the library either way.
I don’t understand. What is “this feature”? Your “contextual”?
OK, fine, and if that missing thing is added, I’ll add the API that I want on top. What’s wrong with that?
I agree. But you said “With references the library would declare an implicit configuration” (my emphasis). I’m pointing out it’s not the library that declares the implicit configuration, it’s the caller.
Yes, worker threads carrying tables of references with table size defined at compilation time.
Would this make for a good point of abstraction though? I’d expect a given library to need several internal values be adjustable, not some interface type like Logger.
Consider something simpler with no extra inputs whatsoever, like adjustable numeric precision or getArgs.
Ah, well now it’s a question of API design, an area where I suspect, based on this comment, we will disagree, but not really the point of the original article. Happy to continue in a fresh thread if you create one. Thanks for the discussion so far!
Could a scoped mutable ref be used to beef up the GHCi prompt with extended run-time environments? I’m thinking of an interactive prompt where I can temporarily open a database connection, not knowing all the statements a priori. The feature would have a type similar to bracket:
withResource :: IO res -- acquire resource
-> (res -> IO ()) -- release action
-> ReaderT res IO r -- type of GHCi statements inside bracket
-> IO r -- result after release
The usual database connection functions require the entire block of statements to be known at compile-time. Squinting at the type signature, one notices that withResource open close has a type isomorphic to ContT r IO res. So I guess my question is about whether continuations can be sneaked into the GHCi REPL.
Of course a custom REPL could provide that, but the wealth of functionality that GHCi provides is just too convenient.
Yes, the end-delimiter of the block must be a meta-command of GHCi, like the :endwith you suggested.
Where is the appropriate issue tracker of GHCi to discuss this further?
Okay, I don’t disagree that the IOScopedRef API allows APIs that aren’t possible without it, but I’m still not convinced that those APIs are useful.
Your libFun example is techinically an example where they are necessary, because it’s basically a function that takes something with your exact API as a parameter. But then the question is: are there real functions that look like libFun? I can’t think of any but maybe you know some?
In particular, the exact constraint libFun has that’s relevant here is that it takes a function from IO _ -> IO _, where this function has to act on exactly IO.
If you generalized it to work on any m that implements MonadIO (Or MonadUnliftIO), you wouldn’t need IOScopedRefs because you could instantiate m with ReaderT Logger IO
That won’t work, at least not if you want to see intermediate results.
The way you have it, that f function could do anything with its continuation, including discarding it or running it several times.
Like, what would you expect to happen if I did
> :with (\_ -> pure ()) ()
> print 5
???
or
> :with (\io -> io () >> io ())
> print 5
???
You could special case the case where you’re using it with bracket (which calls its continuation exactly once in a predictable way)
In my personal projects, I prefer to work with bare IO instead of usign an app-wide ReaderT, and I’m willing to pay the price of decreased type safety in order to achieve that. Granted, I wouldn’t like to use dynamic binding of this sort for all dependencies in my app, but for some of them (request-scoped resources like database connections taken from a connection pool) it’s convenient as an alternative to ReaderT.
Here’s a thread about this approach. Having native support for thread-bound storage would free me from having to implement it myself in a less efficient manner.
And here’s the same pattern in another project. I have a Repository datatype that takes an IO Connection as parameter. That IO Connection can be obtained in different ways, here it’s obtained by reading it from a thread-local variable set by the Servant handler. The Repository doesn’t have to live in some ReaderT monad, and neither any other component that depend on it.