Haskell's missing mutable reference type

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 question then is how to implement foo in terms of these three functions. This implies

foo :: IOScopedRef a -> IO ()
foo ref = do
  a <- readIOScopedRef ref
  print a

main =
  withScopedIORef 5 $ \ref ->
    modifyIOScopedRef (+2) ref $
      foo ref

which can be reduced to

foo :: Show a => a -> IO ()
foo = print

main = do
  let ref = 5
  foo $ (+2) ref

which is thread-safe and exception-tolerant in exactly the way you expect.

1 Like

I agree with you, but what are you using that argument to conclude?

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.

1 Like

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?

It’s not a “you can” question, the person writing the library either provides you with the functions to do a given thing or they don’t.

In current Haskell you need to augment the Logger and pass the modified value to all relevant functions.

With references the library would declare an implicit configuration reference and you’d augment that for all functions invoked within a context.

1 Like

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?

I don’t know what “with references” means.

Yes, though I would object to this feature being implemented like that.

As in with either contextual values I outlined above or IOScopedRef.

1 Like

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.

1 Like

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.

1 Like

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.

1 Like

I would also like something like this! I wonder if GHCi could support a new top-level form with which I used like

ghci> :with f v

where f :: (a -> IO r) -> IO r for some a and r. Then you’d get a “nested” GHCi block with v :: a in scope, which you could return from with :endwith:

ghci> :with (bracket acquire release) conn
ghci with1> conn
Connection { hostname = "...", ... }
ghci with1> let results = runQuery conn
ghci with1> results
[ Result 1, Result 2 ]
ghci with1> :endwith (head results)
Result 1
ghci>

IOScopedRef could be useful here, but I don’t think it’s a key element.

1 Like

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?

GHCi is par of GHC so: https://gitlab.haskell.org/ghc/ghc/-/issues

Yes, that is what I was asking for thanks!

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)

> :withBracket acquire release conn
> conn
Connection { hostname = "...", ... }
...
:endwith (head results)

but that’s not really all that different from calling acquire and release yourself.

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.