You could still do that! It’s only that when using libFun, you would locally instantiate that m with ReaderT Logger IO, which (modulo newtypes) would give you
IIUC, wouldn’t that force you to make libFun polymorphic over the monad?
When I build the graph of components for my application during initialization, I’m ok with passing the ref to any component that uses it directly, like the Repository in my example. What I don’t want is having to pass it to the components that depend on Repository. Like this component which uses the repository but doesn’t need to know about the connection or how it’s obtained.
If I want to wire everything at initialization, I have the problem that connections aren’t really obtained at that moment, but are allocated (or taken from a pool) for each request, once the app is running. The dynamic binding trick lets me feign that the connection already exists at the beginning, so to speak.
Of course ReaderT or effect systems would be other ways of handling it, but they are ruled out by my other preferences (work in plain IO if possible, don’t force components to be polymorphic over the effect monad for the sake of other components, wire everything at the beginning, try to avoid transformers or effect systems).
The way I’m doing it resembles more how request-scoped database connections are injected in Java frameworks like Spring.
Yes, that’s what Fork-fragile reader-like operations in Haskell catalogues. You’ll see a couple of them are even in base. Some of them work directly on IO, some on MonadIO m or MonadUnliftIO m, but …
(MonadIO will not be enough in the general case because it doesn’t allow lifting things like catch, bracket, forkIO, etc.. So, in the general case, we may need as much as MonadUnliftIO.)
Is MonadUnliftIO good enough? Functionally yes, practically no. I address this question in A reference implementation of IOScopedRef. Writing monadic code with an unknown bind is bad for performance, as explained by Alexis King in Effects for Less.
Ah yes, that’s unfortunate. Maybe we could have a Bracket type that abstracts acquire and release. Alternatively, speculatively, a linearly typed continuation?
There are many examples in the ecosystem of global, top-level IORefs created with unsafePerformIO. I don’t like that style, but if others do they could do the same with IOScopedRef. (I didn’t describe a newIOScopedRef primitive in the article, but there’s no reason one couldn’t exist. Anyway, it seems from his comment below, @danidiaz doesn’t even want to do that.)
Yes, with the inevitable performance consequences.
Why does the “app is running” distinction here matter? If an endpoint handler works over a single connection, then that’s the argument it consumes; how the connection is acquired shouldn’t be relevant.
I’m in the “pass everything explicitly as arguments” camp, so my question is pretty much “how is doing things the stupid way not enough here”.
Maybe it’s time to have the API design discussion then (and if mods think it’s too off topic for this thread then they can split it off).
Suppose I am thinking of writing a function like writeUserData which has the following type (isomorphic to the one in the linked section), which uses a type Severity which I also define:
data Severity
writeUserData ::
(Severity -> String -> IO ()) ->
(forall a. (Severity -> Severity) -> IO a -> IO a) ->
IO ()
where the first argument is a callback to log messages at a severity and the second argument (call it modifySeverity) locally modifies the severity. What is your alternative to this API?
Important condition: as the author of writeUserDataI do not want to fix the interpretation of modifySeverity.
For example, when the Severity reaches a certain elevated level the caller may want modifySeverity to obtain a connection to a different logging channel (maybe it starts writing to a file rather than stdout) for the duration of elevated execution (but no longer than that). I want to support that use case.
How are you imagining implementing writeUserData in a thread-safe way where the severity modification is open to interpretation by the caller, without IOScopedRef?
This assumes that the logger is its own separate actor and that’s way too complex for my tastes. I’d go with a static approach where some flows are known upfront to log to file when certain conditions are met. All you’d then need is to open a file and pass a different logger down.
Right, but my writeUserData also supports your tastes. I’m asking if I want to write writeUserData to support a wider variety of tastes then how else should I do? Or do you consider the usecase of varying logging behaviour based locally on Severity to be somehow a taste that one shouldnot have? If so, why?
modifications to an IOScopedRef in one thread would not be observed by another thread
Do you mean any other thread or would child threads see it? I’m pretty sure that’s what you would want, otherwise your logging example gets very annoying as soon as you do any concurrency inside a modification block.
In fact, it seems to me that this is the crux of it rather than a thread-local variable, its a thread-and-children-local-variable. The “scoped” part of your proposal can be achieved by just forking a child thread before doing the modification!
In fact, it seems to me that this is the crux of it: rather than a thread-local variable, it’s a thread-and-children-local-variable. The “scoped” part of your proposal can be achieved by just forking a child thread before doing the modification!
This was wrong - you probably don’t want modifications by child threads to be seen in parent threads! So perhaps what you need is:
A thread-local mutable variable;
Child threads have their value initialized to the parent’s value when they are forked
I’m skeptical. Can you share an example?
It’s not quite your example, but here’s a common thing you do with a logging library.
loggerExampleConcurrently :: IO ()
loggerExampleConcurrently = withStdoutLogger 0 $ \logger -> do
logMsg logger 1 "Getting user"
user <- getUser
(d, ()) <-
Control.Concurrent.Async.concurrently
( setDomain logger "datafetch $ do
logMsg logger 0 "Getting data"
d <- getData user
Control.Concurrent.Async.forConcurrently d.transactions \t -> do
-- what's the domain here?
logMsg logger 0 "Getting data"
getTransaction t
)
( -- Do some unimportant background processing
do
Control.Concurrent.threadDelay 1000
logMsg logger 0 "Background work done"
)
writeTransactions d
logMsg logger 0 "Done"
We would want the log domain to be “datafetch” inside the nested concurrent map, but not in the adjacent concurrent processing. See e.g. the various local functions on MonadLog: Log.Class
As you point out in your other article, the example of existing functionality that works this way (thread masking state) also has the property of being inherited by child threads.
I also think it’s just quite intuitive: new threads are “lexically” inside the body of the modification, isn’t it natural that they see it?
Sounds great. Not very clear from the article that that’s the intended semantics though!
So I guess my API question is whether this is best thought of as a new kind of thread-local variable. Is there anything that you can do with your IOScopedRef API that you couldn’t do with a InheritedThreadLocalIORef? (I imagine the API would be pretty much identical to that for IORef - just the behaviour would be different)
Thus far, we haven’t had any support for thread-local variables in GHC, as people have managed workarounds with Map ThreadId IORef or whatever. But I don’t think we have the tools to do that for a version where the value is inherited by child threads - we’d need a hook to run on thread creation or something. So it does seem like something that might need support in GHC.
How would the behaviour differ? Do you mean that InheritedThreadLocalIORef could be modified within a thread like an IORef? If so then it would need a function of type
InheritedThreadLocalIORef a -> a -> IO ()
which isn’t in the IOScopedRef API. But IOScopedRef could be implemented in terms of InheritedThreadLocalIORef. I just don’t like the semantics of the latter. It would allow you to observe whether you’re running in a forked thread. See “the interaction between the semantics of thread creation and of IOScopedRef are good” in the Concurrency section.
What’s the use of having thread local state that allows non-scoped modifications?
In that example you do the modification inside the fork, so it wasn’t obvious to me whether the modification would be visible if you did it the other way around.
How would the behaviour differ? Do you mean that InheritedThreadLocalIORef could be modified within a thread like an IORef?
Yes. You modify it just like an IORef, it’s just that such a modification is visible to a) your current thread, and b) child threads (if it happens before you fork the thread). That is, exactly how masking state works today.
I just don’t like the semantics of the latter. It would allow you to observe whether you’re running in a forked thread. See “the interaction between the semantics of thread creation and of IOScopedRef are good” in the Concurrency section.
I don’t understand how it would do that, can you give an example?
What’s the use of having thread local state that allows non-scoped modifications?
What’s a “non-scoped modification”? Do you mean writeIORef versus withModifiedIORef :: IORef a -> (a -> a) -> IO a -> IO a? People seem to be perfectly happy with the non-scoped modification functions on IORef, and you can implement the scoped ones with it if you want?
The point of having thread-local state is: modifications in child threads don’t affect the parent or sibling threads.
The point of having inherited thread-local state is: forked code “continues” from the parent context in a natural way.
The problems you observe in fork-fragile-reader-like-operations seem to me to be related to these two things. I’m arguing that the “scoped” part of it is perhaps not the crux!
and the result depends on whether foo ran its body in a new thread or not. Now that’s not terrible. There are all sorts of ways we could get similar behaviour, such as myThreadId, but it does suggest to me it’s not a particularly principled primitive, whereas it seems to me that IOScopedRefis principled (although I don’t know in what terms to formulate that claim).
Right, whereas I think the “scoped” part probably is the crux. That’s the part where I see an opportunity for a new primitive with principled behaviour. I understand the general form of your argument that the “scoped” part isn’t the crux: in principle there’s a use for a souped-up version of the thing I propose, which allows for more behaviours. But In practice is there a use for it? Where are people using that?
I see! That’s interesting. Yes, there is something ugly there. I’m not sure how to state the property that you’re aiming for here, but it seems probably desirable. It’s interesting that we’d need to strip away a bunch of the normal IORef interface to ensure it.
And indeed, we don’t have non-scoped modification functions for masking state, which is perhaps telling.
I think a non-scoped InheritedThreadLocalIORef makes sense from a machine perspective but that doesn’t mean it has good semantics! I will have more of a think about this.