Haskell State Accessors (second attempt: Composability)

Last week I introduced some constructs to make programming games with Haskell easier, mostly the idea of an accessor for dealing nicely with highly stateful functions. The (refined once) abstraction looked like this:

data Accessor s a
    = Accessor { getVal :: forall m. MonadState s m => m a
               , setVal :: forall m. MonadState s m => a -> m ()
               }

The theory was, you had your global state data structure, and you could define accessors into pieces of it. So for example, if you had the following state:

data Foo = Foo { p1score_ :: Int, p2score_ :: Int }

You (or a TH module) would define:

p1score = Accessor (gets p1score_) (x -> get >>= s -> s { p1score_ = x })
p2score = Accessor (gets p2score_) (x -> get >>= s -> s { p2score_ = x })

And then given the Accessor abstraction you could do stuff like define a := operator for readability and whatnot. I was proud that you could write accessors that accessed things other than the top level of the data structure.

But something still wasn’t right. I wanted to be able to do something like a.b.c in OO langauges, where a, b, and c were accessors. So here is my new Accessor abstraction:

The idea is that an Accessor is an object which accesses a value of type a as a function of a value of type s (I call it s for state, the typical value). But it no longer has anything to do with a monad, it’s an abstraction simply for extracting data from other data. The new signature is:

data Accessor s a
    = Accessor { getVal :: s -> a
               , setVal :: a -> s -> s
               }

getVal retrieves an a from an s, and setVal inserts an a into an already existing s.

UPDATE: I require the following laws to hold in order to ensure that this is actually behaving as an accessor:

getVal a (setVal a x s) == x
setVal a (getVal a s) s == s

As usual in Haskell, this simplified version is much more powerful than the more complex one above. In particular, we may define a composition operator:

(@.) :: Accessor a b -> Accessor b c -> Accessor a c
f @. g =
    Accessor { getVal = getVal g . getVal f
             , setVal = c a -> setVal f (setVal g c (getVal f a)) a
             }

That is to say, @. takes an accessor from a to b and from b to c and generates a way to access c from a, bidirectionally.

We may also define the previous State monady getVal and setVal like so:

getA :: MonadState s m => Accessor s a -> m a
getA acc = fmap (getVal acc) get

putA :: MonadState s m => Accessor s a -> a -> m ()
putA acc x = get >>= put . setVal acc x

modA :: MonadState s m => Accessor s a -> (a -> a) -> m ()
modA acc f = fmap f (getA acc) >>= putA acc

Given the appropriate (automatable) definition of accessors for foo and bar, we can do things like this:

data Foo =
    Foo { foo_ :: Foo
        , bar_ :: Int
        }

modA (foo @. bar) (+1)        -- increment foo.bar by 1
liftIO . print =<< foo @. foo @. foo @. bar

I plan to write a TH generator for such accessors in the near future.

Here is a test demonstrating the idea and showing that it can work.

2 thoughts on “Haskell State Accessors (second attempt: Composability)

  1. Very interesting concept :) One small question, however.. Have you actually tried this code? For instance, how do you create a Foo structure, or do you tie the knot?

    Btw, once you have setA, obviously it would be the perfect candidate for :=

    Cheers, and thanks for the interesting read :)

  2. I didn’t try the code at the end; having Foo refer to itself was just a lazy way to demonstrate @. without having to declare two separate datas. But creating a Foo is easy enough:

    let mkfoo n = Foo { foo_ = mkfoo (n+1), bar_ = n } in mkfoo 0
    

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s