Instance Wrappers
The following newtype wrappers can imbue types with an automatic Mutable
instance, with different behaviors. They are useful in one of these two
situations:
One, if your type comes from an external library, and you still want to use
it as a Mutable, these newtype wrappers can be used to treat an external data
type as if it had a Mutable instance without actually giving them an orphan
Mutable instance.
For example, if an external library offered
newtype VecD = VecD (V.Vector Double)
and you don't want to give VecD an orphan instance, then you can use
CoerceMut VecD (V.Vector Double), instead. This has a proper Mutable
instance utilizes MVector, as it should.
Two, if you are leveraging GRef to build an automatic mutable version of
your data type but want to override the "default" behavior of a component.
For example, let's say you had a composite type:
data MyType = MT
{ mtInt :: Int
, mtDoube :: Double
, mtString :: String
}
deriving Generic
instance Mutable s MyType where
type Ref s MyType = GRef s MyType
This leverages the Mutable instances of Int, Double, and String.
However, the normal Mutable instance for String isn't too great: it uses a
mutable linked list (since it's a type alias for [Char]), which is a bit
over-kill. We can use the VarMut newtype wrapper to instead treat String
as a single object to be modified whole-wise instead of piecewise:
data MyType = MT
{ mtInt :: Int
, mtDoube :: Double
, mtString :: VarMut String
}
deriving Generic
instance Mutable s MyType where
type Ref s MyType = GRef s MyType
Now Ref s MyType is a composite data type of a MutVar s Int, a MutVar s
Double, and MutVar s String. VarMut overrides with a "whole-wise
mutation" instance.
VarMut
Overrides with (or provides) "whole-wise" mutation, eliminating any piecewise granularity.
The type VarMut String is a whole-wise mutating reference, and is essentially
MutVar s String. The example above shows a situation where this might be
useful.
CoerceMut
Overrides with (or provides) a mutation in terms of some equivalent type (usually, a newtype unwrapped version). Usually useful for providing an instance for external types. To repeat the example above:
newtype VecD = VecD (V.Vector Double)
Then CoerceMut VecD (V.Vector Double) has a Mutable instance that uses
MVector underneath.
TraverseMut
Overrides with (or provides) a mutation in terms of a type's Traversable
instance. See the information on TraverseRef in the previous
section for more information on the
details for how this instance works.
For example, the Mutable instance for Vector a is an MVector s a, where
each item is included in its "pure" form. But wouldn't it be nice if we
instead had a mutable Vector a instead be Vector (Ref s a), where every
slot contains a mutable value?
You can get that behavior with TraverseMut Vector a.
Immutable
This wrapper is typically used to override the mutation of a specific field when using generic derivation.
For example, looking at the type from above:
data MyType = MT
{ mtInt :: Int
, mtDoube :: Double
, mtString :: String
}
deriving Generic
Let's say you reaaaalllly don't want that mtString field to be mutable.
Like, at all. You don't want to allocate anything, and you want all copies
into it to be ignored and all freezes to return the original String.
In that case, you can use Immutable:
data MyType = MT
{ mtInt :: Int
, mtDoube :: Double
, mtString :: Immutable String
}
deriving Generic
instance Mutable s MyType where
type Ref s MyType = GRef s MyType
And now Ref s MyType will basically be a tupling of MutVar s Int, MutVar s
Double, and an immutable String. If you try to modify it, modifications
will be ignored. Freezing a Ref s MyType will get the original string back.
This does break a lot of the expectations of mutability, but sometimes this can be useful for low-level optimizations or hacks.