The solution to this problem is in changing the pure helper functions. We don't really want them to be pure, we want to leak out a single side-effect - whether or not they read specific pieces of data.
Let's say we have a pure function that uses only clothing and coins:
moreVanityThanWealth :: IntMap Clothing -> IntMap Coins -> Bool
moreVanityThanWealth clothing coins = ...
It's usually nice to know that a function only cares about e.g. clothing and coins, but in your case this knowledge is irrelevant and is just creating headaches. We are going to deliberately forget this detail. If we followed mb14's suggestion, we would pass an entire pure MudData'
like the following to the helper functions.
data MudData' = MudData' { _armorTbl :: IntMap Armor
, _clothingTbl :: IntMap Clothing
, _coinsTbl :: IntMap Coins
moreVanityThanWealth :: MudData' -> Bool
moreVanityThanWealth md =
let clothing = _clothingTbl md
coins = _coinsTbl md
in ...
MudData
and MudData'
are almost identical to each other. One of them wraps its fields in TVar
s and the other one doesn't. We can modify MudData
so that it takes an extra type parameter (of kind * -> *
) for what to wrap the fields in. MudData
will have the slightly unusual kind (* -> *) -> *
, which is closely related to lenses but doesn't have much library support. I call this pattern a Model.
data MudData f = MudData { _armorTbl :: f (IntMap Armor)
, _clothingTbl :: f (IntMap Clothing)
, _coinsTbl :: f (IntMap Coins)
We can recover the original MudData
with MudData TVar
. We can recreate the pure version by wrapping the fields in Identity
, newtype Identity a = Identity {runIdentity :: a}
. In terms of MudData Identity
, our function would be written as
moreVanityThanWealth :: MudData Identity -> Bool
moreVanityThanWealth md =
let clothing = runIdentity . _clothingTbl $ md
coins = runIdentity . _coinsTbl $ md
in ...
We've successfully forgotten which parts of the MudData
we've used, but now we don't have the lock granularity we want. We need to recover, as a side effect, exactly what we just forgot. If we wrote the STM
version of the helper it would look like
moreVanityThanWealth :: MudData TVar -> STM Bool
moreVanityThanWealth md =
do
clothing <- readTVar . _clothingTbl $ md
coins <- readTVar . _coinsTbl $ md
return ...
This STM
version for MudData TVar
is almost exactly the same as the pure version we just wrote for MudData Identity
. They only differ by the type of the reference (TVar
vs. Identity
), what function we use to get the values out of the references (readTVar
vs runIdentity
), and how the result is returned (in STM
or as a plain value). It would be nice if the same function could be used to provide both. We are going to extract what is common between the two functions. To do so, we'll introduce a type class MonadReadRef r m
for the Monad
s we can read some type of reference from. r
is the type of the reference, readRef
is the function to get the values out of the references, and m
is how the result is returned. The following MonadReadRef
is closely related to the MonadRef
class from ref-fd.
{-# LANGUAGE FunctionalDependencies #-}
class Monad m => MonadReadRef r m | m -> r where
readRef :: r a -> m a
As long as code is parameterized over all MonadReadRef r m
s, it is pure. We can see this by running it with the following instance of MonadReadRef
for ordinary values held in an Identity
. The id
in readRef = id
is the same as return . runIdentity
.
instance MonadReadRef Identity Identity where
readRef = id
We'll rewrite moreVanityThanWealth
in terms of MonadReadRef
.
moreVanityThanWealth :: MonadReadRef r m => MudData r -> m Bool
moreVanityThanWealth md =
do
clothing <- readRef . _clothingTbl $ md
coins <- readRef . _coinsTbl $ md
return ...
When we add a MonadReadRef
instance for TVar
s in STM
, we can use these "pure" computations in STM
but leak the side-effect of which TVar
s were read.
instance MonadReadRef TVar STM where
readRef = readTVar
IntMap
s instead of aMudData
? Would the latter be an option? Hard to tell without seeing some code -- can you share a small snippet showing a case where the helper needs so many arguments? – BleakMudData
is the helper functions are pure and theMudData
has lots ofTVar
s for granularity. The pure helper functions may only be used on subsets of theMudData
, so adding another data structure (or a type parameter toMudData
) won't help - reading all theTVar
s to create that one single structure would eliminate the granularity. – AnnabelleannabergiteTVar
s ensure deadlock freedom, since they can only be accessed in STM transactions. – BleakTVar IntMap
s (orMudData
carrying all theTVar
s) and rewrite the helpers inside theSTM
monad so that they can actually read the data. Maybe with lenses and some applicative syntax one can reduce thereadTVar
boilerplate. (Here I am doing wild guesses, I admit.) – BleakMudData
withoutTVar
s like @Deceitful suggested and be passed the wholeMudData
,TVar
s and all, like you suggest. – Annabelleannabergite