[Haskell-cafe] state updates and efficiency

Tim Newsham newsham at lava.net
Wed Dec 6 13:25:43 EST 2006

> I'll make a random comment...

Thanks for the comments!

>> instance MonadReader s (State s) where
>>   ask = get
>>   local f m = fmap (fst . runState m  . f) get

ahh, hadn't thought of doing that.

> You want performance but this pushes more work to the compiler.  If this were
> all functional instead of Monadic it might be simpler to start with.

My code has a lot of values in each record, maps of records, records
with lists in them, etc.  I actually wrote some of the code without
using the state monad to start.  It was a little complicated.  Rewriting
it with the state monad made it more modular and shorter (if you dont
count all of the lifters I had to write).

>>> withF1M = withInnerM f1 (\f r -> r {f1=f})
> Main *main comment* is to separate manipulating the complex data structure from
> the State commands.  And to make it more abstract:
> get1,get3,get4 :: T1 -> Int
> get2 :: T1 -> T2
> get1 = f1

> And note the different but important design choice:
>  You don't need to know how the data is nested to access a field.

My code doesnt only operate at the top-most layer.  There are some
complex state transformations that logically occur on one structure
thats nested down several layers.  So those operations are most
naturally written with a State monad on that object.  Other operations
are written at a higher layer but may still need access to some
lower layer state...

So, if I were to follow your suggestion, I would have to make
several variations of the accessors/modifiers depending on what
level I were to access them from.  One reason I went with the
lifters was to avoid any blow-up in the number of accessors I
had to write (and in hopes that later I can auto-generate them).

> If you insert a T1'and'a'half data field "between" T1 and T2 then you just need
> to update get2/put2 to fix (get|put|mod)(3|4) and this also fixes all the code
> that uses any of these functions.

Yes.  This is a downside.  I do have some abstractions that try
to hide this occasionally...

> -- My choice is to use a strict modify that also returns the new value
> modify' :: (MonadState a m) => (a -> a) -> m a
> modify' f = do x <- liftM f get
>               put $! x
>               return x
> -- Now tweakT1 can be a one-liner or longer
> tweakT1,tweakT1'long :: (MonadState T1 f) => Int -> Int -> f Int
> tweakT1 v1 v3 = liftM get4 (modify' (mod1 (+ v1) . mod3 (+ v3)))

I like it.

> The compiler may or may not optimize the mod1 and mod3 into one T1 construction
> instead of two. If you modify parts 1 and 3 of the state in tandem a lot then
> mod13 g1 g3 o@(T1 {f1=v1,f2=v2@(T2 {f3=v3})}) = o {f1=g1 v1,f2=v2 {f3=g3 v3}}

not the prettiest function in the bunch.  I can see maybe using
something like that if profiling said I really really needed it...

>>>   withRead $ withF2R $ withF4R $ ask
> And that was a very roundabout way to derive
> liftM get4 == withRead . withF2R . withF4R

true, but its composable and usable at all layers.  I could have written 
"withRead $ withF4R $ ask" in a State T2 a monad or "withRead $ withXXX $ 
withYYY $ withF2R $ withF4R $ ask" at a much higher level...

Thanks for your comments.  I need to think over my code to figure out if 
it makes sense to hide the nestings (I suspect the answer is "sometimes"), 
but I think I can definitely do that in some cases.

I'm starting to think that it might make sense for me to eliminate some of 
the nesting using cross-references (something simple like an integer 
ID with maps mapping IDs to objects at the top level).  Then perhaps most 
of my state manipulations will occur at the top level (among having other 
benefits)...  Starting to feel more DB-like.

Tim Newsham

More information about the Haskell-Cafe mailing list