I have a long-running process which is forkIO
'd, which produces pixel color values:
takesAgesToRun :: [[Color]]
myForkedProcess :: IORef [[Color]] -> IO ()
myForkedProcess ref = do let colors = takesAgesToRun
writeIORef ref colors
(where Color
just holds three Double
values).
As expected, when read on the "other side" of the IORef
, the value that was stored is just a thunk, and hence blocks the main process.
I know I need to fully evaluate the [[Color]]
value to head normal form, but there seem to be two ways of achieving that, and further, I'm not sure how to incorporate either into my code.
How would I go about this? Do I use rnf
, deepSeq
, or some other threading strategy? Is one of these the preferred one, and the others deprecated? And how does it fit into my code?
(PS please ignore the fact that storing the image as a list of a list of colors is stupid - this is just a simplified version of the code).
Use deepSeq
. It's used just like seq
. You would incorporate it like this:
myForkedProcess :: IORef [[Color]] -> IO ()
myForkedProcess ref = do let colors = takesAgesToRun
deepSeq colors $ writeIORef ref colors
This will force "colors" to be fully evaluated before the "writeIORef" call.
In order for this to work, you will need an NFData
instance for Color
. Exactly how to write this depends upon the definition of Color, but here are two examples:
-- just for reference
data Color = Color Double Double Double
instance NFData Color where
rnf (Color r g b) = r `seq` g `seq` b `seq` ()
-- closer to the likely actual implementation for Color
data Color2 = Color2 !Double !Double !Double
instance NFData Color2 where
-- the default implementation is fine
For the Color
instance, you need to ensure that all components of the color are fully evaluated[1] whenever Color is. That's what the seq
s do. We can use seq
instead of deepSeq
here because we know that each component is a Double
, therefore is fully evaluated by seq. If a component were a more complex data type, then we would need to use deepSeq
when writing the NFData instance.
In Color2
it's a bit simpler. Because of the bang patterns, we know that the components are fully evaluated when Color2
is. This means we can use the default implementation, which evaluates Color2
to weak head normal form, which due to the bang patterns is fully evaluated.
rnf
is mainly useful when used in combination with Control.Parallel.Strategies. Here's the current definition of deepSeq
deepseq :: NFData a => a -> b -> b
deepseq a b = rnf a `seq` b
All deepseq does is call rnf
and guarantee that its output () is evaluated. This is really the only way to use rnf
directly.
[1] Haskell provides only two general ways to evaluate stuff: pattern matching and seq
. Everything else is built upon one or both of these. For the NFData Color instance, Color
is first evaluated to WHNF by pattern matching with the Color
constructor, then the components are evaluated via seq.
Of course there is also a third, highly specialized, way to evaluate stuff: i.e. a function main :: IO ()
will be executed in order to evaluate the ()
.