I wrote this fragment a few years ago and have talked about it to folks individually, but it came up again in discussion on the #haskell
channel, so I figured it was worth posting about in a central location.
Evaluating to Normal Form
The deepseq
package's Control.DeepSeq
provides the incredibly useful NFData
class.
class NFData a where
rnf :: a -> ()
rnf
evaluates its argument fully to normal form.
This class is abused by efficiency afficionados everywhere to ensure that no undue laziness leaks into their data structures.
However, it is easy to abuse, and often winds up having to do a lot of unnecessary work forcing things we already know to be forced!
Once and For All
We can prevent that by making a rather tricky little instance of NFData
:
import Control.DeepSeq
import Control.Lens
import Data.Copointed
import Data.Foldable
-- show
data Once a = Once () a
runOnce :: Once a -> a
runOnce (Once _ a) = a
once :: NFData a => a -> Once a
once a = Once (rnf a) a
instance NFData (Once a) where
rnf (Once () _) = ()
-- /show
instance Foldable Once where
foldMap f (Once _ a) = f a
instance Copointed Once where
copoint (Once _ a) = a
_Once :: NFData a => Iso' (Once a) a
_Once = iso runOnce once
main = putStrLn "It compiled."
Now, anywhere you expect to spam rnf
in your code, just wrap the value in Once
, and any attempts will only force the fragment underneath once.
This can make a massive difference in the performance of code that happens to abuse rnf
, enabling it to avoid doing useless work and avoid thrashing caches walking over irrelevant data.
You don't have to use Once
to make this trick work. You can of course put extra ()
arguments as needed recursively within your data structure and replicate the NFData
trick above yourself.
With A Little Bit of Lens
With lens
you can collapse the entire API for working with Once
to a single isomorphism:
_Once :: NFData a => Iso' (Once a) a
_Once = iso runOnce once
September 3, 2013