You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I noticed that Async has a Functor instance, which changes the result of an Async. But it (necessarily) does so entirely lazily; there's no way to ensure that computation is completed by the time wait returns. I suggest adding the following (by whatever name you like):
mapAsync:: (a->Solob) ->Asynca->Asyncb
mapAsync f (Async a w) =Async a $ w >>=\caseLeft e ->pure (Left e)
Right r |MkSolo r' <- f r ->pure (Right r')
there's no way to ensure that computation is completed by the time wait returns.
This is true, but your function is also misleading, as it will compute the b in the thread which calls the wait, not in the (originally spawned) asynchronous thread.
there's no way to ensure that computation is completed by the time wait returns.
This is true, but your function is also misleading, as it will compute the b in the thread which calls the wait, not in the (originally spawned) asynchronous thread.
They're equally misleading in that way. The documentation for both should reflect that.
Moreover, in both cases, the calculation is performed once per invocation of wait, even within the same thread. If you're mapping over an Async, you probably don't want to do anything too expensive there.
I'm not sure I'm convinced (a) there's a problem that needs fixing and (b) if there was, that this is the right solution.
On (a) do you have something you really wanted to do that you couldn't?
I was looking at this module for an unrelated (and uninteresting) reason, and happened to notice the Functor instance. What is it for? As we've both mentioned, the computation it introduces is repeated for each wait call, so it's probably best used as a lightweight shim when passing an Async to something that needs an Async of a slightly different type—say, if you have an Async (a, b) and need an Async a. Typically, the result of wait will be used immediately, yes, but occasionally it might be stored in a data structure, in which case it would be best if it weren't a silly thunk.
On (b)
* This doesn't prevent the computation being repeated by each call to `wait`, which is something else that one might expect to be able to do?
It does not, and neither does fmap. The current Async design doesn't give us any way to avoid that, and I can't think of any similar design that would. We can't create a new thread to perform the computation, because we're in an entirely pure context.
* The use of `Solo` seems a bit weird - you can still write something that defers the work.
I believe the use of Solo offers as much flexibility to the caller as possible without breaking the abstraction barrier. It allows the caller to decide exactly how much of the computation should be performed before returning.
-- Discard the second component to avoid leaking it, but defer an expensive function-- call.
(`mapAsync` m) $\(a, _) ->MkSolo (f a)
-- Force the full calculation
(`mapAsync` m) $\a ->MkSolo$! g a
In principle, the most powerful such function would be
bazooka::Asynca-> (a->STMb) ->Asyncb
bazooka (Async tid m) f =Async tid (m >>= f)
But (as I read it) the use of STM is an implementation detail, and bazooka doesn't feel like something anyone would likely want.
Instead I think I would wrap wait in something that caches its result, there are various ways to do that.
I would imagine that in most cases, fmap for Async isn't very useful if you can tweak the wait call that receives it. After all, it's a shim. I am curious how you would wrap wait to avoid this sort of recalculation, though.
My high-level view on this is that the need to control evaluation order in this way is relatively rare, so there needs to be a strong motivation to add something to an API for the purposes of evaluation-order control only. I'm not seeing a strong motivation here, and there are easy workarounds.
The Function instance is occasionally useful, and (at least in my experience) the fact that it doesn't get forced by wait has never been a problem. Often you're immediately consuming the result, which forces it anyway, and it's rare to wait more than once.
The use of STM isn't an implementation detail - there are operations like waitSTM for example. Your bazooka is potentially useful I'd say (but needs a better name!)
The use of STM isn't an implementation detail - there are operations like waitSTM for example. Your bazooka is potentially useful I'd say (but needs a better name!)
Couldn't waitSTM hypothetically be implemented like this?
waitSTM = unsafeIOtoSTM . wait
If you're happy to have bazooka, that's good enough to implement my mapAsync, so that's fine by me. Perhaps call it afterWaitSTM, to indicate that it adds an action to perform after each wait?
The use of STM isn't an implementation detail - there are operations like waitSTM for example. Your bazooka is potentially useful I'd say (but needs a better name!)
Couldn't waitSTM hypothetically be implemented like this?
waitSTM = unsafeIOtoSTM . wait
Goodness me no 😆
unsafeIOToSTM is really unsafe, there's no guarantee this would respect the STM semantics, even if wait is just a call to atomically as it is now.
Well, it is indeed debatable whether unsafeIOToSTM has any truly safe uses. Obviously you can't do any side effects in there because the effects can't be rolled back if the transaction aborts. Typically it's used for debugging where the unsafety doesn't matter. Is wait a side effect? Well, the context of this is you were asking whether we can consider STM an implementation detail, so we don't know the implementation of wait - it might mutate some internal data structures, and therefore we can't assume it's safe to use with unsafeIOToSTM.
But suppose we just want to know whether it would be "safe" in some sense to define waitSTM = unsafeIOToSTM . wait with the current definition of wait - frankly I don't know. Maybe. It relies on an assumption that the STM computation inside the Async doesn't write to anything, which is probably true right now but we have no way to enforce that, and if you want to add afterWaitSTM then the user could violate it.
Activity
phadej commentedon Jan 6, 2026
This is true, but your function is also misleading, as it will compute the
bin the thread which calls thewait, not in the (originally spawned) asynchronous thread.treeowl commentedon Jan 6, 2026
They're equally misleading in that way. The documentation for both should reflect that.
treeowl commentedon Jan 6, 2026
Moreover, in both cases, the calculation is performed once per invocation of
wait, even within the same thread. If you're mapping over anAsync, you probably don't want to do anything too expensive there.simonmar commentedon Jan 7, 2026
I'm not sure I'm convinced (a) there's a problem that needs fixing and (b) if there was, that this is the right solution.
On (a) do you have something you really wanted to do that you couldn't?
On (b)
wait, which is something else that one might expect to be able to do?Soloseems a bit weird - you can still write something that defers the work.Instead I think I would wrap
waitin something that caches its result, there are various ways to do that.treeowl commentedon Jan 16, 2026
I was looking at this module for an unrelated (and uninteresting) reason, and happened to notice the
Functorinstance. What is it for? As we've both mentioned, the computation it introduces is repeated for eachwaitcall, so it's probably best used as a lightweight shim when passing anAsyncto something that needs anAsyncof a slightly different type—say, if you have anAsync (a, b)and need anAsync a. Typically, the result ofwaitwill be used immediately, yes, but occasionally it might be stored in a data structure, in which case it would be best if it weren't a silly thunk.It does not, and neither does
fmap. The currentAsyncdesign doesn't give us any way to avoid that, and I can't think of any similar design that would. We can't create a new thread to perform the computation, because we're in an entirely pure context.I believe the use of
Solooffers as much flexibility to the caller as possible without breaking the abstraction barrier. It allows the caller to decide exactly how much of the computation should be performed before returning.In principle, the most powerful such function would be
But (as I read it) the use of
STMis an implementation detail, andbazookadoesn't feel like something anyone would likely want.I would imagine that in most cases,
fmapforAsyncisn't very useful if you can tweak thewaitcall that receives it. After all, it's a shim. I am curious how you would wrapwaitto avoid this sort of recalculation, though.simonmar commentedon Jan 19, 2026
My high-level view on this is that the need to control evaluation order in this way is relatively rare, so there needs to be a strong motivation to add something to an API for the purposes of evaluation-order control only. I'm not seeing a strong motivation here, and there are easy workarounds.
The Function instance is occasionally useful, and (at least in my experience) the fact that it doesn't get forced by
waithas never been a problem. Often you're immediately consuming the result, which forces it anyway, and it's rare towaitmore than once.The use of STM isn't an implementation detail - there are operations like
waitSTMfor example. Yourbazookais potentially useful I'd say (but needs a better name!)To cache
waityou could use something like https://hackage-content.haskell.org/package/extra-1.8.1/docs/Control-Concurrent-Extra.html#v:onceI'll leave this PR open in case others have thoughts, but for now I'm not persuaded to do anything.
treeowl commentedon Jan 19, 2026
Couldn't
waitSTMhypothetically be implemented like this?If you're happy to have
bazooka, that's good enough to implement mymapAsync, so that's fine by me. Perhaps call itafterWaitSTM, to indicate that it adds an action to perform after eachwait?simonmar commentedon Jan 20, 2026
Goodness me no 😆
unsafeIOToSTMis really unsafe, there's no guarantee this would respect the STM semantics, even ifwaitis just a call toatomicallyas it is now.treeowl commentedon Jan 20, 2026
For my own curiosity, what makes that approach unsafe for this particular operation? If you can't even use it for this, does it have any use at all?
simonmar commentedon Jan 20, 2026
Well, it is indeed debatable whether
unsafeIOToSTMhas any truly safe uses. Obviously you can't do any side effects in there because the effects can't be rolled back if the transaction aborts. Typically it's used for debugging where the unsafety doesn't matter. Iswaita side effect? Well, the context of this is you were asking whether we can consider STM an implementation detail, so we don't know the implementation ofwait- it might mutate some internal data structures, and therefore we can't assume it's safe to use withunsafeIOToSTM.But suppose we just want to know whether it would be "safe" in some sense to define
waitSTM = unsafeIOToSTM . waitwith the current definition ofwait- frankly I don't know. Maybe. It relies on an assumption that the STM computation inside the Async doesn't write to anything, which is probably true right now but we have no way to enforce that, and if you want to addafterWaitSTMthen the user could violate it.