Beautiful Mutable Values
Mutability can be awesome!
Take back the power of mutable objects with all the safety and explicit
state of Haskell. Associate and generate "piecewise-mutable" versions for your
composite data types in a composable and automatic way. Think of it like a
"generalized MVector
for all ADTs". It also leverages GHC Generics to make
working with piecewise mutability as simple as possible.
Making piecewise updates on your giant composite data types (like artificial neural networks or game states in your game loop) got you down because they require re-allocating the entire value? Tired of requiring a full deep copy every time you make a small change, and want to be able to build mutable versions of your types automatically in composable ways? This is the package for you.
data MyType = MT
{ mtInt :: Int
, mtDouble :: Double
, mtVec :: V.Vector Double
}
deriving (Show, Generic)
instance Mutable s MyType where
type Ref s MyType = GRef s MyType
The type Ref s MyType
is now a "mutable MyType
", just like how MVector s
a
is a "mutable Vector a
". You have:
thawRef :: MyType -> ST s (Ref s MyType)
freezeRef :: Ref s MyType -> ST s MyType
(These actions are in ST
, the mutable memory monad that comes with GHC. The
real types of these are more polymorphic and are generalized to work for all
mutable monads with PrimMonad
instance.)
You can use thawRef
to allocate a mutable MyType
that essentially consists
of a mutable Int
, a mutable Double
, and a mutable Vector
(an MVector
)
all tupled together. You can edit these pieces in isolation, and then
freezeRef
it all back together:
doStuff :: MyType -> MyType
doStuff x = runST $ do
r <- thawRef x
replicateM_ 1000 $ do
-- modify the Int in `mtInt`
modifyPart (fieldMut #mtInt) r (+ 1)
-- the `mtVec` field is now an MVector
withField #mtVec r $ \v ->
MV.modify v (+1) 0
freezeRef r
doStuff $ MT 0 19.3 (V.fromList [1..12]) =>
MT {mtInt = 1000, mtDouble = 19.3, mtVec = [1001.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0]}
If you were to do this normally with pure values, this would be extremely
expensive, especially if mtVec
is a huge vector --- it would require copying
every item in the entire vector every step, being O(n * l) , with n number
of repetitions and l length of vector and number of fields. With mutable
vectors and mutable cells, this now becomes O(n + l).
The main motivation for this library is to implement automatically derivable piecewise-mutable references for the purposes of mutation-heavy algorithms, like artificial neural networks. In the end, you're able to have an Artificial Neural Network (which can have huuuuge vectors) and being able to do piecewise updates on them (automatically) without having to copy over the entire network every training step.
There is also support for mutable sum types, as well. Here is the automatic definition of a mutable linked list:
data List a = Nil | Cons a (List a)
deriving (Show, Generic)
infixr 5 `Cons`
instance Mutable s a => Mutable s (List a) where
type Ref s (List a) = GRef s (List a)
We can write a function to "pop" out the top value and shift the rest of the list up:
popStack
:: Mutable s a
=> Ref s (List a)
-> ST s (Maybe a)
popStack xs = do
c <- projectBranch (constrMB #_Cons) xs
forM c $ \(y, ys) -> do
o <- freezeRef y
moveRef xs ys
pure o
runST $ do
r <- thawRef $ 1 `Cons` 2 `Cons` 3 `Cons` Nil
y <- popStack r
(y,) <$> freezeRef r =>
(Just 1,Cons 2 (Cons 3 Nil))
Check out the getting started page, or the Haddock Documentation to jump right in! You can also read my introductory blog post about the motivations for this library and things I learned while developing it.